+ {eventConfig.label} ({event}) +
++ {event !== "CLS" ? value.toFixed(0) : value.toFixed(2) || 0} +
+- {eventConfig.label} ({event}) -
-- {data?.median.toFixed(2) || 0} -
-{row.original.cls ? row.original.cls.toFixed(2) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "fcp",
+ header: "FCP",
+ cell: ({ row }) => {
+ return (
+ {row.original.fcp ? row.original.fcp.toFixed(0) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "inp",
+ header: "INP",
+ cell: ({ row }) => {
+ return (
+ {row.original.inp ? row.original.inp.toFixed(0) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "lcp",
+ header: "LCP",
+ cell: ({ row }) => {
+ return (
+ {row.original.lcp ? row.original.lcp.toFixed(0) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "ttfb",
+ header: "TTFB",
+ cell: ({ row }) => {
+ return (
+ {row.original.ttfb ? row.original.ttfb.toFixed(0) : "-"}
+ );
+ },
+ },
+ // {
+ // accessorKey: "updatedAt",
+ // header: "Last Updated",
+ // cell: ({ row }) => {
+ // return {formatDate(row.getValue("updatedAt"))};
+ // },
+ // },
+];
diff --git a/apps/web/src/components/data-table/rum/data-table.tsx b/apps/web/src/components/data-table/rum/data-table.tsx
new file mode 100644
index 0000000000..c8cdc7e34f
--- /dev/null
+++ b/apps/web/src/components/data-table/rum/data-table.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import type { ColumnDef } from "@tanstack/react-table";
+import {
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import * as React from "react";
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@openstatus/ui";
+
+interface DataTableProps{row.original.cls ? row.original.cls.toFixed(2) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "fcp",
+ header: "FCP",
+ cell: ({ row }) => {
+ return (
+ {row.original.fcp ? row.original.fcp.toFixed(0) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "inp",
+ header: "INP",
+ cell: ({ row }) => {
+ return (
+ {row.original.inp ? row.original.inp.toFixed(0) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "lcp",
+ header: "LCP",
+ cell: ({ row }) => {
+ return (
+ {row.original.lcp ? row.original.lcp.toFixed(0) : "-"}
+ );
+ },
+ },
+ {
+ accessorKey: "ttfb",
+ header: "TTFB",
+ cell: ({ row }) => {
+ return (
+ {row.original.ttfb ? row.original.ttfb.toFixed(0) : "-"}
+ );
+ },
+ },
+ // {
+ // accessorKey: "updatedAt",
+ // header: "Last Updated",
+ // cell: ({ row }) => {
+ // return {formatDate(row.getValue("updatedAt"))};
+ // },
+ // },
+];
diff --git a/apps/web/src/components/data-table/session/data-table.tsx b/apps/web/src/components/data-table/session/data-table.tsx
new file mode 100644
index 0000000000..c8cdc7e34f
--- /dev/null
+++ b/apps/web/src/components/data-table/session/data-table.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import type { ColumnDef } from "@tanstack/react-table";
+import {
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import * as React from "react";
+
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@openstatus/ui";
+
+interface DataTablePropsYou have been invited by ${opts.ctx.user.email} ${!!opts.ctx.workspace.name ? `to join the workspace '${opts.ctx.workspace.name}'.` : "to join a workspace."}
+ subject: "You have been invited to join OpenStatus.dev", + html: `You have been invited by ${opts.ctx.user.email} ${ + opts.ctx.workspace.name + ? `to join the workspace '${opts.ctx.workspace.name}'.` + : "to join a workspace." + }
Click here to access the workspace: accept invitation.
+Click here to access the workspace: accept invitation.
If you don't have an account yet, it will require you to create one.
`, }), diff --git a/packages/api/src/router/monitor.ts b/packages/api/src/router/monitor.ts index 275e1e5522..13ec400ed7 100644 --- a/packages/api/src/router/monitor.ts +++ b/packages/api/src/router/monitor.ts @@ -41,7 +41,7 @@ export const monitorRouter = createTRPCRouter({ await opts.ctx.db.query.monitor.findMany({ where: and( eq(monitor.workspaceId, opts.ctx.workspace.id), - isNull(monitor.deletedAt), + isNull(monitor.deletedAt) ), }) ).length; @@ -104,7 +104,7 @@ export const monitorRouter = createTRPCRouter({ const allNotifications = await opts.ctx.db.query.notification.findMany({ where: and( eq(notification.workspaceId, opts.ctx.workspace.id), - inArray(notification.id, notifications), + inArray(notification.id, notifications) ), }); @@ -120,7 +120,7 @@ export const monitorRouter = createTRPCRouter({ const allTags = await opts.ctx.db.query.monitorTag.findMany({ where: and( eq(monitorTag.workspaceId, opts.ctx.workspace.id), - inArray(monitorTag.id, tags), + inArray(monitorTag.id, tags) ), }); @@ -136,7 +136,7 @@ export const monitorRouter = createTRPCRouter({ const allPages = await opts.ctx.db.query.page.findMany({ where: and( eq(page.workspaceId, opts.ctx.workspace.id), - inArray(page.id, pages), + inArray(page.id, pages) ), }); @@ -163,7 +163,7 @@ export const monitorRouter = createTRPCRouter({ where: and( eq(monitor.id, opts.input.id), eq(monitor.workspaceId, opts.ctx.workspace.id), - isNull(monitor.deletedAt), + isNull(monitor.deletedAt) ), with: { monitorTagsToMonitors: { with: { monitorTag: true } }, @@ -199,7 +199,7 @@ export const monitorRouter = createTRPCRouter({ where: and( eq(monitor.id, opts.input.id), isNull(monitor.deletedAt), - eq(monitor.public, true), + eq(monitor.public, true) ), }); if (!_monitor) return undefined; @@ -211,7 +211,7 @@ export const monitorRouter = createTRPCRouter({ }); const hasPageRelation = _page?.monitorsToPages.find( - ({ monitorId }) => _monitor.id === monitorId, + ({ monitorId }) => _monitor.id === monitorId ); if (!hasPageRelation) return undefined; @@ -271,8 +271,8 @@ export const monitorRouter = createTRPCRouter({ and( eq(monitor.id, opts.input.id), eq(monitor.workspaceId, opts.ctx.workspace.id), - isNull(monitor.deletedAt), - ), + isNull(monitor.deletedAt) + ) ) .returning() .get(); @@ -287,7 +287,7 @@ export const monitorRouter = createTRPCRouter({ (x) => !currentMonitorNotifications .map(({ notificationId }) => notificationId) - ?.includes(x), + ?.includes(x) ); if (addedNotifications.length > 0) { @@ -311,9 +311,9 @@ export const monitorRouter = createTRPCRouter({ eq(notificationsToMonitors.monitorId, currentMonitor.id), inArray( notificationsToMonitors.notificationId, - removedNotifications, - ), - ), + removedNotifications + ) + ) ) .run(); } @@ -328,7 +328,7 @@ export const monitorRouter = createTRPCRouter({ (x) => !currentMonitorTags .map(({ monitorTagId }) => monitorTagId) - ?.includes(x), + ?.includes(x) ); if (addedTags.length > 0) { @@ -350,8 +350,8 @@ export const monitorRouter = createTRPCRouter({ .where( and( eq(monitorTagsToMonitors.monitorId, currentMonitor.id), - inArray(monitorTagsToMonitors.monitorTagId, removedTags), - ), + inArray(monitorTagsToMonitors.monitorTagId, removedTags) + ) ) .run(); } @@ -363,7 +363,7 @@ export const monitorRouter = createTRPCRouter({ .all(); const addedPages = pages.filter( - (x) => !currentMonitorPages.map(({ pageId }) => pageId)?.includes(x), + (x) => !currentMonitorPages.map(({ pageId }) => pageId)?.includes(x) ); if (addedPages.length > 0) { @@ -385,8 +385,8 @@ export const monitorRouter = createTRPCRouter({ .where( and( eq(monitorsToPages.monitorId, currentMonitor.id), - inArray(monitorsToPages.pageId, removedPages), - ), + inArray(monitorsToPages.pageId, removedPages) + ) ) .run(); } @@ -401,8 +401,8 @@ export const monitorRouter = createTRPCRouter({ .where( and( eq(monitor.id, opts.input.id), - eq(monitor.workspaceId, opts.ctx.workspace.id), - ), + eq(monitor.workspaceId, opts.ctx.workspace.id) + ) ) .get(); if (!monitorToDelete) return; @@ -433,7 +433,7 @@ export const monitorRouter = createTRPCRouter({ const monitors = await opts.ctx.db.query.monitor.findMany({ where: and( eq(monitor.workspaceId, opts.ctx.workspace.id), - isNull(monitor.deletedAt), + isNull(monitor.deletedAt) ), with: { monitorTagsToMonitors: { with: { monitorTag: true } }, @@ -446,7 +446,7 @@ export const monitorRouter = createTRPCRouter({ monitorTagsToMonitors: z .array(z.object({ monitorTag: selectMonitorTagSchema })) .default([]), - }), + }) ) .parse(monitors); }), @@ -461,8 +461,8 @@ export const monitorRouter = createTRPCRouter({ and( eq(monitor.id, opts.input.id), eq(monitor.workspaceId, opts.ctx.workspace.id), - isNull(monitor.deletedAt), - ), + isNull(monitor.deletedAt) + ) ) .get(); @@ -481,8 +481,8 @@ export const monitorRouter = createTRPCRouter({ .where( and( eq(monitor.id, opts.input.id), - eq(monitor.workspaceId, opts.ctx.workspace.id), - ), + eq(monitor.workspaceId, opts.ctx.workspace.id) + ) ) .run(); }), @@ -510,8 +510,8 @@ export const monitorRouter = createTRPCRouter({ notification, and( eq(notificationsToMonitors.notificationId, notification.id), - eq(notification.workspaceId, opts.ctx.workspace.id), - ), + eq(notification.workspaceId, opts.ctx.workspace.id) + ) ) .where(eq(notificationsToMonitors.monitorId, opts.input.id)) .all(); @@ -524,7 +524,7 @@ export const monitorRouter = createTRPCRouter({ await opts.ctx.db.query.monitor.findMany({ where: and( eq(monitor.workspaceId, opts.ctx.workspace.id), - isNull(monitor.deletedAt), + isNull(monitor.deletedAt) ), }) ).length; diff --git a/packages/api/src/router/tinybird/index.ts b/packages/api/src/router/tinybird/index.ts index 4d5dcc0a9d..6f89afb8a7 100644 --- a/packages/api/src/router/tinybird/index.ts +++ b/packages/api/src/router/tinybird/index.ts @@ -29,9 +29,43 @@ export const tinybirdRouter = createTRPCRouter({ url: z.string().url().optional(), region: z.enum(flyRegions).optional(), cronTimestamp: z.number().int().optional(), - }), + }) ) .query(async (opts) => { return await tb.endpointResponseDetails("7d")(opts.input); }), + + totalRumMetricsForApplication: protectedProcedure + .input(z.object({ dsn: z.string(), period: z.enum(["24h", "7d", "30d"]) })) + .query(async (opts) => { + return await tb.applicationRUMMetrics()(opts.input); + }), + rumMetricsForApplicationPerPage: protectedProcedure + .input(z.object({ dsn: z.string(), period: z.enum(["24h", "7d", "30d"]) })) + .query(async (opts) => { + return await tb.applicationRUMMetricsPerPage()(opts.input); + }), + + rumMetricsForPath: protectedProcedure + .input( + z.object({ + dsn: z.string(), + path: z.string(), + period: z.enum(["24h", "7d", "30d"]), + }) + ) + .query(async (opts) => { + return await tb.applicationRUMMetricsForPath()(opts.input); + }), + sessionRumMetricsForPath: protectedProcedure + .input( + z.object({ + dsn: z.string(), + path: z.string(), + period: z.enum(["24h", "7d", "30d"]), + }) + ) + .query(async (opts) => { + return await tb.applicationSessionMetricsPerPath()(opts.input); + }), }); diff --git a/packages/api/src/router/workspace.ts b/packages/api/src/router/workspace.ts index 5e1afce599..2e0d43dda6 100644 --- a/packages/api/src/router/workspace.ts +++ b/packages/api/src/router/workspace.ts @@ -5,9 +5,11 @@ import { z } from "zod"; import { and, eq, sql } from "@openstatus/db"; import { + application, monitor, notification, page, + selectApplicationSchema, selectWorkspaceSchema, user, usersToWorkspaces, @@ -75,6 +77,14 @@ export const workspaceRouter = createTRPCRouter({ return selectWorkspaceSchema.parse(result); }), + getApplicationWorkspaces: protectedProcedure.query(async (opts) => { + const result = await opts.ctx.db.query.application.findMany({ + where: eq(application.workspaceId, opts.ctx.workspace.id), + }); + + return selectApplicationSchema.array().parse(result); + }), + getUserWorkspaces: protectedProcedure.query(async (opts) => { const result = await opts.ctx.db.query.usersToWorkspaces.findMany({ where: eq(usersToWorkspaces.userId, opts.ctx.user.id), @@ -114,7 +124,7 @@ export const workspaceRouter = createTRPCRouter({ await opts.ctx.db.query.usersToWorkspaces.findFirst({ where: and( eq(usersToWorkspaces.userId, opts.ctx.user.id), - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) ), }); @@ -131,8 +141,8 @@ export const workspaceRouter = createTRPCRouter({ .where( and( eq(usersToWorkspaces.userId, opts.input.id), - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), - ), + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) + ) ) .run(); }), @@ -144,7 +154,7 @@ export const workspaceRouter = createTRPCRouter({ await opts.ctx.db.query.usersToWorkspaces.findFirst({ where: and( eq(usersToWorkspaces.userId, opts.ctx.user.id), - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), + eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id) ), }); diff --git a/packages/db/.env.example b/packages/db/.env.example index dbbe1b6054..32cdbf2382 100644 --- a/packages/db/.env.example +++ b/packages/db/.env.example @@ -1,6 +1,2 @@ DATABASE_URL=file:./../../openstatus-dev.db DATABASE_AUTH_TOKEN=any-token - -CLICKHOUSE_URL=http://localhost:8123 -CLICKHOUSE_USERNAME=default -CLICKHOUSE_PASSWORD= diff --git a/packages/db/src/schema/applications/index.ts b/packages/db/src/schema/applications/index.ts index f4fe0545f5..e654ec5da9 100644 --- a/packages/db/src/schema/applications/index.ts +++ b/packages/db/src/schema/applications/index.ts @@ -1 +1,2 @@ export * from "./application"; +export * from "./validation"; diff --git a/packages/db/src/schema/applications/validation.ts b/packages/db/src/schema/applications/validation.ts new file mode 100644 index 0000000000..eefc9fc754 --- /dev/null +++ b/packages/db/src/schema/applications/validation.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { application } from "./application"; + +export const selectApplicationSchema = createSelectSchema(application); diff --git a/packages/db/src/seed.mts b/packages/db/src/seed.mts index f906028c29..5cb03940da 100644 --- a/packages/db/src/seed.mts +++ b/packages/db/src/seed.mts @@ -9,9 +9,11 @@ import { incidentTable, monitor, monitorsToPages, + monitorsToStatusReport, notification, notificationsToMonitors, page, + pagesToStatusReports, statusReport, statusReportUpdate, user, @@ -155,7 +157,7 @@ async function main() { id: 1, statusReportId: 1, status: "investigating", - message: "", + message: "Message", date: new Date(), }) .run(); @@ -177,11 +179,33 @@ async function main() { id: 2, statusReportId: 2, status: "investigating", - message: "", + message: "Message", date: new Date(), }) .run(); + await db.insert(monitorsToStatusReport).values([ + { + monitorId: 1, + statusReportId: 2, + }, + { + monitorId: 2, + statusReportId: 2, + }, + ]); + + await db.insert(pagesToStatusReports).values([ + { + pageId: 1, + statusReportId: 2, + }, + { + pageId: 1, + statusReportId: 1, + }, + ]); + await db .insert(incidentTable) .values({ diff --git a/packages/tinybird/src/os-client.ts b/packages/tinybird/src/os-client.ts index 82d4b9f007..bc15dc1f13 100644 --- a/packages/tinybird/src/os-client.ts +++ b/packages/tinybird/src/os-client.ts @@ -4,7 +4,11 @@ import { z } from "zod"; import { flyRegions } from "@openstatus/utils"; import type { tbIngestWebVitalsArray } from "./validation"; -import { tbIngestWebVitals } from "./validation"; +import { + responseRumPageQuery, + sessionRumPageQuery, + tbIngestWebVitals, +} from "./validation"; const isProd = process.env.NODE_ENV === "production"; @@ -43,7 +47,7 @@ export class OSTinybird { // FIXME: use Tinybird instead with super(args) maybe // how about passing here the `opts: {revalidate}` to access it within the functions? constructor(private args: { token: string; baseUrl?: string | undefined }) { - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV !== "development") { this.tb = new NoopTinybird(); } else { this.tb = new Tinybird(args); @@ -112,7 +116,7 @@ export class OSTinybird { opts?: { cache?: RequestCache | undefined; revalidate: number | undefined; - }, // RETHINK: not the best way to handle it + } // RETHINK: not the best way to handle it ) => { try { const res = await this.tb.buildPipe({ @@ -171,7 +175,7 @@ export class OSTinybird { endpointStatusPeriod( period: "7d" | "45d", - timezone: "UTC" = "UTC", // "EST" | "PST" | "CET" + timezone: "UTC" = "UTC" // "EST" | "PST" | "CET" ) { const parameters = z.object({ monitorId: z.string() }); @@ -180,7 +184,7 @@ export class OSTinybird { opts?: { cache?: RequestCache | undefined; revalidate: number | undefined; - }, // RETHINK: not the best way to handle it + } // RETHINK: not the best way to handle it ) => { try { const res = await this.tb.buildPipe({ @@ -336,6 +340,116 @@ export class OSTinybird { event: tbIngestWebVitals, })(data); } + + applicationRUMMetrics() { + const parameters = z.object({ + dsn: z.string(), + period: z.enum(["24h", "7d", "30d"]), + }); + + return async (props: z.infer