Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

๐Ÿ”ฅ Add inp (#846)

* ๐Ÿšง wip rum

* ๐Ÿšง wip rum

* ๐Ÿšง wip rum

authored by

Thibault Le Ouay and committed by
GitHub
9422a6cb f0f51084

+435 -74
+1 -12
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 1 import { api } from "@/trpc/server"; 12 - import { timeFormater } from "./util"; 13 2 import { DataTableWrapper } from "./data-table-wrapper"; 14 3 15 4 const RouteTable = async ({ dsn }: { dsn: string }) => { 16 5 const data = await api.tinybird.rumMetricsForApplicationPerPage.query({ 17 6 dsn: dsn, 7 + period: "24h", 18 8 }); 19 9 if (!data) { 20 10 return null; 21 11 } 22 - console.log(data.length); 23 12 return ( 24 13 <div className=""> 25 14 <DataTableWrapper data={data} />
+35
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/_components/rum-card.tsx
··· 1 + import { Card } from "@tremor/react"; 2 + import { getColorByType, webVitalsConfig } from "@openstatus/rum"; 3 + import type { WebVitalEvents, WebVitalsValues } from "@openstatus/rum"; 4 + import { CategoryBar } from "./category-bar"; 5 + 6 + export function prepareWebVitalValues(values: WebVitalsValues) { 7 + return values.map((value) => ({ 8 + ...value, 9 + color: getColorByType(value.type), 10 + })); 11 + } 12 + 13 + export const RUMCard = async ({ 14 + event, 15 + value, 16 + }: { 17 + event: WebVitalEvents; 18 + value: number; 19 + }) => { 20 + const eventConfig = webVitalsConfig[event]; 21 + return ( 22 + <Card> 23 + <p className="text-muted-foreground text-sm"> 24 + {eventConfig.label} ({event}) 25 + </p> 26 + <p className="font-semibold text-3xl text-foreground"> 27 + {event !== "CLS" ? value.toFixed(0) : value.toFixed(2) || 0} 28 + </p> 29 + <CategoryBar 30 + values={prepareWebVitalValues(eventConfig.values)} 31 + marker={value || 0} 32 + /> 33 + </Card> 34 + ); 35 + };
+7 -39
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/_components/rum-metric-card.tsx
··· 1 - import { Card } from "@tremor/react"; 2 - 3 - import { getColorByType, webVitalsConfig } from "@openstatus/rum"; 4 - import type { WebVitalEvents, WebVitalsValues } from "@openstatus/rum"; 1 + import { getColorByType } from "@openstatus/rum"; 2 + import type { WebVitalsValues } from "@openstatus/rum"; 5 3 6 4 import { api } from "@/trpc/server"; 7 - import { CategoryBar } from "./category-bar"; 8 - import { date } from "zod"; 9 - import { latencyFormatter } from "@/components/ping-response-analysis/utils"; 10 - 11 - function prepareWebVitalValues(values: WebVitalsValues) { 12 - return values.map((value) => ({ 13 - ...value, 14 - color: getColorByType(value.type), 15 - })); 16 - } 17 - 18 - const RUMCard = async ({ 19 - event, 20 - value, 21 - }: { 22 - event: WebVitalEvents; 23 - value: number; 24 - }) => { 25 - const eventConfig = webVitalsConfig[event]; 26 - return ( 27 - <Card> 28 - <p className="text-muted-foreground text-sm"> 29 - {eventConfig.label} ({event}) 30 - </p> 31 - <p className="font-semibold text-3xl text-foreground"> 32 - {event !== "CLS" ? value.toFixed(0) : value.toFixed(2) || 0} 33 - </p> 34 - <CategoryBar 35 - values={prepareWebVitalValues(eventConfig.values)} 36 - marker={value || 0} 37 - /> 38 - </Card> 39 - ); 40 - }; 5 + import { RUMCard } from "./rum-card"; 41 6 42 7 export const RUMMetricCards = async ({ dsn }: { dsn: string }) => { 43 8 const data = await api.tinybird.totalRumMetricsForApplication.query({ 44 9 dsn: dsn, 10 + period: "24h", 45 11 }); 46 12 return ( 47 - <div className="grid grid-cols-1 gap-2 lg:grid-cols-4 md:grid-cols-2"> 13 + <div className="grid grid-cols-1 gap-2 lg:grid-cols-5 md:grid-cols-2"> 48 14 <RUMCard event="CLS" value={data?.cls || 0} /> 49 15 <RUMCard event="FCP" value={data?.fcp || 0} /> 16 + <RUMCard event="INP" value={data?.inp || 0} /> 17 + 50 18 <RUMCard event="LCP" value={data?.lcp || 0} /> 51 19 <RUMCard event="TTFB" value={data?.ttfb || 0} /> 52 20 </div>
+12
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/overview/_components/data-table-wrapper.tsx
··· 1 + import { columns } from "@/components/data-table/session/columns"; 2 + import { DataTable } from "@/components/data-table/session/data-table"; 3 + import type { sessionRumPageQuery } from "@openstatus/tinybird/src/validation"; 4 + import type { z } from "zod"; 5 + 6 + export const DataTableWrapper = ({ 7 + data, 8 + }: { 9 + data: z.infer<typeof sessionRumPageQuery>[]; 10 + }) => { 11 + return <DataTable columns={columns} data={data} />; 12 + };
+35
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/overview/_components/path-card.tsx
··· 1 + import { api } from "@/trpc/server"; 2 + import { useSearchParams } from "next/navigation"; 3 + import { use } from "react"; 4 + import { RUMCard } from "../../_components/rum-card"; 5 + 6 + export const PathCard = async ({ 7 + dsn, 8 + path, 9 + }: { 10 + dsn: string; 11 + path: string; 12 + }) => { 13 + if (!path) { 14 + return null; 15 + } 16 + 17 + const data = await api.tinybird.rumMetricsForPath.query({ 18 + dsn, 19 + path, 20 + period: "24h", 21 + }); 22 + if (!data) { 23 + return null; 24 + } 25 + return ( 26 + <div className="grid grid-cols-1 gap-2 lg:grid-cols-5 md:grid-cols-2"> 27 + <RUMCard event="CLS" value={data?.cls || 0} /> 28 + <RUMCard event="FCP" value={data?.fcp || 0} /> 29 + <RUMCard event="INP" value={data?.inp || 0} /> 30 + 31 + <RUMCard event="LCP" value={data?.lcp || 0} /> 32 + <RUMCard event="TTFB" value={data?.ttfb || 0} /> 33 + </div> 34 + ); 35 + };
+24
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/overview/_components/session-table.tsx
··· 1 + import { api } from "@/trpc/server"; 2 + import { DataTableWrapper } from "./data-table-wrapper"; 3 + import { useSearchParams } from "next/navigation"; 4 + import { use } from "react"; 5 + 6 + const SessionTable = async ({ dsn, path }: { dsn: string; path: string }) => { 7 + const data = await api.tinybird.sessionRumMetricsForPath.query({ 8 + dsn: dsn, 9 + period: "24h", 10 + path: path, 11 + }); 12 + 13 + if (!data) { 14 + return null; 15 + } 16 + 17 + return ( 18 + <div> 19 + <DataTableWrapper data={data} /> 20 + </div> 21 + ); 22 + }; 23 + 24 + export { SessionTable };
+40
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/overview/page.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { PathCard } from "./_components/path-card"; 4 + import { api } from "@/trpc/server"; 5 + import { Suspense } from "react"; 6 + import { Skeleton } from "@openstatus/ui/src/components/skeleton"; 7 + import Loading from "../loading"; 8 + import { auth } from "@/lib/auth"; 9 + import { SessionTable } from "./_components/session-table"; 10 + import { z } from "zod"; 11 + 12 + const searchParamsSchema = z.object({ 13 + path: z.string(), 14 + }); 15 + 16 + export default async function RUMPage({ 17 + searchParams, 18 + }: { 19 + searchParams: { [key: string]: string | string[] | undefined }; 20 + }) { 21 + const session = await auth(); 22 + if (!session?.user) { 23 + return <Loading />; 24 + } 25 + 26 + const data = searchParamsSchema.parse(searchParams); 27 + const applications = await api.workspace.getApplicationWorkspaces.query(); 28 + if (applications.length === 0 || !applications[0].dsn) { 29 + return null; 30 + } 31 + 32 + return ( 33 + <> 34 + <PathCard dsn={applications[0].dsn} path={data.path} /> 35 + <div> 36 + <SessionTable dsn={applications[0].dsn} path={data.path} /> 37 + </div> 38 + </> 39 + ); 40 + }
-5
apps/web/src/app/app/[workspaceSlug]/(dashboard)/rum/page.tsx
··· 1 - import Link from "next/link"; 2 - import { notFound } from "next/navigation"; 3 1 import * as React from "react"; 4 - 5 - import { webVitalEvents } from "@openstatus/rum"; 6 - import { Button } from "@openstatus/ui"; 7 2 8 3 import { EmptyState } from "@/components/dashboard/empty-state"; 9 4 import { api } from "@/trpc/server";
-2
apps/web/src/app/status-page/[domain]/badge/route.tsx
··· 50 50 const { status } = await getStatus(params.domain); 51 51 const theme = req.nextUrl.searchParams.get("theme"); 52 52 const size = req.nextUrl.searchParams.get("size"); 53 - console.log(size); 54 53 let s = SIZE.sm; 55 54 if (size) { 56 55 if (SIZE[size]) { ··· 58 57 } 59 58 } 60 59 const { label, color } = statusDictionary[status]; 61 - console.log(s); 62 60 const light = "border-gray-200 text-gray-700 bg-white"; 63 61 const dark = "border-gray-800 text-gray-300 bg-gray-900"; 64 62
+22 -5
apps/web/src/components/data-table/rum/columns.tsx
··· 13 13 cell: ({ row }) => { 14 14 return ( 15 15 <Link 16 - href={"./status-reports/overview"} 16 + href={`./rum/overview?path=${encodeURIComponent(row.original.path)}`} 17 17 className="w-8 max-w-8 hover:underline" 18 18 > 19 19 <span className="truncate">{row.getValue("path")}</span> ··· 32 32 accessorKey: "cls", 33 33 header: "CLS", 34 34 cell: ({ row }) => { 35 - return <code>{row.original.cls.toFixed(2)}</code>; 35 + return ( 36 + <code>{row.original.cls ? row.original.cls.toFixed(2) : "-"}</code> 37 + ); 36 38 }, 37 39 }, 38 40 { 39 41 accessorKey: "fcp", 40 42 header: "FCP", 41 43 cell: ({ row }) => { 42 - return <code>{row.original.fcp.toFixed(0)}</code>; 44 + return ( 45 + <code>{row.original.fcp ? row.original.fcp.toFixed(0) : "-"} </code> 46 + ); 47 + }, 48 + }, 49 + { 50 + accessorKey: "inp", 51 + header: "INP", 52 + cell: ({ row }) => { 53 + return ( 54 + <code>{row.original.inp ? row.original.inp.toFixed(0) : "-"}</code> 55 + ); 43 56 }, 44 57 }, 45 58 { 46 59 accessorKey: "lcp", 47 60 header: "LCP", 48 61 cell: ({ row }) => { 49 - return <code>{row.original.lcp.toFixed(0)}</code>; 62 + return ( 63 + <code>{row.original.lcp ? row.original.lcp.toFixed(0) : "-"}</code> 64 + ); 50 65 }, 51 66 }, 52 67 { 53 68 accessorKey: "ttfb", 54 69 header: "TTFB", 55 70 cell: ({ row }) => { 56 - return <code>{row.original.ttfb.toFixed(0)}</code>; 71 + return ( 72 + <code>{row.original.ttfb ? row.original.ttfb.toFixed(0) : "-"}</code> 73 + ); 57 74 }, 58 75 }, 59 76 // {
+69
apps/web/src/components/data-table/session/columns.tsx
··· 1 + "use client"; 2 + 3 + import type { ColumnDef } from "@tanstack/react-table"; 4 + import Link from "next/link"; 5 + 6 + import type { sessionRumPageQuery } from "@openstatus/tinybird/src/validation"; 7 + import type { z } from "zod"; 8 + 9 + export const columns: ColumnDef<z.infer<typeof sessionRumPageQuery>>[] = [ 10 + { 11 + accessorKey: "session", 12 + header: "Session", 13 + cell: ({ row }) => { 14 + return <>{row.original.session_id}</>; 15 + }, 16 + }, 17 + { 18 + accessorKey: "cls", 19 + header: "CLS", 20 + cell: ({ row }) => { 21 + return ( 22 + <code>{row.original.cls ? row.original.cls.toFixed(2) : "-"}</code> 23 + ); 24 + }, 25 + }, 26 + { 27 + accessorKey: "fcp", 28 + header: "FCP", 29 + cell: ({ row }) => { 30 + return ( 31 + <code>{row.original.fcp ? row.original.fcp.toFixed(0) : "-"} </code> 32 + ); 33 + }, 34 + }, 35 + { 36 + accessorKey: "inp", 37 + header: "INP", 38 + cell: ({ row }) => { 39 + return ( 40 + <code>{row.original.inp ? row.original.inp.toFixed(0) : "-"}</code> 41 + ); 42 + }, 43 + }, 44 + { 45 + accessorKey: "lcp", 46 + header: "LCP", 47 + cell: ({ row }) => { 48 + return ( 49 + <code>{row.original.lcp ? row.original.lcp.toFixed(0) : "-"}</code> 50 + ); 51 + }, 52 + }, 53 + { 54 + accessorKey: "ttfb", 55 + header: "TTFB", 56 + cell: ({ row }) => { 57 + return ( 58 + <code>{row.original.ttfb ? row.original.ttfb.toFixed(0) : "-"}</code> 59 + ); 60 + }, 61 + }, 62 + // { 63 + // accessorKey: "updatedAt", 64 + // header: "Last Updated", 65 + // cell: ({ row }) => { 66 + // return <span>{formatDate(row.getValue("updatedAt"))}</span>; 67 + // }, 68 + // }, 69 + ];
+81
apps/web/src/components/data-table/session/data-table.tsx
··· 1 + "use client"; 2 + 3 + import type { ColumnDef } from "@tanstack/react-table"; 4 + import { 5 + flexRender, 6 + getCoreRowModel, 7 + useReactTable, 8 + } from "@tanstack/react-table"; 9 + import * as React from "react"; 10 + 11 + import { 12 + Table, 13 + TableBody, 14 + TableCell, 15 + TableHead, 16 + TableHeader, 17 + TableRow, 18 + } from "@openstatus/ui"; 19 + 20 + interface DataTableProps<TData, TValue> { 21 + columns: ColumnDef<TData, TValue>[]; 22 + data: TData[]; 23 + } 24 + 25 + export function DataTable<TData, TValue>({ 26 + columns, 27 + data, 28 + }: DataTableProps<TData, TValue>) { 29 + const table = useReactTable({ 30 + data, 31 + columns, 32 + getCoreRowModel: getCoreRowModel(), 33 + }); 34 + 35 + return ( 36 + <div className="rounded-md border"> 37 + <Table> 38 + <TableHeader className="bg-muted/50"> 39 + {table.getHeaderGroups().map((headerGroup) => ( 40 + <TableRow key={headerGroup.id} className="hover:bg-transparent"> 41 + {headerGroup.headers.map((header) => { 42 + return ( 43 + <TableHead key={header.id}> 44 + {header.isPlaceholder 45 + ? null 46 + : flexRender( 47 + header.column.columnDef.header, 48 + header.getContext() 49 + )} 50 + </TableHead> 51 + ); 52 + })} 53 + </TableRow> 54 + ))} 55 + </TableHeader> 56 + <TableBody> 57 + {table.getRowModel().rows?.length ? ( 58 + table.getRowModel().rows.map((row) => ( 59 + <TableRow 60 + key={row.id} 61 + data-state={row.getIsSelected() && "selected"} 62 + > 63 + {row.getVisibleCells().map((cell) => ( 64 + <TableCell key={cell.id}> 65 + {flexRender(cell.column.columnDef.cell, cell.getContext())} 66 + </TableCell> 67 + ))} 68 + </TableRow> 69 + )) 70 + ) : ( 71 + <TableRow> 72 + <TableCell colSpan={columns.length} className="h-24 text-center"> 73 + No results. 74 + </TableCell> 75 + </TableRow> 76 + )} 77 + </TableBody> 78 + </Table> 79 + </div> 80 + ); 81 + }
+1 -1
package.json
··· 19 19 "turbo": "1.13.3", 20 20 "typescript": "5.4.5" 21 21 }, 22 - "packageManager": "pnpm@9.1.3", 22 + "packageManager": "pnpm@9.1.4", 23 23 "name": "openstatus", 24 24 "workspaces": [ 25 25 "apps/*",
+25 -2
packages/api/src/router/tinybird/index.ts
··· 36 36 }), 37 37 38 38 totalRumMetricsForApplication: protectedProcedure 39 - .input(z.object({ dsn: z.string() })) 39 + .input(z.object({ dsn: z.string(), period: z.enum(["24h", "7d", "30d"]) })) 40 40 .query(async (opts) => { 41 41 return await tb.applicationRUMMetrics()(opts.input); 42 42 }), 43 43 rumMetricsForApplicationPerPage: protectedProcedure 44 - .input(z.object({ dsn: z.string() })) 44 + .input(z.object({ dsn: z.string(), period: z.enum(["24h", "7d", "30d"]) })) 45 45 .query(async (opts) => { 46 46 return await tb.applicationRUMMetricsPerPage()(opts.input); 47 + }), 48 + 49 + rumMetricsForPath: protectedProcedure 50 + .input( 51 + z.object({ 52 + dsn: z.string(), 53 + path: z.string(), 54 + period: z.enum(["24h", "7d", "30d"]), 55 + }) 56 + ) 57 + .query(async (opts) => { 58 + return await tb.applicationRUMMetricsForPath()(opts.input); 59 + }), 60 + sessionRumMetricsForPath: protectedProcedure 61 + .input( 62 + z.object({ 63 + dsn: z.string(), 64 + path: z.string(), 65 + period: z.enum(["24h", "7d", "30d"]), 66 + }) 67 + ) 68 + .query(async (opts) => { 69 + return await tb.applicationSessionMetricsPerPath()(opts.input); 47 70 }), 48 71 });
+71 -6
packages/tinybird/src/os-client.ts
··· 4 4 import { flyRegions } from "@openstatus/utils"; 5 5 6 6 import type { tbIngestWebVitalsArray } from "./validation"; 7 - import { responseRumPageQuery, tbIngestWebVitals } from "./validation"; 7 + import { 8 + responseRumPageQuery, 9 + sessionRumPageQuery, 10 + tbIngestWebVitals, 11 + } from "./validation"; 8 12 9 13 const isProd = process.env.NODE_ENV === "production"; 10 14 ··· 43 47 // FIXME: use Tinybird instead with super(args) maybe 44 48 // how about passing here the `opts: {revalidate}` to access it within the functions? 45 49 constructor(private args: { token: string; baseUrl?: string | undefined }) { 46 - if (process.env.NODE_ENV === "development") { 50 + if (process.env.NODE_ENV !== "development") { 47 51 this.tb = new NoopTinybird(); 48 52 } else { 49 53 this.tb = new Tinybird(args); ··· 338 342 } 339 343 340 344 applicationRUMMetrics() { 341 - const parameters = z.object({ dsn: z.string() }); 345 + const parameters = z.object({ 346 + dsn: z.string(), 347 + period: z.enum(["24h", "7d", "30d"]), 348 + }); 342 349 343 350 return async (props: z.infer<typeof parameters>) => { 344 351 try { ··· 348 355 data: z.object({ 349 356 cls: z.number(), 350 357 fcp: z.number(), 351 - fid: z.number(), 358 + // fid: z.number(), 352 359 lcp: z.number(), 360 + inp: z.number(), 353 361 ttfb: z.number(), 354 362 }), 355 363 opts: { ··· 365 373 }; 366 374 } 367 375 applicationRUMMetricsPerPage() { 368 - const parameters = z.object({ dsn: z.string() }); 369 - 376 + const parameters = z.object({ 377 + dsn: z.string(), 378 + period: z.enum(["24h", "7d", "30d"]), 379 + }); 370 380 return async (props: z.infer<typeof parameters>) => { 371 381 try { 372 382 const res = await this.tb.buildPipe({ ··· 380 390 }, 381 391 })(props); 382 392 return res.data; 393 + } catch (e) { 394 + console.error(e); 395 + } 396 + }; 397 + } 398 + applicationSessionMetricsPerPath() { 399 + const parameters = z.object({ 400 + dsn: z.string(), 401 + period: z.enum(["24h", "7d", "30d"]), 402 + path: z.string(), 403 + }); 404 + return async (props: z.infer<typeof parameters>) => { 405 + try { 406 + const res = await this.tb.buildPipe({ 407 + pipe: "rum_page_query_per_path", 408 + parameters, 409 + data: sessionRumPageQuery, 410 + opts: { 411 + next: { 412 + revalidate: MIN_CACHE, 413 + }, 414 + }, 415 + })(props); 416 + return res.data; 417 + } catch (e) { 418 + console.error(e); 419 + } 420 + }; 421 + } 422 + applicationRUMMetricsForPath() { 423 + const parameters = z.object({ 424 + dsn: z.string(), 425 + path: z.string(), 426 + period: z.enum(["24h", "7d", "30d"]), 427 + }); 428 + return async (props: z.infer<typeof parameters>) => { 429 + try { 430 + const res = await this.tb.buildPipe({ 431 + pipe: "rum_total_query_per_path", 432 + parameters, 433 + data: z.object({ 434 + cls: z.number(), 435 + fcp: z.number(), 436 + // fid: z.number(), 437 + lcp: z.number(), 438 + inp: z.number(), 439 + ttfb: z.number(), 440 + }), 441 + opts: { 442 + next: { 443 + revalidate: MIN_CACHE, 444 + }, 445 + }, 446 + })(props); 447 + return res.data[0]; 383 448 } catch (e) { 384 449 console.error(e); 385 450 }
+12 -2
packages/tinybird/src/validation.ts
··· 25 25 }); 26 26 27 27 export const responseRumPageQuery = z.object({ 28 - href: z.string().url(), 29 28 path: z.string(), 30 29 totalSession: z.number(), 31 30 cls: z.number(), 32 31 fcp: z.number(), 33 - fid: z.number(), 32 + // fid: z.number(), 33 + inp: z.number(), 34 + lcp: z.number(), 35 + ttfb: z.number(), 36 + }); 37 + 38 + export const sessionRumPageQuery = z.object({ 39 + session_id: z.string(), 40 + cls: z.number(), 41 + fcp: z.number(), 42 + // fid: z.number(), 43 + inp: z.number(), 34 44 lcp: z.number(), 35 45 ttfb: z.number(), 36 46 });