diff --git a/apps/web/src/api/server-functions/screenshots/shared-schema.ts b/apps/web/src/api/server-functions/screenshots/shared-schema.ts new file mode 100644 index 000000000..2ed9fe8ca --- /dev/null +++ b/apps/web/src/api/server-functions/screenshots/shared-schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const GetScreenshotRequestSharedSchema = z.object({}); + +export type GetScreenshotRequestShared = z.infer; diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index c2df03b6c..db68345ac 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -18,6 +18,7 @@ import { Route as AuthRouteImport } from './routes/auth' import { Route as AppRouteImport } from './routes/app' import { Route as IndexRouteImport } from './routes/index' import { Route as AppIndexRouteImport } from './routes/app/index' +import { Route as ScreenshotsHelloWorldRouteImport } from './routes/screenshots/hello-world' import { Route as InfoGettingStartedRouteImport } from './routes/info/getting-started' import { Route as AuthResetPasswordRouteImport } from './routes/auth.reset-password' import { Route as AuthLogoutRouteImport } from './routes/auth.logout' @@ -154,6 +155,7 @@ import { Route as AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdCont import { Route as AppAppAssetChatsChatIdDashboardsDashboardIdMetricsMetricIdContentChartRouteImport } from './routes/app/_app/_asset/chats.$chatId/dashboards.$dashboardId/metrics.$metricId/_content/chart' import { ServerRoute as AuthConfirmServerRouteImport } from './routes/auth.confirm' import { ServerRoute as AuthCallbackServerRouteImport } from './routes/auth.callback' +import { ServerRoute as ScreenshotsMetricsMetricIdServerRouteImport } from './routes/screenshots/metrics.$metricId' const EmbedChatChatIdReportsReportIdRouteImport = createFileRoute( '/embed/chat/$chatId/reports/$reportId', @@ -235,6 +237,11 @@ const AppIndexRoute = AppIndexRouteImport.update({ path: '/', getParentRoute: () => AppRoute, } as any) +const ScreenshotsHelloWorldRoute = ScreenshotsHelloWorldRouteImport.update({ + id: '/screenshots/hello-world', + path: '/screenshots/hello-world', + getParentRoute: () => rootRouteImport, +} as any) const InfoGettingStartedRoute = InfoGettingStartedRouteImport.update({ id: '/info/getting-started', path: '/info/getting-started', @@ -1180,6 +1187,12 @@ const AuthCallbackServerRoute = AuthCallbackServerRouteImport.update({ path: '/auth/callback', getParentRoute: () => rootServerRouteImport, } as any) +const ScreenshotsMetricsMetricIdServerRoute = + ScreenshotsMetricsMetricIdServerRouteImport.update({ + id: '/screenshots/metrics/$metricId', + path: '/screenshots/metrics/$metricId', + getParentRoute: () => rootServerRouteImport, + } as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -1191,6 +1204,7 @@ export interface FileRoutesByFullPath { '/auth/logout': typeof AuthLogoutRoute '/auth/reset-password': typeof AuthResetPasswordRoute '/info/getting-started': typeof InfoGettingStartedRoute + '/screenshots/hello-world': typeof ScreenshotsHelloWorldRoute '/app/': typeof AppIndexRoute '/app/home': typeof AppAppHomeRouteWithChildren '/app/new-user': typeof AppAppNewUserRouteWithChildren @@ -1327,6 +1341,7 @@ export interface FileRoutesByTo { '/auth/logout': typeof AuthLogoutRoute '/auth/reset-password': typeof AuthResetPasswordRoute '/info/getting-started': typeof InfoGettingStartedRoute + '/screenshots/hello-world': typeof ScreenshotsHelloWorldRoute '/app/test-pagination': typeof AppAppTestPaginationRoute '/embed/dashboard/$dashboardId': typeof EmbedDashboardDashboardIdRoute '/embed/metric/$metricId': typeof EmbedMetricMetricIdRoute @@ -1440,6 +1455,7 @@ export interface FileRoutesById { '/auth/logout': typeof AuthLogoutRoute '/auth/reset-password': typeof AuthResetPasswordRoute '/info/getting-started': typeof InfoGettingStartedRoute + '/screenshots/hello-world': typeof ScreenshotsHelloWorldRoute '/app/': typeof AppIndexRoute '/app/_app/_asset': typeof AppAppAssetRouteWithChildren '/app/_app/home': typeof AppAppHomeRouteWithChildren @@ -1597,6 +1613,7 @@ export interface FileRouteTypes { | '/auth/logout' | '/auth/reset-password' | '/info/getting-started' + | '/screenshots/hello-world' | '/app/' | '/app/home' | '/app/new-user' @@ -1733,6 +1750,7 @@ export interface FileRouteTypes { | '/auth/logout' | '/auth/reset-password' | '/info/getting-started' + | '/screenshots/hello-world' | '/app/test-pagination' | '/embed/dashboard/$dashboardId' | '/embed/metric/$metricId' @@ -1845,6 +1863,7 @@ export interface FileRouteTypes { | '/auth/logout' | '/auth/reset-password' | '/info/getting-started' + | '/screenshots/hello-world' | '/app/' | '/app/_app/_asset' | '/app/_app/home' @@ -1998,31 +2017,43 @@ export interface RootRouteChildren { EmbedRoute: typeof EmbedRouteWithChildren HealthcheckRoute: typeof HealthcheckRoute InfoGettingStartedRoute: typeof InfoGettingStartedRoute + ScreenshotsHelloWorldRoute: typeof ScreenshotsHelloWorldRoute } export interface FileServerRoutesByFullPath { '/auth/callback': typeof AuthCallbackServerRoute '/auth/confirm': typeof AuthConfirmServerRoute + '/screenshots/metrics/$metricId': typeof ScreenshotsMetricsMetricIdServerRoute } export interface FileServerRoutesByTo { '/auth/callback': typeof AuthCallbackServerRoute '/auth/confirm': typeof AuthConfirmServerRoute + '/screenshots/metrics/$metricId': typeof ScreenshotsMetricsMetricIdServerRoute } export interface FileServerRoutesById { __root__: typeof rootServerRouteImport '/auth/callback': typeof AuthCallbackServerRoute '/auth/confirm': typeof AuthConfirmServerRoute + '/screenshots/metrics/$metricId': typeof ScreenshotsMetricsMetricIdServerRoute } export interface FileServerRouteTypes { fileServerRoutesByFullPath: FileServerRoutesByFullPath - fullPaths: '/auth/callback' | '/auth/confirm' + fullPaths: + | '/auth/callback' + | '/auth/confirm' + | '/screenshots/metrics/$metricId' fileServerRoutesByTo: FileServerRoutesByTo - to: '/auth/callback' | '/auth/confirm' - id: '__root__' | '/auth/callback' | '/auth/confirm' + to: '/auth/callback' | '/auth/confirm' | '/screenshots/metrics/$metricId' + id: + | '__root__' + | '/auth/callback' + | '/auth/confirm' + | '/screenshots/metrics/$metricId' fileServerRoutesById: FileServerRoutesById } export interface RootServerRouteChildren { AuthCallbackServerRoute: typeof AuthCallbackServerRoute AuthConfirmServerRoute: typeof AuthConfirmServerRoute + ScreenshotsMetricsMetricIdServerRoute: typeof ScreenshotsMetricsMetricIdServerRoute } declare module '@tanstack/react-router' { @@ -2069,6 +2100,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppIndexRouteImport parentRoute: typeof AppRoute } + '/screenshots/hello-world': { + id: '/screenshots/hello-world' + path: '/screenshots/hello-world' + fullPath: '/screenshots/hello-world' + preLoaderRoute: typeof ScreenshotsHelloWorldRouteImport + parentRoute: typeof rootRouteImport + } '/info/getting-started': { id: '/info/getting-started' path: '/info/getting-started' @@ -3130,6 +3168,13 @@ declare module '@tanstack/react-start/server' { preLoaderRoute: typeof AuthCallbackServerRouteImport parentRoute: typeof rootServerRouteImport } + '/screenshots/metrics/$metricId': { + id: '/screenshots/metrics/$metricId' + path: '/screenshots/metrics/$metricId' + fullPath: '/screenshots/metrics/$metricId' + preLoaderRoute: typeof ScreenshotsMetricsMetricIdServerRouteImport + parentRoute: typeof rootServerRouteImport + } } } @@ -4140,6 +4185,7 @@ const rootRouteChildren: RootRouteChildren = { EmbedRoute: EmbedRouteWithChildren, HealthcheckRoute: HealthcheckRoute, InfoGettingStartedRoute: InfoGettingStartedRoute, + ScreenshotsHelloWorldRoute: ScreenshotsHelloWorldRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) @@ -4147,6 +4193,7 @@ export const routeTree = rootRouteImport const rootServerRouteChildren: RootServerRouteChildren = { AuthCallbackServerRoute: AuthCallbackServerRoute, AuthConfirmServerRoute: AuthConfirmServerRoute, + ScreenshotsMetricsMetricIdServerRoute: ScreenshotsMetricsMetricIdServerRoute, } export const serverRouteTree = rootServerRouteImport ._addFileChildren(rootServerRouteChildren) diff --git a/apps/web/src/routes/screenshots/hello-world.tsx b/apps/web/src/routes/screenshots/hello-world.tsx new file mode 100644 index 000000000..3b2c14bd7 --- /dev/null +++ b/apps/web/src/routes/screenshots/hello-world.tsx @@ -0,0 +1,24 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { useMount } from '@/hooks/useMount'; + +export const Route = createFileRoute('/screenshots/hello-world')({ + component: RouteComponent, +}); + +function RouteComponent() { + useMount(() => { + // getMetricScreenshot({ + // data: { + // metricId: '123', + // }, + // }).then((res) => { + // console.log(res); + // }); + }); + + return ( +
+ Hello "/screenshot/hello-world"! +
+ ); +} diff --git a/apps/web/src/routes/screenshots/metrics.$metricId.tsx b/apps/web/src/routes/screenshots/metrics.$metricId.tsx new file mode 100644 index 000000000..fd5c5a2ff --- /dev/null +++ b/apps/web/src/routes/screenshots/metrics.$metricId.tsx @@ -0,0 +1,59 @@ +import { createServerFileRoute } from '@tanstack/react-start/server'; +import { chromium } from 'playwright'; +import { z } from 'zod'; +import { Route as HelloWorldRoute } from './hello-world'; + +const GetMetricScreenshotParamsSchema = z.object({ + metricId: z.string(), +}); + +const GetMetricScreenshotQuerySchema = z.object({ + version_number: z.coerce.number().min(1).optional(), + width: z.coerce.number().min(100).max(3840).default(800), + height: z.coerce.number().min(100).max(2160).default(450), +}); + +export const ServerRoute = createServerFileRoute('/screenshots/metrics/$metricId').methods({ + GET: async ({ request, params }) => { + console.time('capture screenshot'); + const { metricId } = GetMetricScreenshotParamsSchema.parse(params); + const { version_number, width, height } = GetMetricScreenshotQuerySchema.parse( + Object.fromEntries(new URL(request.url).searchParams) + ); + const origin = new URL(request.url).origin; + console.timeLog('capture screenshot', 'params parsed'); + const browser = await chromium.launch(); + console.timeLog('capture screenshot', 'browser launched'); + try { + const page = await browser.newPage({ + viewport: { width, height }, + }); + console.timeLog('capture screenshot', 'page created'); + + const fullPath = `${origin}${HelloWorldRoute.fullPath}`; + await page.goto(fullPath, { waitUntil: 'networkidle' }); + console.timeLog('capture screenshot', 'page navigated'); + const screenshotBuffer = await page.screenshot({ + type: 'png', + }); + console.timeLog('capture screenshot', 'screenshot taken'); + console.timeEnd('capture screenshot'); + return new Response(new Uint8Array(screenshotBuffer), { + headers: { + 'Content-Type': 'image/png', + 'Content-Length': screenshotBuffer.length.toString(), + }, + }); + } catch (error) { + // console.error('Error capturing metric screenshot', error); + return new Response( + JSON.stringify({ + message: 'Failed to capture screenshot', + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } finally { + await browser.close(); + } + }, +});