From 8efbe92990c0ca2dba663b52b2fe77d295e360bf Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Thu, 3 Oct 2024 19:48:32 +0200 Subject: [PATCH 1/7] wip: --- .../(dashboard)/checks/layout.tsx | 16 ++ .../(dashboard)/checks/loading.tsx | 5 + .../(dashboard)/checks/page.tsx | 15 ++ .../src/components/data-table/rum/columns.tsx | 83 ----------- .../components/data-table/session/columns.tsx | 69 --------- .../data-table/session/data-table.tsx | 81 ---------- .../data-table/single-check/columns.tsx | 134 +++++++++++++++++ .../{rum => single-check}/data-table.tsx | 17 ++- apps/web/src/components/icons.tsx | 2 + .../components/layout/header/app-header.tsx | 8 +- .../src/components/layout/header/app-tabs.tsx | 8 +- apps/web/src/config/pages.ts | 15 +- apps/web/src/lib/timing.ts | 34 +++++ packages/api/src/router/tinybird/index.ts | 36 +---- packages/tinybird/src/os-client.ts | 140 ++++-------------- packages/tinybird/src/validation.ts | 63 +++----- 16 files changed, 296 insertions(+), 430 deletions(-) create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/loading.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx delete mode 100644 apps/web/src/components/data-table/rum/columns.tsx delete mode 100644 apps/web/src/components/data-table/session/columns.tsx delete mode 100644 apps/web/src/components/data-table/session/data-table.tsx create mode 100644 apps/web/src/components/data-table/single-check/columns.tsx rename apps/web/src/components/data-table/{rum => single-check}/data-table.tsx (77%) create mode 100644 apps/web/src/lib/timing.ts diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx new file mode 100644 index 0000000000..89ed84905e --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from "react"; + +import { Header } from "@/components/dashboard/header"; +import AppPageLayout from "@/components/layout/app-page-layout"; + +export default async function Layout({ children }: { children: ReactNode }) { + return ( + +
+ {children} + + ); +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/loading.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/loading.tsx new file mode 100644 index 0000000000..ae51d6a5b8 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/loading.tsx @@ -0,0 +1,5 @@ +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx new file mode 100644 index 0000000000..794a22169f --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx @@ -0,0 +1,15 @@ +import { columns } from "@/components/data-table/single-check/columns"; +import { DataTable } from "@/components/data-table/single-check/data-table"; +import { env } from "@/env"; +import { api } from "@/trpc/server"; +import { OSTinybird } from "@openstatus/tinybird"; + +const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); + +export default async function Page() { + const workspace = await api.workspace.getWorkspace.query(); + const data = await tb.endpointSingleCheckList()({ + workspaceId: workspace.id, + }); + return
{data ? : null}
; +} diff --git a/apps/web/src/components/data-table/rum/columns.tsx b/apps/web/src/components/data-table/rum/columns.tsx deleted file mode 100644 index 4c85c55361..0000000000 --- a/apps/web/src/components/data-table/rum/columns.tsx +++ /dev/null @@ -1,83 +0,0 @@ -"use client"; - -import type { ColumnDef } from "@tanstack/react-table"; -import Link from "next/link"; - -import type { responseRumPageQuery } from "@openstatus/tinybird/src/validation"; -import type { z } from "zod"; - -export const columns: ColumnDef>[] = [ - { - accessorKey: "path", - header: "Page", - cell: ({ row }) => { - return ( - - {row.getValue("path")} - - ); - }, - }, - { - accessorKey: "totalSession", - header: "Total Session", - cell: ({ row }) => { - return <>{row.original.totalSession}; - }, - }, - { - accessorKey: "cls", - header: "CLS", - cell: ({ row }) => { - return ( - {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/columns.tsx b/apps/web/src/components/data-table/session/columns.tsx deleted file mode 100644 index 757310f394..0000000000 --- a/apps/web/src/components/data-table/session/columns.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import type { ColumnDef } from "@tanstack/react-table"; -import Link from "next/link"; - -import type { sessionRumPageQuery } from "@openstatus/tinybird/src/validation"; -import type { z } from "zod"; - -export const columns: ColumnDef>[] = [ - { - accessorKey: "session", - header: "Session", - cell: ({ row }) => { - return <>{row.original.session_id}; - }, - }, - { - accessorKey: "cls", - header: "CLS", - cell: ({ row }) => { - return ( - {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 deleted file mode 100644 index d7e57ef593..0000000000 --- a/apps/web/src/components/data-table/session/data-table.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"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 { - columns: ColumnDef[]; - data: TData[]; -} - -export function DataTable({ - columns, - data, -}: DataTableProps) { - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
- ); -} diff --git a/apps/web/src/components/data-table/single-check/columns.tsx b/apps/web/src/components/data-table/single-check/columns.tsx new file mode 100644 index 0000000000..56e5c7b154 --- /dev/null +++ b/apps/web/src/components/data-table/single-check/columns.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { formatNumber } from "@/components/monitor-dashboard/metrics-card"; +import { StatusCodeBadge } from "@/components/monitor/status-code-badge"; +import { getTimingPhases } from "@/components/ping-response-analysis/utils"; +import { getTimingColor, getTimingPercentage } from "@/lib/timing"; +import { cn, formatDate } from "@/lib/utils"; +import type { SingleChecker } from "@openstatus/tinybird"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@openstatus/ui"; +import { flyRegionsDict } from "@openstatus/utils"; +import type { ColumnDef } from "@tanstack/react-table"; +import { format } from "date-fns"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "timestamp", + header: "Timestamp", + cell: ({ row }) => { + const timestamp = row.original.timestamp; + if (!timestamp) return null; + return ( +
+ {format(new Date(timestamp), "LLL dd, y HH:mm:ss")} +
+ ); + }, + }, + { + accessorKey: "region", + header: "Region", + cell: ({ row }) => { + const region = row.original.region; + const { code, flag, location } = flyRegionsDict[region]; + return ( +
+

{location}

+

+ {flag} {code} +

+
+ ); + }, + }, + { + accessorKey: "status", + header: "Status Code", // add whitespace-nowrap + cell: ({ row }) => { + const status = row.original.status; + if (!status) return null; + return ; + }, + }, + { + accessorKey: "latency", + header: "Latency", + cell: ({ row }) => { + const latency = row.original.latency; + return
{formatNumber(latency)}ms
; + }, + }, + { + accessorKey: "requestId", + header: "Request Id", // add whitespace-nowrap + cell: ({ row }) => { + return
{row.getValue("requestId")}
; + }, + }, + { + accessorKey: "headers", + header: "Headers", + cell: ({ row }) => { + return ( +
+ {JSON.stringify(row.getValue("headers"))} +
+ ); + }, + }, + { + accessorKey: "timing", + header: () =>
Timing Phases
, + cell: ({ row }) => { + const { timing, latency } = row.original; + const timingPhases = getTimingPhases(timing); + const percentage = getTimingPercentage(timingPhases, latency); + return ( + + +
+ {Object.entries(timingPhases).map(([key, value]) => ( +
+ ))} +
+ + +
+ {Object.entries(timingPhases).map(([key, value]) => { + const color = getTimingColor(key); + return ( +
+
+
+
+ {key} +
+
+
+
+ {percentage[key]} +
+
+ {new Intl.NumberFormat("en-US", { + maximumFractionDigits: 3, + }).format(value)} + ms +
+
+
+ ); + })} +
+ + + ); + }, + }, +]; diff --git a/apps/web/src/components/data-table/rum/data-table.tsx b/apps/web/src/components/data-table/single-check/data-table.tsx similarity index 77% rename from apps/web/src/components/data-table/rum/data-table.tsx rename to apps/web/src/components/data-table/single-check/data-table.tsx index d7e57ef593..bc5420136d 100644 --- a/apps/web/src/components/data-table/rum/data-table.tsx +++ b/apps/web/src/components/data-table/single-check/data-table.tsx @@ -1,9 +1,10 @@ "use client"; -import type { ColumnDef } from "@tanstack/react-table"; +import type { ColumnDef, SortingState } from "@tanstack/react-table"; import { flexRender, getCoreRowModel, + getSortedRowModel, useReactTable, } from "@tanstack/react-table"; import * as React from "react"; @@ -26,10 +27,14 @@ export function DataTable({ columns, data, }: DataTableProps) { + const [sorting, setSorting] = React.useState([]); const table = useReactTable({ data, columns, + state: { sorting }, getCoreRowModel: getCoreRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), }); return ( @@ -40,7 +45,10 @@ export function DataTable({ {headerGroup.headers.map((header) => { return ( - + {header.isPlaceholder ? null : flexRender( @@ -61,7 +69,10 @@ export function DataTable({ data-state={row.getIsSelected() && "selected"} > {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx index bae04adac0..17a0efdb08 100644 --- a/apps/web/src/components/icons.tsx +++ b/apps/web/src/components/icons.tsx @@ -42,6 +42,7 @@ import { Plug, Puzzle, Ratio, + RefreshCcw, Search, SearchCheck, Server, @@ -127,6 +128,7 @@ export const Icons = { "book-open-check": BookOpenCheck, info: Info, server: Server, + "refresh-ccw": RefreshCcw, discord: ({ ...props }: LucideProps) => ( - new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime(), + new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime() ) .pop(); export function AppHeader() { const [lastViewed, setLastViewed] = useCookieState( "last-viewed-changelog", - new Date(0).toISOString(), + new Date(0).toISOString() ); const show = @@ -56,9 +56,9 @@ export function AppHeader() {
  • diff --git a/apps/web/src/components/layout/header/app-tabs.tsx b/apps/web/src/components/layout/header/app-tabs.tsx index 03c0170ad0..ed54e233e6 100644 --- a/apps/web/src/components/layout/header/app-tabs.tsx +++ b/apps/web/src/components/layout/header/app-tabs.tsx @@ -7,6 +7,7 @@ import { StatusDot } from "@/components/monitor/status-dot"; import { pagesConfig } from "@/config/pages"; import { api } from "@/trpc/client"; import { useEffect, useState } from "react"; +import { Badge } from "@openstatus/ui"; export function AppTabs() { const params = useParams(); @@ -17,7 +18,7 @@ export function AppTabs() { return (
    - {pagesConfig.map(({ title, segment, href }) => { + {pagesConfig.map(({ title, segment, href, badge }) => { const active = segment === selectedSegment; return ( {title} + {badge ? ( + + {badge} + + ) : null} {/* {segment === "incidents" ? : null} */} ); diff --git a/apps/web/src/config/pages.ts b/apps/web/src/config/pages.ts index 9e79ac2267..5cbd7a6009 100644 --- a/apps/web/src/config/pages.ts +++ b/apps/web/src/config/pages.ts @@ -1,4 +1,5 @@ import type { ValidIcon } from "@/components/icons"; +import { BadgeProps } from "@openstatus/ui"; export type Page = { title: string; @@ -9,6 +10,8 @@ export type Page = { disabled?: boolean; segment: string; children?: Page[]; + /** add a "beta" or "new" badge to the page */ + badge?: string; }; export const settingsPagesConfig: Page[] = [ @@ -166,6 +169,14 @@ export const pagesConfig = [ segment: "monitors", children: monitorPagesConfig, }, + { + title: "Single Checks", + description: "Where you can seel all your single checks", + href: "/checks", + icon: "refresh-ccw", + segment: "checks", + badge: "beta", + }, { title: "Incidents", description: "All your incidents.", @@ -198,7 +209,7 @@ export const pagesConfig = [ segment: "settings", children: settingsPagesConfig, }, -] as const satisfies readonly Page[]; +] satisfies Page[]; type MarketingPageType = Page; @@ -289,7 +300,7 @@ export const marketingPagesConfig = [ export function getPageBySegment( segment: string | string[], - currentPage: readonly Page[] = pagesConfig, + currentPage: readonly Page[] = pagesConfig ): Page | undefined { if (typeof segment === "string") { const page = currentPage.find((page) => page.segment === segment); diff --git a/apps/web/src/lib/timing.ts b/apps/web/src/lib/timing.ts new file mode 100644 index 0000000000..fcd753f0b1 --- /dev/null +++ b/apps/web/src/lib/timing.ts @@ -0,0 +1,34 @@ +export function getTimingColor(timing: string) { + switch (timing) { + case "dns": + return "bg-emerald-500"; + case "connection": + return "bg-cyan-500"; + case "tls": + return "bg-blue-500"; + case "ttfb": + return "bg-violet-500"; + case "transfer": + return "bg-purple-500"; + default: + return "bg-gray-500"; + } +} + +export function getTimingPercentage( + timing: Record, + latency?: number +): Record { + const total = + latency || Object.values(timing).reduce((acc, curr) => acc + curr, 0); + const percentage: Record = { ...timing }; + Object.entries(timing).forEach(([key, value]) => { + const pValue = Math.round((value / total) * 1000) / 1000; + percentage[key as keyof typeof timing] = /^0\.00[0-9]+/.test( + pValue.toString() + ) + ? "<1%" + : `${(pValue * 100).toFixed(1)}%`; + }); + return percentage; +} diff --git a/packages/api/src/router/tinybird/index.ts b/packages/api/src/router/tinybird/index.ts index 59a8a7c0b6..eb56be530d 100644 --- a/packages/api/src/router/tinybird/index.ts +++ b/packages/api/src/router/tinybird/index.ts @@ -29,43 +29,9 @@ 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/tinybird/src/os-client.ts b/packages/tinybird/src/os-client.ts index 0b9315bf5a..7c649cf597 100644 --- a/packages/tinybird/src/os-client.ts +++ b/packages/tinybird/src/os-client.ts @@ -3,20 +3,13 @@ import { z } from "zod"; import { flyRegions } from "../../db/src/schema/constants"; -import type { tbIngestWebVitalsArray } from "./validation"; -import { - responseRumPageQuery, - sessionRumPageQuery, - tbIngestWebVitals, -} from "./validation"; - const isProd = process.env.NODE_ENV === "production"; const DEV_CACHE = 3_600; // 1h const MIN_CACHE = isProd ? 60 : DEV_CACHE; // 60s const DEFAULT_CACHE = isProd ? 120 : DEV_CACHE; // 2min -const _MAX_CACHE = 86_400; // 1d +const MAX_CACHE = 86_400; // 1d const VERSION = "v1"; @@ -118,7 +111,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({ @@ -178,7 +171,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() }); @@ -187,7 +180,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({ @@ -339,83 +332,45 @@ export class OSTinybird { } }; } - ingestWebVitals(data: z.infer) { - return this.tb.buildIngestEndpoint({ - datasource: "web_vitals__v0", - event: tbIngestWebVitals, - })(data); - } - applicationRUMMetrics() { + endpointSingleCheckList() { const parameters = z.object({ - dsn: z.string(), - period: z.enum(["24h", "7d", "30d"]), + workspaceId: z.number(), }); return async (props: z.infer) => { try { const res = await this.tb.buildPipe({ - pipe: "rum_total_query", + pipe: `single_checks_get__${VERSION}`, parameters, data: z.object({ - cls: z.number(), - fcp: z.number(), - // fid: z.number(), - lcp: z.number(), - inp: z.number(), - ttfb: z.number(), + requestId: z.number(), + headers: z + .string() + .nullable() + .transform((val) => { + if (!val) return null; + const value = z + .record(z.string(), z.string()) + .safeParse(JSON.parse(val)); + if (value.success) return value.data; + return null; + }), + body: z.string().nullable().optional(), + workspaceId: z.number(), + latency: z.number().int(), + // REMINDER: we should call it statusCode to stay consistent + status: z.number().int().nullable().default(null), + // url: z.string().url(), + timestamp: z.number().int().optional(), + region: z.enum(flyRegions), + timing: z + .string() + .transform((val) => JSON.parse(val)) + .pipe(timingSchema), }), opts: { - next: { - revalidate: MIN_CACHE, - }, - }, - })(props); - return res.data[0]; - } catch (e) { - console.error(e); - } - }; - } - applicationRUMMetricsPerPage() { - const parameters = z.object({ - dsn: z.string(), - period: z.enum(["24h", "7d", "30d"]), - }); - return async (props: z.infer) => { - try { - const res = await this.tb.buildPipe({ - pipe: "rum_page_query", - parameters, - data: responseRumPageQuery, - opts: { - next: { - revalidate: MIN_CACHE, - }, - }, - })(props); - return res.data; - } catch (e) { - console.error(e); - } - }; - } - applicationSessionMetricsPerPath() { - const parameters = z.object({ - dsn: z.string(), - period: z.enum(["24h", "7d", "30d"]), - path: z.string(), - }); - return async (props: z.infer) => { - try { - const res = await this.tb.buildPipe({ - pipe: "rum_page_query_per_path", - parameters, - data: sessionRumPageQuery, - opts: { - next: { - revalidate: MIN_CACHE, - }, + cache: "no-store", }, })(props); return res.data; @@ -424,37 +379,6 @@ export class OSTinybird { } }; } - applicationRUMMetricsForPath() { - const parameters = z.object({ - dsn: z.string(), - path: z.string(), - period: z.enum(["24h", "7d", "30d"]), - }); - return async (props: z.infer) => { - try { - const res = await this.tb.buildPipe({ - pipe: "rum_total_query_per_path", - parameters, - data: z.object({ - cls: z.number(), - fcp: z.number(), - // fid: z.number(), - lcp: z.number(), - inp: z.number(), - ttfb: z.number(), - }), - opts: { - next: { - revalidate: MIN_CACHE, - }, - }, - })(props); - return res.data[0]; - } catch (e) { - console.error(e); - } - }; - } } /** diff --git a/packages/tinybird/src/validation.ts b/packages/tinybird/src/validation.ts index 482a308a0c..d1cd0552c0 100644 --- a/packages/tinybird/src/validation.ts +++ b/packages/tinybird/src/validation.ts @@ -1,51 +1,8 @@ import * as z from "zod"; import { monitorFlyRegionSchema } from "../../db/src/schema/constants"; import type { flyRegions } from "../../db/src/schema/constants"; +import { timingSchema } from "./os-client"; -export const tbIngestWebVitals = z.object({ - dsn: z.string(), - href: z.string(), - speed: z.string(), - path: z.string(), - screen: z.string(), - name: z.string(), - rating: z.string().optional(), - value: z.number(), - id: z.string(), - session_id: z.string(), - browser: z.string().default(""), - city: z.string().default(""), - country: z.string().default(""), - continent: z.string().default(""), - device: z.string().default(""), - region_code: z.string().default(""), - timezone: z.string().default(""), - os: z.string(), - timestamp: z.number().int(), -}); - -export const responseRumPageQuery = z.object({ - path: z.string(), - totalSession: z.number(), - cls: z.number(), - fcp: z.number(), - // fid: z.number(), - inp: z.number(), - lcp: z.number(), - ttfb: z.number(), -}); - -export const sessionRumPageQuery = z.object({ - session_id: z.string(), - cls: z.number(), - fcp: z.number(), - // fid: z.number(), - inp: z.number(), - lcp: z.number(), - ttfb: z.number(), -}); - -export const tbIngestWebVitalsArray = z.array(tbIngestWebVitals); /** * Values for the datasource ping_response */ @@ -110,6 +67,23 @@ export const tbParameterResponseList = z.object({ cronTimestamp: z.number().int().optional(), }); +/** + * Values from the pipe single_checker + */ +export const tbBuildSingleChecker = z.object({ + requestId: z.number(), + headers: z.record(z.string(), z.string()).nullable().optional(), + body: z.string().nullable().optional(), + workspaceId: z.number(), + latency: z.number().int(), + // REMINDER: we should call it statusCode + status: z.number().int().nullable().default(null), + // url: z.string().url(), + timestamp: z.number().int().optional(), + timing: timingSchema, + region: monitorFlyRegionSchema, +}); + /** * Params for pipe response_details */ @@ -283,6 +257,7 @@ export const tbBuildResponseTimeMetricsByRegion = z }) .merge(latencyMetrics); +export type SingleChecker = z.infer; export type Ping = z.infer; export type Region = (typeof flyRegions)[number]; // TODO: rename type AvailabeRegion export type Monitor = z.infer; From 41af9772112b67224e207009f273bcc7ccae2688 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 4 Oct 2024 09:20:54 +0200 Subject: [PATCH 2/7] chore: add action button for api docs --- .../(dashboard)/checks/layout.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx index 89ed84905e..97c19f0bc8 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx @@ -2,13 +2,27 @@ import type { ReactNode } from "react"; import { Header } from "@/components/dashboard/header"; import AppPageLayout from "@/components/layout/app-page-layout"; +import { Button } from "@openstatus/ui"; +import { ArrowUpRight } from "lucide-react"; export default async function Layout({ children }: { children: ReactNode }) { return (
    + + API Docs + + + } /> {children} From 5f479ddb88aa7f37bda41a70ea3e6e56e2d119b7 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 4 Oct 2024 10:00:49 +0200 Subject: [PATCH 3/7] chore: search params --- .../(dashboard)/checks/client.tsx | 34 +++++++++++++++++++ .../(dashboard)/checks/page.tsx | 22 ++++++++++-- .../(dashboard)/checks/search-params.tsx | 7 ++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/client.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/search-params.tsx diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/client.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/client.tsx new file mode 100644 index 0000000000..0858e4093c --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/client.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useQueryStates } from "nuqs"; +import { useEffect } from "react"; +import { searchParamsParsers } from "./search-params"; + +export function Client({ totalRows }: { totalRows: number }) { + const [search, setSearch] = useQueryStates(searchParamsParsers); + + useEffect(() => { + if (typeof window === "undefined") return; + + function onScroll() { + // TODO: add a threshold for the "Load More" button + const onPageBottom = + window.innerHeight + Math.round(window.scrollY) >= + document.body.offsetHeight; + if (onPageBottom && search.pageSize * search.page <= totalRows) { + setSearch( + { + // page: search.page + 1 + pageSize: search.pageSize + 10, + }, + { shallow: false } + ); + } + } + + window.addEventListener("scroll", onScroll); + return () => window.removeEventListener("scroll", onScroll); + }, [search]); + + return null; +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx index 794a22169f..bcec7ba29e 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx @@ -3,13 +3,31 @@ import { DataTable } from "@/components/data-table/single-check/data-table"; import { env } from "@/env"; import { api } from "@/trpc/server"; import { OSTinybird } from "@openstatus/tinybird"; +import { searchParamsCache } from "./search-params"; +import { Client } from "./client"; const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); -export default async function Page() { +type Props = { + params: { domain: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export default async function Page({ searchParams }: Props) { + const { page, pageSize } = searchParamsCache.parse(searchParams); const workspace = await api.workspace.getWorkspace.query(); const data = await tb.endpointSingleCheckList()({ workspaceId: workspace.id, + page, + pageSize, }); - return
    {data ? : null}
    ; + + console.log({ data: data?.length, page, pageSize }); + + return ( +
    + {data ? : null} + +
    + ); } diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/search-params.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/search-params.tsx new file mode 100644 index 0000000000..cd19dd214e --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/search-params.tsx @@ -0,0 +1,7 @@ +import { createSearchParamsCache, parseAsInteger } from "nuqs/server"; + +export const searchParamsParsers = { + pageSize: parseAsInteger.withDefault(10), + page: parseAsInteger.withDefault(0), +}; +export const searchParamsCache = createSearchParamsCache(searchParamsParsers); From 361e23eb88e27fccaa81565b69b010ac276b1b94 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 4 Oct 2024 10:28:09 +0200 Subject: [PATCH 4/7] wip: --- .../src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx | 3 --- packages/tinybird/src/os-client.ts | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx index bcec7ba29e..40cee28852 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx @@ -9,7 +9,6 @@ import { Client } from "./client"; const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); type Props = { - params: { domain: string }; searchParams: { [key: string]: string | string[] | undefined }; }; @@ -22,8 +21,6 @@ export default async function Page({ searchParams }: Props) { pageSize, }); - console.log({ data: data?.length, page, pageSize }); - return (
    {data ? : null} diff --git a/packages/tinybird/src/os-client.ts b/packages/tinybird/src/os-client.ts index 7c649cf597..976c04fd53 100644 --- a/packages/tinybird/src/os-client.ts +++ b/packages/tinybird/src/os-client.ts @@ -336,6 +336,8 @@ export class OSTinybird { endpointSingleCheckList() { const parameters = z.object({ workspaceId: z.number(), + pageSize: z.number().optional(), + page: z.number().optional(), }); return async (props: z.infer) => { From fbb96ac0e9d2c4b797b62561a7d0172e89db5262 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Sat, 5 Oct 2024 16:04:04 +0200 Subject: [PATCH 5/7] wip: --- .../checks/{ => (overview)}/layout.tsx | 0 .../checks/{ => (overview)}/loading.tsx | 0 .../(dashboard)/checks/(overview)/page.tsx | 19 ++++ .../checks/{ => (overview)}/search-params.tsx | 0 .../(dashboard)/checks/{ => [id]}/client.tsx | 0 .../(dashboard)/checks/[id]/layout.tsx | 32 +++++++ .../(dashboard)/checks/[id]/loading.tsx | 5 + .../(dashboard)/checks/{ => [id]}/page.tsx | 4 +- .../checks/[id]/request-details-dialog.tsx | 74 ++++++++++++++ .../(dashboard)/checks/[id]/search-params.tsx | 7 ++ .../components/data-table/check/columns.tsx | 56 +++++++++++ .../data-table/check/data-table.tsx | 96 +++++++++++++++++++ packages/api/src/edge.ts | 2 + packages/api/src/router/check.ts | 33 +++++++ packages/db/src/schema/check/check.ts | 4 +- packages/db/src/schema/check/constants.ts | 9 -- packages/db/src/schema/check/index.ts | 2 +- packages/db/src/schema/check/validation.ts | 11 +++ packages/db/src/schema/monitors/validation.ts | 4 +- packages/tinybird/src/os-client.ts | 1 + 20 files changed, 344 insertions(+), 15 deletions(-) rename apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/{ => (overview)}/layout.tsx (100%) rename apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/{ => (overview)}/loading.tsx (100%) create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/page.tsx rename apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/{ => (overview)}/search-params.tsx (100%) rename apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/{ => [id]}/client.tsx (100%) create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/loading.tsx rename apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/{ => [id]}/page.tsx (86%) create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx create mode 100644 apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/search-params.tsx create mode 100644 apps/web/src/components/data-table/check/columns.tsx create mode 100644 apps/web/src/components/data-table/check/data-table.tsx create mode 100644 packages/api/src/router/check.ts delete mode 100644 packages/db/src/schema/check/constants.ts create mode 100644 packages/db/src/schema/check/validation.ts diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/layout.tsx similarity index 100% rename from apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/layout.tsx rename to apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/layout.tsx diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/loading.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/loading.tsx similarity index 100% rename from apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/loading.tsx rename to apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/loading.tsx diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/page.tsx new file mode 100644 index 0000000000..1dd155d92f --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/page.tsx @@ -0,0 +1,19 @@ +import { columns } from "@/components/data-table/check/columns"; +import { DataTable } from "@/components/data-table/check/data-table"; +import { env } from "@/env"; +import { api } from "@/trpc/server"; +import { OSTinybird } from "@openstatus/tinybird"; +import { searchParamsCache } from "./search-params"; + +const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); + +type Props = { + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export default async function Page({ searchParams }: Props) { + const { page, pageSize } = searchParamsCache.parse(searchParams); + const data = await api.check.getChecksByWorkspace.query(); + + return
    {data ? : null}
    ; +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/search-params.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/search-params.tsx similarity index 100% rename from apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/search-params.tsx rename to apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/(overview)/search-params.tsx diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/client.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/client.tsx similarity index 100% rename from apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/client.tsx rename to apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/client.tsx diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx new file mode 100644 index 0000000000..66c6419b0d --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx @@ -0,0 +1,32 @@ +import type { ReactNode } from "react"; + +import { Header } from "@/components/dashboard/header"; +import AppPageLayout from "@/components/layout/app-page-layout"; +import { api } from "@/trpc/server"; +import { notFound } from "next/navigation"; +import { RequestDetailsDialog } from "./request-details-dialog"; + +export default async function Layout({ + params, + children, +}: { + params: { id: string }; + children: ReactNode; +}) { + const check = await api.check.getCheckById.query({ + id: Number.parseInt(params.id), + }); + + if (!check) return notFound(); + + return ( + +
    {check.url}
    } + actions={} + /> + {children} + + ); +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/loading.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/loading.tsx new file mode 100644 index 0000000000..ae51d6a5b8 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx similarity index 86% rename from apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx rename to apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx index 40cee28852..90e3258a35 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/page.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx @@ -9,14 +9,16 @@ import { Client } from "./client"; const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); type Props = { + params: { id: string }; searchParams: { [key: string]: string | string[] | undefined }; }; -export default async function Page({ searchParams }: Props) { +export default async function Page({ params, searchParams }: Props) { const { page, pageSize } = searchParamsCache.parse(searchParams); const workspace = await api.workspace.getWorkspace.query(); const data = await tb.endpointSingleCheckList()({ workspaceId: workspace.id, + requestId: Number.parseInt(params.id), page, pageSize, }); diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx new file mode 100644 index 0000000000..7e43fedfb0 --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx @@ -0,0 +1,74 @@ +import { Check } from "@openstatus/db/src/schema"; +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@openstatus/ui"; +import { format } from "date-fns"; +import { Minus } from "lucide-react"; + +export function RequestDetailsDialog({ check }: { check: Check }) { + return ( + + + + + + + Request Details + Details of #{check.id} + +
    +
    +
    URL
    +
    {check.url}
    +
    +
    +
    Method
    +
    {check.method}
    +
    +
    +
    Created At
    +
    + {format(new Date(check.createdAt!), "LLL dd, y HH:mm:ss")} +
    +
    +
    +
    Regions
    +
    + {check.regions.length ? ( + check.regions.join(", ") + ) : ( + + )} +
    +
    +
    +
    Headers
    +
    + {!check.headers ? ( + + ) : ( +
    {JSON.stringify(check.headers, null, 2)}
    + )} +
    +
    +
    +
    Body
    +
    + {!check.headers ? ( + + ) : ( +
    {JSON.stringify(check.headers, null, 2)}
    + )} +
    +
    +
    +
    +
    + ); +} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/search-params.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/search-params.tsx new file mode 100644 index 0000000000..cd19dd214e --- /dev/null +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/search-params.tsx @@ -0,0 +1,7 @@ +import { createSearchParamsCache, parseAsInteger } from "nuqs/server"; + +export const searchParamsParsers = { + pageSize: parseAsInteger.withDefault(10), + page: parseAsInteger.withDefault(0), +}; +export const searchParamsCache = createSearchParamsCache(searchParamsParsers); diff --git a/apps/web/src/components/data-table/check/columns.tsx b/apps/web/src/components/data-table/check/columns.tsx new file mode 100644 index 0000000000..b98aa4ef66 --- /dev/null +++ b/apps/web/src/components/data-table/check/columns.tsx @@ -0,0 +1,56 @@ +"use client"; + +import type { ColumnDef } from "@tanstack/react-table"; +import { format } from "date-fns"; +import { Check } from "@openstatus/db/src/schema"; +import { DataTableBadges } from "../data-table-badges"; +import { Minus } from "lucide-react"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ row }) => { + const createdAt = row.original.createdAt; + if (!createdAt) return null; + return ( +
    + {format(new Date(createdAt), "LLL dd, y HH:mm:ss")} +
    + ); + }, + meta: { + headerClassName: "whitespace-nowrap", + }, + }, + { + accessorKey: "url", + header: "URL", + cell: ({ row }) => { + return
    {row.getValue("url")}
    ; + }, + }, + { + accessorKey: "regions", + header: "Regions", + cell: ({ row }) => { + const regions = row.original.regions; + if (!regions.length) + return ; + return ; + }, + }, + { + accessorKey: "countRequests", + header: "Request Count", + cell: ({ row }) => { + return
    #{row.getValue("countRequests") || 0}
    ; + }, + meta: { + headerClassName: "whitespace-nowrap", + }, + }, + { + accessorKey: "id", + }, +]; diff --git a/apps/web/src/components/data-table/check/data-table.tsx b/apps/web/src/components/data-table/check/data-table.tsx new file mode 100644 index 0000000000..898bd09ce8 --- /dev/null +++ b/apps/web/src/components/data-table/check/data-table.tsx @@ -0,0 +1,96 @@ +"use client"; + +import type { ColumnDef, VisibilityState } from "@tanstack/react-table"; +import { + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import * as React from "react"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@openstatus/ui"; +import { useRouter } from "next/navigation"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [columnVisibility, setColumnVisibility] = + React.useState({ id: false }); + const router = useRouter(); + const table = useReactTable({ + data, + columns, + state: { columnVisibility }, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + return ( +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + router.push(`./checks/${row.getValue("id")}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
    +
    + ); +} diff --git a/packages/api/src/edge.ts b/packages/api/src/edge.ts index 03487f909a..3f3e26e63d 100644 --- a/packages/api/src/edge.ts +++ b/packages/api/src/edge.ts @@ -1,3 +1,4 @@ +import { checkRouter } from "./router/check"; import { domainRouter } from "./router/domain"; import { incidentRouter } from "./router/incident"; import { integrationRouter } from "./router/integration"; @@ -30,4 +31,5 @@ export const edgeRouter = createTRPCRouter({ tinybird: tinybirdRouter, monitorTag: monitorTagRouter, maintenance: maintenanceRouter, + check: checkRouter, }); diff --git a/packages/api/src/router/check.ts b/packages/api/src/router/check.ts new file mode 100644 index 0000000000..3c738ce4f1 --- /dev/null +++ b/packages/api/src/router/check.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +import { and, desc, eq } from "@openstatus/db"; +import { check, selectCheckSchema } from "@openstatus/db/src/schema"; + +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const checkRouter = createTRPCRouter({ + getChecksByWorkspace: protectedProcedure.query(async (opts) => { + const checks = await opts.ctx.db + .select() + .from(check) + .where(eq(check.workspaceId, opts.ctx.workspace.id)) + .orderBy(desc(check.createdAt)) + .all(); + return selectCheckSchema.array().parse(checks); + }), + getCheckById: protectedProcedure + .input(z.object({ id: z.number() })) + .query(async (opts) => { + const _check = await opts.ctx.db + .select() + .from(check) + .where( + and( + eq(check.id, opts.input.id), + eq(check.workspaceId, opts.ctx.workspace.id) + ) + ) + .get(); + return selectCheckSchema.parse(_check); + }), +}); diff --git a/packages/db/src/schema/check/check.ts b/packages/db/src/schema/check/check.ts index 713418386c..46a06e23c7 100644 --- a/packages/db/src/schema/check/check.ts +++ b/packages/db/src/schema/check/check.ts @@ -5,17 +5,17 @@ import { workspace } from "../workspaces"; export const check = sqliteTable("check", { id: integer("id").primaryKey({ autoIncrement: true }), - regions: text("regions").default("").notNull(), url: text("url", { length: 4096 }).notNull(), headers: text("headers").default(""), body: text("body").default(""), method: text("method", { enum: monitorMethods }).default("GET"), + regions: text("regions").default("").notNull(), countRequests: integer("count_requests").default(1), workspaceId: integer("workspace_id").references(() => workspace.id), createdAt: integer("created_at", { mode: "timestamp" }).default( - sql`(strftime('%s', 'now'))`, + sql`(strftime('%s', 'now'))` ), }); diff --git a/packages/db/src/schema/check/constants.ts b/packages/db/src/schema/check/constants.ts deleted file mode 100644 index 59d5ca2a60..0000000000 --- a/packages/db/src/schema/check/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const flyCheckerRegions = [ - // TODO: Add more regions - "ams", - "iad", - "hkg", - "jnb", - "syd", - "gru", -] as const; diff --git a/packages/db/src/schema/check/index.ts b/packages/db/src/schema/check/index.ts index d03eac78cf..8204bf0309 100644 --- a/packages/db/src/schema/check/index.ts +++ b/packages/db/src/schema/check/index.ts @@ -1,2 +1,2 @@ export * from "./check"; -export * from "./constants"; +export * from "./validation"; diff --git a/packages/db/src/schema/check/validation.ts b/packages/db/src/schema/check/validation.ts new file mode 100644 index 0000000000..0b4a9a4b69 --- /dev/null +++ b/packages/db/src/schema/check/validation.ts @@ -0,0 +1,11 @@ +import { createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; + +import { check } from "./check"; +import { regionsToArraySchema } from "../monitors"; + +export const selectCheckSchema = createSelectSchema(check).extend({ + regions: regionsToArraySchema, +}); + +export type Check = z.infer; diff --git a/packages/db/src/schema/monitors/validation.ts b/packages/db/src/schema/monitors/validation.ts index 355f2277b2..4e50e0036e 100644 --- a/packages/db/src/schema/monitors/validation.ts +++ b/packages/db/src/schema/monitors/validation.ts @@ -15,7 +15,7 @@ export const monitorJobTypesSchema = z.enum(monitorJobTypes); // biome-ignore lint/correctness/noUnusedVariables: function stringToArrayProcess(_string: T) {} -const regionsToArraySchema = z.preprocess((val) => { +export const regionsToArraySchema = z.preprocess((val) => { if (String(val).length > 0) { return String(val).split(","); } @@ -37,7 +37,7 @@ const headersToArraySchema = z.preprocess( } return []; }, - z.array(z.object({ key: z.string(), value: z.string() })).default([]), + z.array(z.object({ key: z.string(), value: z.string() })).default([]) ); export const selectMonitorSchema = createSelectSchema(monitor, { diff --git a/packages/tinybird/src/os-client.ts b/packages/tinybird/src/os-client.ts index 976c04fd53..cd4f33f0dd 100644 --- a/packages/tinybird/src/os-client.ts +++ b/packages/tinybird/src/os-client.ts @@ -336,6 +336,7 @@ export class OSTinybird { endpointSingleCheckList() { const parameters = z.object({ workspaceId: z.number(), + requestId: z.number().optional(), pageSize: z.number().optional(), page: z.number().optional(), }); From fa47a671c267aa73e17cda8d935c71e7df432d1a Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Sat, 5 Oct 2024 21:12:52 +0200 Subject: [PATCH 6/7] wip: route, metadata and more --- apps/server/src/v1/check/post.ts | 4 +- apps/server/src/v1/check/schema.ts | 8 + apps/server/src/v1/monitors/schema.ts | 1 + .../(dashboard)/checks/[id]/client.tsx | 4 +- .../(dashboard)/checks/[id]/layout.tsx | 2 +- .../(dashboard)/checks/[id]/page.tsx | 2 +- .../checks/[id]/request-details-dialog.tsx | 28 +- .../components/data-table/check/columns.tsx | 26 +- .../data-table/check/data-table.tsx | 2 +- .../forms/monitor/section-requests.tsx | 2 +- .../components/layout/header/app-header.tsx | 4 +- .../src/components/layout/header/app-tabs.tsx | 4 +- apps/web/src/components/pill.tsx | 29 + apps/web/src/config/pages.ts | 2 +- apps/web/src/lib/timing.ts | 5 +- packages/api/src/router/check.ts | 4 +- packages/api/src/router/tinybird/index.ts | 2 +- packages/db/drizzle/0037_rapid_jean_grey.sql | 1 + packages/db/drizzle/meta/0037_snapshot.json | 2205 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/check/check.ts | 6 +- packages/db/src/schema/check/validation.ts | 16 +- packages/db/src/schema/monitors/validation.ts | 2 +- 23 files changed, 2331 insertions(+), 35 deletions(-) create mode 100644 apps/web/src/components/pill.tsx create mode 100644 packages/db/drizzle/0037_rapid_jean_grey.sql create mode 100644 packages/db/drizzle/meta/0037_snapshot.json diff --git a/apps/server/src/v1/check/post.ts b/apps/server/src/v1/check/post.ts index 4badd35b76..03e9a7e080 100644 --- a/apps/server/src/v1/check/post.ts +++ b/apps/server/src/v1/check/post.ts @@ -48,7 +48,7 @@ export function registerPostCheck(api: typeof checkAPI) { const workspaceId = Number(c.get("workspaceId")); const input = c.req.valid("json"); - const { headers, regions, runCount, aggregated, ...rest } = data; + const { headers, metadata, regions, runCount, aggregated, ...rest } = data; const newCheck = await db .insert(check) @@ -56,6 +56,8 @@ export function registerPostCheck(api: typeof checkAPI) { workspaceId: Number(workspaceId), regions: regions.join(","), countRequests: runCount, + headers: headers?.length ? JSON.stringify(headers) : undefined, + metadata: metadata ? JSON.stringify(metadata) : undefined, ...rest, }) .returning() diff --git a/apps/server/src/v1/check/schema.ts b/apps/server/src/v1/check/schema.ts index d6aadf81e1..85ebc20ec2 100644 --- a/apps/server/src/v1/check/schema.ts +++ b/apps/server/src/v1/check/schema.ts @@ -19,6 +19,14 @@ export const CheckSchema = MonitorSchema.pick({ .boolean() .optional() .openapi({ description: "Whether to aggregate the results or not" }), + metadata: z + .record(z.string()) + .optional() + .openapi({ + description: + "The metadata of the check. Makes it easier to search for checks", + example: { env: "production", platform: "github" }, + }), // webhook: z // .string() // .optional() diff --git a/apps/server/src/v1/monitors/schema.ts b/apps/server/src/v1/monitors/schema.ts index 3fecc77924..c1eb59e95b 100644 --- a/apps/server/src/v1/monitors/schema.ts +++ b/apps/server/src/v1/monitors/schema.ts @@ -134,6 +134,7 @@ export const MonitorSchema = z method: z.enum(monitorMethods).default("GET").openapi({ example: "GET" }), body: z .preprocess((val) => { + if (typeof val === "object") return JSON.stringify(val); return String(val); }, z.string()) .nullish() diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/client.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/client.tsx index 0858e4093c..1b7b8619df 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/client.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/client.tsx @@ -21,14 +21,14 @@ export function Client({ totalRows }: { totalRows: number }) { // page: search.page + 1 pageSize: search.pageSize + 10, }, - { shallow: false } + { shallow: false }, ); } } window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); - }, [search]); + }, [search, setSearch, totalRows]); return null; } diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx index 66c6419b0d..e935b6d9d3 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/layout.tsx @@ -23,7 +23,7 @@ export default async function Layout({
    {check.url}
    } + description={
    {check.url}
    } actions={} /> {children} diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx index 90e3258a35..19d8aea7d5 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/page.tsx @@ -3,8 +3,8 @@ import { DataTable } from "@/components/data-table/single-check/data-table"; import { env } from "@/env"; import { api } from "@/trpc/server"; import { OSTinybird } from "@openstatus/tinybird"; -import { searchParamsCache } from "./search-params"; import { Client } from "./client"; +import { searchParamsCache } from "./search-params"; const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); diff --git a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx index 7e43fedfb0..2601af2154 100644 --- a/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx +++ b/apps/web/src/app/app/[workspaceSlug]/(dashboard)/checks/[id]/request-details-dialog.tsx @@ -1,4 +1,4 @@ -import { Check } from "@openstatus/db/src/schema"; +import type { Check } from "@openstatus/db/src/schema"; import { Button, Dialog, @@ -14,8 +14,8 @@ import { Minus } from "lucide-react"; export function RequestDetailsDialog({ check }: { check: Check }) { return ( - - + + @@ -24,21 +24,25 @@ export function RequestDetailsDialog({ check }: { check: Check }) {
    -
    URL
    +
    URL
    {check.url}
    -
    Method
    +
    Method
    {check.method}
    -
    Created At
    +
    Created At
    - {format(new Date(check.createdAt!), "LLL dd, y HH:mm:ss")} + {!check.createdAt ? ( + + ) : ( + format(new Date(check.createdAt), "LLL dd, y HH:mm:ss") + )}
    -
    Regions
    +
    Regions
    {check.regions.length ? ( check.regions.join(", ") @@ -48,7 +52,7 @@ export function RequestDetailsDialog({ check }: { check: Check }) {
    -
    Headers
    +
    Headers
    {!check.headers ? ( @@ -58,12 +62,12 @@ export function RequestDetailsDialog({ check }: { check: Check }) {
    -
    Body
    +
    Body
    - {!check.headers ? ( + {!check.body ? ( ) : ( -
    {JSON.stringify(check.headers, null, 2)}
    +
    {JSON.stringify(check.body, null, 2)}
    )}
    diff --git a/apps/web/src/components/data-table/check/columns.tsx b/apps/web/src/components/data-table/check/columns.tsx index b98aa4ef66..b7d1e2d10b 100644 --- a/apps/web/src/components/data-table/check/columns.tsx +++ b/apps/web/src/components/data-table/check/columns.tsx @@ -1,10 +1,11 @@ "use client"; +import { Pill } from "@/components/pill"; +import type { Check } from "@openstatus/db/src/schema"; import type { ColumnDef } from "@tanstack/react-table"; import { format } from "date-fns"; -import { Check } from "@openstatus/db/src/schema"; -import { DataTableBadges } from "../data-table-badges"; import { Minus } from "lucide-react"; +import { DataTableBadges } from "../data-table-badges"; export const columns: ColumnDef[] = [ { @@ -27,7 +28,26 @@ export const columns: ColumnDef[] = [ accessorKey: "url", header: "URL", cell: ({ row }) => { - return
    {row.getValue("url")}
    ; + return ( +
    {row.getValue("url")}
    + ); + }, + }, + { + accessorKey: "metadata", + header: "Metadata", + cell: ({ row }) => { + const metadata = row.original.metadata; + if (!Object.keys(metadata).length) return null; + return ( +
    + {Object.entries(metadata).map(([key, value]) => ( + + {value} + + ))} +
    + ); }, }, { diff --git a/apps/web/src/components/data-table/check/data-table.tsx b/apps/web/src/components/data-table/check/data-table.tsx index 898bd09ce8..e3173048fb 100644 --- a/apps/web/src/components/data-table/check/data-table.tsx +++ b/apps/web/src/components/data-table/check/data-table.tsx @@ -56,7 +56,7 @@ export function DataTable({ ? null : flexRender( header.column.columnDef.header, - header.getContext() + header.getContext(), )} ); diff --git a/apps/web/src/components/forms/monitor/section-requests.tsx b/apps/web/src/components/forms/monitor/section-requests.tsx index df9a009f41..1541b79d11 100644 --- a/apps/web/src/components/forms/monitor/section-requests.tsx +++ b/apps/web/src/components/forms/monitor/section-requests.tsx @@ -90,7 +90,7 @@ export function SectionRequests({ form, type }: Props) { HTTP TCP{" "} - + Coming soon diff --git a/apps/web/src/components/layout/header/app-header.tsx b/apps/web/src/components/layout/header/app-header.tsx index 36c65537de..295b53c5df 100644 --- a/apps/web/src/components/layout/header/app-header.tsx +++ b/apps/web/src/components/layout/header/app-header.tsx @@ -15,14 +15,14 @@ import { UserNav } from "./user-nav"; const lastChangelog = allChangelogs .sort( (a, b) => - new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime() + new Date(a.publishedAt).getTime() - new Date(b.publishedAt).getTime(), ) .pop(); export function AppHeader() { const [lastViewed, setLastViewed] = useCookieState( "last-viewed-changelog", - new Date(0).toISOString() + new Date(0).toISOString(), ); const show = diff --git a/apps/web/src/components/layout/header/app-tabs.tsx b/apps/web/src/components/layout/header/app-tabs.tsx index ed54e233e6..77b3833911 100644 --- a/apps/web/src/components/layout/header/app-tabs.tsx +++ b/apps/web/src/components/layout/header/app-tabs.tsx @@ -6,8 +6,8 @@ import { TabsContainer, TabsLink } from "@/components/dashboard/tabs-link"; import { StatusDot } from "@/components/monitor/status-dot"; import { pagesConfig } from "@/config/pages"; import { api } from "@/trpc/client"; -import { useEffect, useState } from "react"; import { Badge } from "@openstatus/ui"; +import { useEffect, useState } from "react"; export function AppTabs() { const params = useParams(); @@ -30,7 +30,7 @@ export function AppTabs() { > {title} {badge ? ( - + {badge} ) : null} diff --git a/apps/web/src/components/pill.tsx b/apps/web/src/components/pill.tsx new file mode 100644 index 0000000000..0e9ef6c87f --- /dev/null +++ b/apps/web/src/components/pill.tsx @@ -0,0 +1,29 @@ +import { cn } from "../lib/utils"; + +export interface PillProps extends React.HTMLAttributes { + label: React.ReactNode; + labelClassName?: string; +} + +export function Pill({ + label, + labelClassName, + className, + children, + ...props +}: PillProps) { + return ( +
    + + {label} + + {children} +
    + ); +} diff --git a/apps/web/src/config/pages.ts b/apps/web/src/config/pages.ts index 5cbd7a6009..00361cd98a 100644 --- a/apps/web/src/config/pages.ts +++ b/apps/web/src/config/pages.ts @@ -300,7 +300,7 @@ export const marketingPagesConfig = [ export function getPageBySegment( segment: string | string[], - currentPage: readonly Page[] = pagesConfig + currentPage: readonly Page[] = pagesConfig, ): Page | undefined { if (typeof segment === "string") { const page = currentPage.find((page) => page.segment === segment); diff --git a/apps/web/src/lib/timing.ts b/apps/web/src/lib/timing.ts index fcd753f0b1..5e76336deb 100644 --- a/apps/web/src/lib/timing.ts +++ b/apps/web/src/lib/timing.ts @@ -17,15 +17,16 @@ export function getTimingColor(timing: string) { export function getTimingPercentage( timing: Record, - latency?: number + latency?: number, ): Record { const total = latency || Object.values(timing).reduce((acc, curr) => acc + curr, 0); const percentage: Record = { ...timing }; + // biome-ignore lint/complexity/noForEach: Object.entries(timing).forEach(([key, value]) => { const pValue = Math.round((value / total) * 1000) / 1000; percentage[key as keyof typeof timing] = /^0\.00[0-9]+/.test( - pValue.toString() + pValue.toString(), ) ? "<1%" : `${(pValue * 100).toFixed(1)}%`; diff --git a/packages/api/src/router/check.ts b/packages/api/src/router/check.ts index 3c738ce4f1..4f08afcd34 100644 --- a/packages/api/src/router/check.ts +++ b/packages/api/src/router/check.ts @@ -24,8 +24,8 @@ export const checkRouter = createTRPCRouter({ .where( and( eq(check.id, opts.input.id), - eq(check.workspaceId, opts.ctx.workspace.id) - ) + eq(check.workspaceId, opts.ctx.workspace.id), + ), ) .get(); return selectCheckSchema.parse(_check); diff --git a/packages/api/src/router/tinybird/index.ts b/packages/api/src/router/tinybird/index.ts index eb56be530d..ed59495daf 100644 --- a/packages/api/src/router/tinybird/index.ts +++ b/packages/api/src/router/tinybird/index.ts @@ -29,7 +29,7 @@ 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); diff --git a/packages/db/drizzle/0037_rapid_jean_grey.sql b/packages/db/drizzle/0037_rapid_jean_grey.sql new file mode 100644 index 0000000000..a242624933 --- /dev/null +++ b/packages/db/drizzle/0037_rapid_jean_grey.sql @@ -0,0 +1 @@ +ALTER TABLE `check` ADD `metadata` text DEFAULT ''; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0037_snapshot.json b/packages/db/drizzle/meta/0037_snapshot.json new file mode 100644 index 0000000000..8c999de17a --- /dev/null +++ b/packages/db/drizzle/meta/0037_snapshot.json @@ -0,0 +1,2205 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ea013452-0fca-4d82-8c8d-fe7ade07e3bf", + "prevId": "7321a546-31be-4313-8e3f-235db16ddee4", + "tables": { + "status_report_to_monitors": { + "name": "status_report_to_monitors", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "status_report_to_monitors_monitor_id_monitor_id_fk": { + "name": "status_report_to_monitors_monitor_id_monitor_id_fk", + "tableFrom": "status_report_to_monitors", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_report_to_monitors_status_report_id_status_report_id_fk": { + "name": "status_report_to_monitors_status_report_id_status_report_id_fk", + "tableFrom": "status_report_to_monitors", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_report_to_monitors_monitor_id_status_report_id_pk": { + "columns": [ + "monitor_id", + "status_report_id" + ], + "name": "status_report_to_monitors_monitor_id_status_report_id_pk" + } + }, + "uniqueConstraints": {} + }, + "status_report": { + "name": "status_report", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "status_report_workspace_id_workspace_id_fk": { + "name": "status_report_workspace_id_workspace_id_fk", + "tableFrom": "status_report", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "status_report_page_id_page_id_fk": { + "name": "status_report_page_id_page_id_fk", + "tableFrom": "status_report", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "status_report_update": { + "name": "status_report_update", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text(4)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_report_id": { + "name": "status_report_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "status_report_update_status_report_id_status_report_id_fk": { + "name": "status_report_update_status_report_id_status_report_id_fk", + "tableFrom": "status_report_update", + "tableTo": "status_report", + "columnsFrom": [ + "status_report_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credential": { + "name": "credential", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_workspace_id_workspace_id_fk": { + "name": "integration_workspace_id_workspace_id_fk", + "tableFrom": "integration", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "page": { + "name": "page", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "slug": { + "name": "slug", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "published": { + "name": "published", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "password": { + "name": "password", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_protected": { + "name": "password_protected", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "page_slug_unique": { + "name": "page_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": { + "page_workspace_id_workspace_id_fk": { + "name": "page_workspace_id_workspace_id_fk", + "tableFrom": "page", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "monitor": { + "name": "monitor", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "job_type": { + "name": "job_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'http'" + }, + "periodicity": { + "name": "periodicity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'other'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "regions": { + "name": "regions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "url": { + "name": "url", + "type": "text(2048)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'GET'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 45000 + }, + "degraded_after": { + "name": "degraded_after", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assertions": { + "name": "assertions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "monitor_workspace_id_workspace_id_fk": { + "name": "monitor_workspace_id_workspace_id_fk", + "tableFrom": "monitor", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "monitors_to_pages": { + "name": "monitors_to_pages", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "monitors_to_pages_monitor_id_monitor_id_fk": { + "name": "monitors_to_pages_monitor_id_monitor_id_fk", + "tableFrom": "monitors_to_pages", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "monitors_to_pages_page_id_page_id_fk": { + "name": "monitors_to_pages_page_id_page_id_fk", + "tableFrom": "monitors_to_pages", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitors_to_pages_monitor_id_page_id_pk": { + "columns": [ + "monitor_id", + "page_id" + ], + "name": "monitors_to_pages_monitor_id_page_id_pk" + } + }, + "uniqueConstraints": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ends_at": { + "name": "ends_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "paid_until": { + "name": "paid_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "limits": { + "name": "limits", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "dsn": { + "name": "dsn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspace_slug_unique": { + "name": "workspace_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "workspace_stripe_id_unique": { + "name": "workspace_stripe_id_unique", + "columns": [ + "stripe_id" + ], + "isUnique": true + }, + "workspace_id_dsn_unique": { + "name": "workspace_id_dsn_unique", + "columns": [ + "id", + "dsn" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "columns": [ + "provider", + "provider_account_id" + ], + "name": "account_provider_provider_account_id_pk" + } + }, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text(256)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "user_tenant_id_unique": { + "name": "user_tenant_id_unique", + "columns": [ + "tenant_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users_to_workspaces": { + "name": "users_to_workspaces", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "users_to_workspaces_user_id_user_id_fk": { + "name": "users_to_workspaces_user_id_user_id_fk", + "tableFrom": "users_to_workspaces", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_to_workspaces_workspace_id_workspace_id_fk": { + "name": "users_to_workspaces_workspace_id_workspace_id_fk", + "tableFrom": "users_to_workspaces", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_to_workspaces_user_id_workspace_id_pk": { + "columns": [ + "user_id", + "workspace_id" + ], + "name": "users_to_workspaces_user_id_workspace_id_pk" + } + }, + "uniqueConstraints": {} + }, + "verification_token": { + "name": "verification_token", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verification_token_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verification_token_identifier_token_pk" + } + }, + "uniqueConstraints": {} + }, + "page_subscriber": { + "name": "page_subscriber", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "page_subscriber_page_id_page_id_fk": { + "name": "page_subscriber_page_id_page_id_fk", + "tableFrom": "page_subscriber", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notification": { + "name": "notification", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'{}'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_workspace_id_workspace_id_fk": { + "name": "notification_workspace_id_workspace_id_fk", + "tableFrom": "notification", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "notifications_to_monitors": { + "name": "notifications_to_monitors", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_to_monitors_monitor_id_monitor_id_fk": { + "name": "notifications_to_monitors_monitor_id_monitor_id_fk", + "tableFrom": "notifications_to_monitors", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notifications_to_monitors_notification_id_notification_id_fk": { + "name": "notifications_to_monitors_notification_id_notification_id_fk", + "tableFrom": "notifications_to_monitors", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notifications_to_monitors_monitor_id_notification_id_pk": { + "columns": [ + "monitor_id", + "notification_id" + ], + "name": "notifications_to_monitors_monitor_id_notification_id_pk" + } + }, + "uniqueConstraints": {} + }, + "monitor_status": { + "name": "monitor_status", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "monitor_status_idx": { + "name": "monitor_status_idx", + "columns": [ + "monitor_id", + "region" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_status_monitor_id_monitor_id_fk": { + "name": "monitor_status_monitor_id_monitor_id_fk", + "tableFrom": "monitor_status", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitor_status_monitor_id_region_pk": { + "columns": [ + "monitor_id", + "region" + ], + "name": "monitor_status_monitor_id_region_pk" + } + }, + "uniqueConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "incident": { + "name": "incident", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'triage'" + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "acknowledged_by": { + "name": "acknowledged_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "incident_screenshot_url": { + "name": "incident_screenshot_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recovery_screenshot_url": { + "name": "recovery_screenshot_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_resolved": { + "name": "auto_resolved", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "incident_monitor_id_started_at_unique": { + "name": "incident_monitor_id_started_at_unique", + "columns": [ + "monitor_id", + "started_at" + ], + "isUnique": true + } + }, + "foreignKeys": { + "incident_monitor_id_monitor_id_fk": { + "name": "incident_monitor_id_monitor_id_fk", + "tableFrom": "incident", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set default", + "onUpdate": "no action" + }, + "incident_workspace_id_workspace_id_fk": { + "name": "incident_workspace_id_workspace_id_fk", + "tableFrom": "incident", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_acknowledged_by_user_id_fk": { + "name": "incident_acknowledged_by_user_id_fk", + "tableFrom": "incident", + "tableTo": "user", + "columnsFrom": [ + "acknowledged_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incident_resolved_by_user_id_fk": { + "name": "incident_resolved_by_user_id_fk", + "tableFrom": "incident", + "tableTo": "user", + "columnsFrom": [ + "resolved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "monitor_tag": { + "name": "monitor_tag", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "monitor_tag_workspace_id_workspace_id_fk": { + "name": "monitor_tag_workspace_id_workspace_id_fk", + "tableFrom": "monitor_tag", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "monitor_tag_to_monitor": { + "name": "monitor_tag_to_monitor", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_tag_id": { + "name": "monitor_tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "monitor_tag_to_monitor_monitor_id_monitor_id_fk": { + "name": "monitor_tag_to_monitor_monitor_id_monitor_id_fk", + "tableFrom": "monitor_tag_to_monitor", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk": { + "name": "monitor_tag_to_monitor_monitor_tag_id_monitor_tag_id_fk", + "tableFrom": "monitor_tag_to_monitor", + "tableTo": "monitor_tag", + "columnsFrom": [ + "monitor_tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk": { + "columns": [ + "monitor_id", + "monitor_tag_id" + ], + "name": "monitor_tag_to_monitor_monitor_id_monitor_tag_id_pk" + } + }, + "uniqueConstraints": {} + }, + "application": { + "name": "application", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dsn": { + "name": "dsn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": { + "application_dsn_unique": { + "name": "application_dsn_unique", + "columns": [ + "dsn" + ], + "isUnique": true + } + }, + "foreignKeys": { + "application_workspace_id_workspace_id_fk": { + "name": "application_workspace_id_workspace_id_fk", + "tableFrom": "application", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "maintenance": { + "name": "maintenance", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "from": { + "name": "from", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "to": { + "name": "to", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "maintenance_workspace_id_workspace_id_fk": { + "name": "maintenance_workspace_id_workspace_id_fk", + "tableFrom": "maintenance", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "maintenance_page_id_page_id_fk": { + "name": "maintenance_page_id_page_id_fk", + "tableFrom": "maintenance", + "tableTo": "page", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "maintenance_to_monitor": { + "name": "maintenance_to_monitor", + "columns": { + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "maintenance_id": { + "name": "maintenance_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "maintenance_to_monitor_monitor_id_monitor_id_fk": { + "name": "maintenance_to_monitor_monitor_id_monitor_id_fk", + "tableFrom": "maintenance_to_monitor", + "tableTo": "monitor", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_to_monitor_maintenance_id_maintenance_id_fk": { + "name": "maintenance_to_monitor_maintenance_id_maintenance_id_fk", + "tableFrom": "maintenance_to_monitor", + "tableTo": "maintenance", + "columnsFrom": [ + "maintenance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "maintenance_to_monitor_monitor_id_maintenance_id_pk": { + "columns": [ + "maintenance_id", + "monitor_id" + ], + "name": "maintenance_to_monitor_monitor_id_maintenance_id_pk" + } + }, + "uniqueConstraints": {} + }, + "check": { + "name": "check", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "url": { + "name": "url", + "type": "text(4096)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'GET'" + }, + "regions": { + "name": "regions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "count_requests": { + "name": "count_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "workspace_id": { + "name": "workspace_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(strftime('%s', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "check_workspace_id_workspace_id_fk": { + "name": "check_workspace_id_workspace_id_fk", + "tableFrom": "check", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 746cb32301..3d44f2d0a6 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -260,6 +260,13 @@ "when": 1723459608109, "tag": "0036_gifted_deathbird", "breakpoints": true + }, + { + "idx": 37, + "version": "6", + "when": 1728151180899, + "tag": "0037_rapid_jean_grey", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/check/check.ts b/packages/db/src/schema/check/check.ts index 46a06e23c7..0d334d6759 100644 --- a/packages/db/src/schema/check/check.ts +++ b/packages/db/src/schema/check/check.ts @@ -12,10 +12,14 @@ export const check = sqliteTable("check", { regions: text("regions").default("").notNull(), countRequests: integer("count_requests").default(1), + /** + * @example '{ env: "production", platform: "github" }' + */ + metadata: text("metadata").default(""), workspaceId: integer("workspace_id").references(() => workspace.id), createdAt: integer("created_at", { mode: "timestamp" }).default( - sql`(strftime('%s', 'now'))` + sql`(strftime('%s', 'now'))`, ), }); diff --git a/packages/db/src/schema/check/validation.ts b/packages/db/src/schema/check/validation.ts index 0b4a9a4b69..053a133ed6 100644 --- a/packages/db/src/schema/check/validation.ts +++ b/packages/db/src/schema/check/validation.ts @@ -1,11 +1,25 @@ import { createSelectSchema } from "drizzle-zod"; import { z } from "zod"; -import { check } from "./check"; import { regionsToArraySchema } from "../monitors"; +import { check } from "./check"; export const selectCheckSchema = createSelectSchema(check).extend({ regions: regionsToArraySchema, + metadata: z + .preprocess((val) => { + try { + if (typeof val === "object") return val; + if (typeof val === "string" && val.length > 0) { + return JSON.parse(String(val)); + } + return {}; + } catch (e) { + console.error(e); + return {}; + } + }, z.record(z.string())) + .default({}), }); export type Check = z.infer; diff --git a/packages/db/src/schema/monitors/validation.ts b/packages/db/src/schema/monitors/validation.ts index 4e50e0036e..477c40c3ec 100644 --- a/packages/db/src/schema/monitors/validation.ts +++ b/packages/db/src/schema/monitors/validation.ts @@ -37,7 +37,7 @@ const headersToArraySchema = z.preprocess( } return []; }, - z.array(z.object({ key: z.string(), value: z.string() })).default([]) + z.array(z.object({ key: z.string(), value: z.string() })).default([]), ); export const selectMonitorSchema = createSelectSchema(monitor, { From 1d9834c1ada95383c906e1eac31cad3f4c9c1397 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Tue, 8 Oct 2024 21:10:13 +0200 Subject: [PATCH 7/7] chore: headers --- apps/server/src/v1/check/post.ts | 20 ++++++-------------- apps/server/src/v1/check/schema.ts | 5 ++++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/server/src/v1/check/post.ts b/apps/server/src/v1/check/post.ts index 03e9a7e080..2fe1efe887 100644 --- a/apps/server/src/v1/check/post.ts +++ b/apps/server/src/v1/check/post.ts @@ -56,7 +56,7 @@ export function registerPostCheck(api: typeof checkAPI) { workspaceId: Number(workspaceId), regions: regions.join(","), countRequests: runCount, - headers: headers?.length ? JSON.stringify(headers) : undefined, + headers: headers ? JSON.stringify(headers) : undefined, metadata: metadata ? JSON.stringify(metadata) : undefined, ...rest, }) @@ -80,16 +80,8 @@ export function registerPostCheck(api: typeof checkAPI) { workspaceId: workspaceId, url: input.url, method: input.method, - headers: input.headers?.reduce((acc, { key, value }) => { - if (!key) return acc; // key === "" is an invalid header - - return { - // biome-ignore lint/performance/noAccumulatingSpread: - ...acc, - [key]: value, - }; - }, {}), - body: input.body ? input.body : undefined, + headers: input.headers ?? undefined, + body: input.body ?? undefined, }), }); currentFetch.push(r); @@ -159,10 +151,10 @@ function getTiming(data: z.infer[]): ReturnGetTiming { prev.dns.push(curr.timing.dnsDone - curr.timing.dnsStart); prev.connect.push(curr.timing.connectDone - curr.timing.connectStart); prev.tls.push( - curr.timing.tlsHandshakeDone - curr.timing.tlsHandshakeStart, + curr.timing.tlsHandshakeDone - curr.timing.tlsHandshakeStart ); prev.firstByte.push( - curr.timing.firstByteDone - curr.timing.firstByteStart, + curr.timing.firstByteDone - curr.timing.firstByteStart ); prev.transfer.push(curr.timing.transferDone - curr.timing.transferStart); prev.latency.push(curr.latency); @@ -175,7 +167,7 @@ function getTiming(data: z.infer[]): ReturnGetTiming { firstByte: [], transfer: [], latency: [], - } as ReturnGetTiming, + } as ReturnGetTiming ); } diff --git a/apps/server/src/v1/check/schema.ts b/apps/server/src/v1/check/schema.ts index 85ebc20ec2..fb6460113e 100644 --- a/apps/server/src/v1/check/schema.ts +++ b/apps/server/src/v1/check/schema.ts @@ -4,7 +4,6 @@ import { MonitorSchema } from "../monitors/schema"; export const CheckSchema = MonitorSchema.pick({ url: true, body: true, - headers: true, method: true, regions: true, }) @@ -27,6 +26,9 @@ export const CheckSchema = MonitorSchema.pick({ "The metadata of the check. Makes it easier to search for checks", example: { env: "production", platform: "github" }, }), + headers: z.record(z.string()).optional().openapi({ + description: "The headers to send with the request", + }), // webhook: z // .string() // .optional() @@ -140,6 +142,7 @@ export const AggregatedResult = z.object({ export const CheckPostResponseSchema = z.object({ id: z.number().int().openapi({ description: "The id of the check" }), + // TBD: include the region + more (e.g. statusCode, latency, ... ) in here! raw: z.array(TimingSchema).openapi({ description: "The raw data of the check", }),