report take screenshot

This commit is contained in:
Nate Kelley 2025-10-08 14:03:23 -06:00
parent 4fe31fab45
commit ee623169c0
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 146 additions and 40 deletions

View File

@ -47,7 +47,7 @@ const app = new Hono().get(
type: search.type,
supabaseCookieKey: c.get('supabaseCookieKey'),
supabaseUser: c.get('supabaseUser'),
accessToken: c.get('accessToken') || '',
accessToken: c.get('accessToken'),
organizationId: chat.organizationId,
});

View File

@ -141,9 +141,7 @@ export async function createChatHandler(
// Just queue the background job - should be <100ms
const taskHandle = await tasks.trigger(
'analyst-agent-task',
{
message_id: actualMessageId,
},
{ message_id: actualMessageId },
{
concurrencyKey: chatId, // Ensure sequential processing per chat
}

View File

@ -98,6 +98,13 @@ describe('getDashboardHandler', () => {
workspaceSharing: 'none',
};
// Mock Hono context
const mockContext = {
env: {},
get: vi.fn(),
set: vi.fn(),
} as any;
beforeEach(() => {
vi.clearAllMocks();
@ -163,7 +170,11 @@ describe('getDashboardHandler', () => {
describe('successful requests', () => {
it('should return dashboard data for valid dashboard ID', async () => {
const result = await getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser);
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123' },
mockUser,
mockContext
);
expect(result.dashboard.id).toBe('dashboard-123');
expect(result.dashboard.name).toBe('Test Dashboard');
@ -206,7 +217,8 @@ describe('getDashboardHandler', () => {
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123', versionNumber: 1 },
mockUser
mockUser,
mockContext
);
expect((result.dashboard.config as any).name).toBe('Version 1');
@ -227,7 +239,8 @@ describe('getDashboardHandler', () => {
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123', password: 'secret123' },
mockUser
mockUser,
mockContext
);
expect(result.permission).toBe('can_view');
@ -239,7 +252,7 @@ describe('getDashboardHandler', () => {
mockGetDashboardById.mockResolvedValue(null);
await expect(
getDashboardHandler({ dashboardId: 'nonexistent-dashboard' }, mockUser)
getDashboardHandler({ dashboardId: 'nonexistent-dashboard' }, mockUser, mockContext)
).rejects.toThrow(new HTTPException(404, { message: 'Dashboard not found' }));
});
@ -254,7 +267,9 @@ describe('getDashboardHandler', () => {
effectiveRole: undefined,
});
await expect(getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser)).rejects.toThrow(
await expect(
getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser, mockContext)
).rejects.toThrow(
new HTTPException(403, { message: "You don't have permission to view this dashboard" })
);
});
@ -274,7 +289,9 @@ describe('getDashboardHandler', () => {
effectiveRole: undefined,
});
await expect(getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser)).rejects.toThrow(
await expect(
getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser, mockContext)
).rejects.toThrow(
new HTTPException(403, { message: 'Public access to this dashboard has expired' })
);
});
@ -291,9 +308,9 @@ describe('getDashboardHandler', () => {
effectiveRole: undefined,
});
await expect(getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser)).rejects.toThrow(
new HTTPException(418, { message: 'Password required for public access' })
);
await expect(
getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser, mockContext)
).rejects.toThrow(new HTTPException(418, { message: 'Password required for public access' }));
});
it('should throw 403 when incorrect password is provided', async () => {
@ -309,7 +326,11 @@ describe('getDashboardHandler', () => {
});
await expect(
getDashboardHandler({ dashboardId: 'dashboard-123', password: 'wrong-password' }, mockUser)
getDashboardHandler(
{ dashboardId: 'dashboard-123', password: 'wrong-password' },
mockUser,
mockContext
)
).rejects.toThrow(
new HTTPException(403, { message: 'Incorrect password for public access' })
);
@ -317,7 +338,11 @@ describe('getDashboardHandler', () => {
it('should throw 404 when requested version does not exist', async () => {
await expect(
getDashboardHandler({ dashboardId: 'dashboard-123', versionNumber: 99 }, mockUser)
getDashboardHandler(
{ dashboardId: 'dashboard-123', versionNumber: 99 },
mockUser,
mockContext
)
).rejects.toThrow(new HTTPException(404, { message: 'Version 99 not found' }));
});
@ -335,7 +360,11 @@ describe('getDashboardHandler', () => {
mockGetDashboardById.mockResolvedValue(versionedDashboard);
await expect(
getDashboardHandler({ dashboardId: 'dashboard-123', versionNumber: 1 }, mockUser)
getDashboardHandler(
{ dashboardId: 'dashboard-123', versionNumber: 1 },
mockUser,
mockContext
)
).rejects.toThrow(new HTTPException(404, { message: 'Version 1 not found' }));
});
});
@ -364,7 +393,11 @@ describe('getDashboardHandler', () => {
};
mockGetDashboardById.mockResolvedValue(versionedDashboard);
const result = await getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser);
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123' },
mockUser,
mockContext
);
expect(result.versions).toEqual([
{ version_number: 1, updated_at: '2023-01-01T00:00:00Z' },
@ -391,7 +424,11 @@ describe('getDashboardHandler', () => {
};
mockGetDashboardById.mockResolvedValue(versionedDashboard);
const result = await getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser);
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123' },
mockUser,
mockContext
);
expect(result.dashboard.version_number).toBe(2);
expect(result.dashboard.config).toEqual(mockDashboardContent); // From current content
@ -405,7 +442,11 @@ describe('getDashboardHandler', () => {
effectiveRole: 'can_edit',
});
const result = await getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser);
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123' },
mockUser,
mockContext
);
expect(result.permission).toBe('can_edit');
});
@ -421,7 +462,11 @@ describe('getDashboardHandler', () => {
effectiveRole: undefined,
});
const result = await getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser);
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123' },
mockUser,
mockContext
);
expect(result.permission).toBe('can_view');
});
@ -429,7 +474,11 @@ describe('getDashboardHandler', () => {
describe('response structure', () => {
it('should return properly formatted dashboard data', async () => {
const result = await getDashboardHandler({ dashboardId: 'dashboard-123' }, mockUser);
const result = await getDashboardHandler(
{ dashboardId: 'dashboard-123' },
mockUser,
mockContext
);
expect(result).toMatchObject({
dashboard: {

View File

@ -1,3 +1,5 @@
import { screenshots_task_keys } from '@buster-app/trigger/task-keys';
import type { TakeDashboardScreenshotTrigger } from '@buster-app/trigger/task-schemas';
import { checkPermission } from '@buster/access-controls';
import {
type User,
@ -14,7 +16,8 @@ import {
import type { DashboardYml } from '@buster/server-shared/dashboards';
import type { VerificationStatus } from '@buster/server-shared/share';
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import { tasks } from '@trigger.dev/sdk';
import { type Context, Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import yaml from 'js-yaml';
import { throwUnauthorizedError } from '../../../../shared-helpers/asset-public-access';
@ -46,7 +49,8 @@ const app = new Hono().get(
versionNumber: version_number,
password,
},
user
user,
c
);
return c.json(response);
@ -61,7 +65,8 @@ export default app;
*/
export async function getDashboardHandler(
params: GetDashboardHandlerParams,
user: User
user: User,
c: Context
): Promise<GetDashboardResponse> {
const { dashboardId, versionNumber, password } = params;
@ -241,6 +246,18 @@ export async function getDashboardHandler(
workspace_member_count: workspaceMemberCount,
};
await tasks.trigger(
screenshots_task_keys.take_dashboard_screenshot,
{
dashboardId,
isOnSaveEvent: false,
organizationId: dashboardFile.organizationId,
supabaseCookieKey: c.get('supabaseCookieKey'),
supabaseUser: c.get('supabaseUser'),
accessToken: c.get('accessToken'),
} satisfies TakeDashboardScreenshotTrigger,
{ concurrencyKey: `take-dashboard-screenshot-${dashboardId}-${versionNumber}` }
);
return response;
}

View File

@ -1,5 +1,5 @@
import { checkPermission } from '@buster/access-controls';
import { getDashboardById, getUserOrganizationId } from '@buster/database/queries';
import { getDashboardById } from '@buster/database/queries';
import {
GetDashboardScreenshotParamsSchema,
GetDashboardScreenshotQuerySchema,
@ -48,7 +48,7 @@ const app = new Hono()
dashboardId,
supabaseCookieKey: c.get('supabaseCookieKey'),
supabaseUser: c.get('supabaseUser'),
accessToken: c.get('accessToken') || '',
accessToken: c.get('accessToken'),
organizationId: dashboard.organizationId,
});

View File

@ -10,14 +10,15 @@ import type { User } from '@buster/database/queries';
import type { GetDashboardResponse } from '@buster/server-shared/dashboards';
import { type ShareUpdateRequest, ShareUpdateRequestSchema } from '@buster/server-shared/share';
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import { type Context, Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { getDashboardHandler } from '../GET';
export async function updateDashboardShareHandler(
dashboardId: string,
request: ShareUpdateRequest,
user: User & { organizationId: string }
user: User & { organizationId: string },
c: Context
) {
// Check if dashboard exists
const dashboard = await getDashboardById({ dashboardId });
@ -104,7 +105,11 @@ export async function updateDashboardShareHandler(
workspace_sharing,
});
const updatedDashboard: GetDashboardResponse = await getDashboardHandler({ dashboardId }, user);
const updatedDashboard: GetDashboardResponse = await getDashboardHandler(
{ dashboardId },
user,
c
);
return updatedDashboard;
}
@ -130,7 +135,8 @@ const app = new Hono().put('/', zValidator('json', ShareUpdateRequestSchema), as
{
...user,
organizationId: userOrg.organizationId,
}
},
c
);
return c.json(updatedDashboard);

View File

@ -27,20 +27,20 @@ const app = new Hono()
password
);
const organizationId =
(await getUserOrganizationId(user.id).then((res) => res?.organizationId)) || '';
await tasks.trigger(
screenshots_task_keys.take_metric_screenshot,
{
metricId: id,
isOnSaveEvent: true,
isOnSaveEvent: false,
supabaseCookieKey: c.get('supabaseCookieKey'),
supabaseUser: c.get('supabaseUser'),
accessToken: c.get('accessToken') || '',
organizationId:
(await getUserOrganizationId(user.id).then((res) => res?.organizationId)) || '',
accessToken: c.get('accessToken'),
organizationId,
} satisfies TakeMetricScreenshotTrigger,
{
idempotencyKey: `take-metric-screenshot-${id}`,
}
{ concurrencyKey: `take-metric-screenshot-${id}-${version_number}` }
);
return c.json(response);

View File

@ -50,7 +50,7 @@ const app = new Hono()
type,
supabaseCookieKey: c.get('supabaseCookieKey'),
supabaseUser: c.get('supabaseUser'),
accessToken: c.get('accessToken') || '',
accessToken: c.get('accessToken'),
organizationId: metric.organizationId,
});

View File

@ -1,11 +1,19 @@
import { screenshots_task_keys } from '@buster-app/trigger/task-keys';
import { TakeReportScreenshotTrigger } from '@buster-app/trigger/task-schemas';
import { checkPermission } from '@buster/access-controls';
import { type User, getMetricIdsInReport, getReportFileById } from '@buster/database/queries';
import {
type User,
getMetricIdsInReport,
getReportFileById,
getUserOrganizationId,
} from '@buster/database/queries';
import {
GetReportParamsSchema,
GetReportQuerySchema,
type GetReportResponse,
} from '@buster/server-shared/reports';
import { zValidator } from '@hono/zod-validator';
import { tasks } from '@trigger.dev/sdk';
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { throwUnauthorizedError } from '../../../../shared-helpers/asset-public-access';
@ -81,6 +89,19 @@ const app = new Hono()
versionNumber,
password
);
await tasks.trigger(
screenshots_task_keys.take_report_screenshot,
{
reportId,
organizationId: (await getUserOrganizationId(user.id))?.organizationId || '',
supabaseCookieKey: c.get('supabaseCookieKey'),
supabaseUser: c.get('supabaseUser'),
accessToken: c.get('accessToken'),
} satisfies TakeReportScreenshotTrigger,
{ concurrencyKey: `take-report-screenshot-${reportId}-${versionNumber}` }
);
return c.json(response);
}
)

View File

@ -41,6 +41,6 @@ declare module 'hono' {
/**
* The access token for the user. Set by the requireAuth middleware.
*/
readonly accessToken?: string;
readonly accessToken: string;
}
}

View File

@ -3,3 +3,18 @@ export {
TakeMetricScreenshotTriggerSchema,
type TakeMetricScreenshotTrigger,
} from './tasks/screenshots/schemas';
export {
TakeDashboardScreenshotTriggerSchema,
type TakeDashboardScreenshotTrigger,
} from './tasks/screenshots/schemas';
export {
TakeReportScreenshotTriggerSchema,
type TakeReportScreenshotTrigger,
} from './tasks/screenshots/schemas';
export {
TakeChartScreenshotTriggerSchema,
type TakeChartScreenshotTrigger,
} from './tasks/screenshots/schemas';