Openstatus www.openstatus.dev

๐Ÿš€ More query rum (#795)

* ๐Ÿšง wip rum

* ๐Ÿ”ฅ more data

authored by

Thibault Le Ouay and committed by
GitHub
ef737fff 56a10519

+169 -8
+68
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/_components/route-table.tsx
··· 1 + import { 2 + Table, 3 + TableBody, 4 + TableCaption, 5 + TableCell, 6 + TableHead, 7 + TableHeader, 8 + TableRow, 9 + } from "@openstatus/ui"; 10 + 11 + import { api } from "@/trpc/server"; 12 + 13 + const RouteTable = async () => { 14 + const data = await api.rumRouter.GetAggregatedPerPage.query(); 15 + if (!data) { 16 + return null; 17 + } 18 + return ( 19 + <div className=" "> 20 + <h2 className="text-lg font-semibold">Page Performance</h2> 21 + <div className=" "> 22 + <Table> 23 + <TableCaption>An overview of your page performance.</TableCaption> 24 + <TableHeader> 25 + <TableRow className="sticky top-0"> 26 + <TableHead className="max-w-6 w-4">Page</TableHead> 27 + <TableHead>Total Events</TableHead> 28 + <TableHead>CLS</TableHead> 29 + <TableHead>FCP</TableHead> 30 + <TableHead>INP</TableHead> 31 + <TableHead>LCP</TableHead> 32 + <TableHead>TTFB</TableHead> 33 + </TableRow> 34 + </TableHeader> 35 + <TableBody> 36 + {data.map((page) => { 37 + return ( 38 + <TableRow key={`${page.href}`}> 39 + <TableCell className="max-w-6 w-2 truncate font-medium"> 40 + {page.href} 41 + </TableCell> 42 + <TableCell>{page.total_event}</TableCell> 43 + <TableCell className=""> 44 + {page.clsValue?.toFixed(2)} 45 + </TableCell> 46 + <TableCell className=""> 47 + {page.fcpValue ? (page.fcpValue / 1000).toFixed(2) : "-"} 48 + </TableCell> 49 + <TableCell className=""> 50 + {page.inpValue ? (page.inpValue / 1000).toFixed(2) : "-"} 51 + </TableCell> 52 + <TableCell className=""> 53 + {page.lcpValue ? (page.lcpValue / 1000).toFixed(2) : "-"} 54 + </TableCell> 55 + <TableCell className=""> 56 + {page.ttfbValue ? (page.ttfbValue / 1000).toFixed(2) : "-"} 57 + </TableCell> 58 + </TableRow> 59 + ); 60 + })} 61 + </TableBody> 62 + </Table> 63 + </div> 64 + </div> 65 + ); 66 + }; 67 + 68 + export { RouteTable };
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/_components/rum-metric-card.tsx
··· 22 22 {eventConfig.label} ({event}) 23 23 </p> 24 24 <p className="text-foreground text-3xl font-semibold"> 25 - {data?.median || 0} 25 + {data?.median.toFixed(2) || 0} 26 26 </p> 27 27 <CategoryBar 28 28 values={prepareWebVitalValues(eventConfig.values)}
+14 -6
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/page.tsx
··· 7 7 8 8 import { EmptyState } from "@/components/dashboard/empty-state"; 9 9 import { api } from "@/trpc/server"; 10 + import { RouteTable } from "./_components/route-table"; 10 11 import { RUMMetricCard } from "./_components/rum-metric-card"; 11 12 12 13 export const dynamic = "force-dynamic"; 13 14 14 15 export default async function RUMPage() { 15 16 const workspace = await api.workspace.getWorkspace.query(); 16 - 17 17 if (!workspace) { 18 18 return notFound(); 19 19 } ··· 39 39 } 40 40 41 41 return ( 42 - <div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-4"> 43 - {webVitalEvents.map((event) => ( 44 - <RUMMetricCard key={event} event={event} /> 45 - ))} 46 - </div> 42 + <> 43 + <div className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-5"> 44 + {webVitalEvents 45 + // Remove FID from the list of events because it's deprecated by google 46 + .filter((v) => v !== "FID") 47 + .map((event) => ( 48 + <RUMMetricCard key={event} event={event} /> 49 + ))} 50 + </div> 51 + <div> 52 + <RouteTable /> 53 + </div> 54 + </> 47 55 ); 48 56 }
+4 -1
apps/web/src/app/layout.tsx
··· 42 42 return ( 43 43 <html lang="en"> 44 44 <body className={`${inter.className} ${calSans.variable}`}> 45 - <OpenStatusProvider dsn="openstatus" /> 45 + {/* Only include RUM in prod */} 46 + {process.env.NODE_ENV === "production" && ( 47 + <OpenStatusProvider dsn="openstatus" /> 48 + )} 46 49 <ThemeProvider attribute="class" defaultTheme="light" enableSystem> 47 50 <Background>{children}</Background> 48 51 <Toaster richColors />
+82
packages/api/src/router/rum/index.ts
··· 4 4 5 5 const event = z.enum(["CLS", "FCP", "FID", "INP", "LCP", "TTFB"]); 6 6 7 + const RouteData = z.object({ 8 + href: z.string(), 9 + total_event: z.coerce.number(), 10 + clsValue: z.number().optional(), 11 + fcpValue: z.number().optional(), 12 + inpValue: z.number().optional(), 13 + lcpValue: z.number().optional(), 14 + ttfbValue: z.number().optional(), 15 + }); 16 + 7 17 export const rumRouter = createTRPCRouter({ 8 18 GetEventMetricsForWorkspace: protectedProcedure 9 19 .input( ··· 39 49 return null; 40 50 } 41 51 }), 52 + 53 + GetAggregatedPerPage: protectedProcedure.query(async (opts) => { 54 + const data = await opts.ctx.clickhouseClient.query({ 55 + query: ` 56 + select 57 + count(*) as total_event, 58 + href 59 + from 60 + cwv 61 + where 62 + dsn = '${opts.ctx.workspace.dsn}' 63 + group by 64 + href 65 + order by 66 + total_event desc 67 + limit 68 + 20 69 + `, 70 + format: "JSONEachRow", 71 + }); 72 + const result = await data.json(); 73 + const schema = z.array( 74 + z.object({ href: z.string(), total_event: z.coerce.number() }), 75 + ); 76 + if (!result) { 77 + return null; 78 + } 79 + const totalRoute = schema.parse(result); 80 + 81 + const allData = []; 82 + for (const currentRoute of totalRoute) { 83 + const pageData = await opts.ctx.clickhouseClient.query({ 84 + query: ` 85 + select 86 + quantile(0.5)(value) as value, 87 + event_name 88 + from 89 + cwv 90 + where 91 + dsn = '${opts.ctx.workspace.dsn}' 92 + and href = '${currentRoute.href}' 93 + group by 94 + event_name 95 + `, 96 + format: "JSONEachRow", 97 + }); 98 + const result = await pageData.json(); 99 + 100 + if (!result) { 101 + return null; 102 + } 103 + const schema = z.array( 104 + z.object({ event_name: event, value: z.number() }), 105 + ); 106 + const d = schema.parse(result); 107 + const r = d.reduce((acc, curr) => { 108 + acc = { 109 + ...acc, 110 + [`${String(curr.event_name).toLowerCase()}Value`]: curr.value, 111 + }; 112 + 113 + return acc; 114 + }); 115 + allData.push({ 116 + ...currentRoute, 117 + ...r, 118 + }); 119 + } 120 + // console.log(allData); 121 + // return; 122 + return z.array(RouteData).parse(allData); 123 + }), 42 124 });