Openstatus www.openstatus.dev

refactor: tinybird structure (#1070)

* refactor: tb

* feat: hover copy to clipboard

* wip: tcp

* chore: clean ups

* chore: format

* chore: format

* wip:

* ci: apply automated fixes

* fix: api

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Maximilian Kaske
autofix-ci[bot]
and committed by
GitHub
e475b2a0 0b91e08c

+2383 -1979
+4 -4
apps/server/src/v1/monitors/results/get.ts
··· 11 11 import type { monitorsApi } from "../index"; 12 12 import { ParamsSchema, ResultRun } from "../schema"; 13 13 14 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 14 + const tb = new OSTinybird(env.TINY_BIRD_API_KEY); 15 15 16 16 const getMonitorStats = createRoute({ 17 17 method: "get", ··· 69 69 throw new HTTPException(404, { message: "Not Found" }); 70 70 } 71 71 // Fetch result from tb pipe 72 - const data = await tb.getResultForOnDemandCheckHttp()({ 72 + const data = await tb.getResultForOnDemandCheckHttp({ 73 73 monitorId: _monitor.id, 74 74 timestamp: _monitorRun.runnedAt?.getTime(), 75 75 url: _monitor.url, 76 76 }); 77 77 // return array of results 78 - if (!data) { 78 + if (!data || data.data.length === 0) { 79 79 throw new HTTPException(404, { message: "Not Found" }); 80 80 } 81 - return c.json(data, 200); 81 + return c.json(data.data, 200); 82 82 }); 83 83 }
+4 -7
apps/server/src/v1/monitors/summary/get.ts
··· 12 12 import type { monitorsApi } from "../index"; 13 13 import { ParamsSchema } from "../schema"; 14 14 15 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 15 + const tb = new OSTinybird(env.TINY_BIRD_API_KEY); 16 16 const redis = Redis.fromEnv(); 17 17 18 18 const dailyStatsSchema = z.object({ ··· 82 82 return c.json({ data: cache }, 200); 83 83 } 84 84 85 - // FIXME: we should use the OSTinybird client 86 85 console.log("fetching from tinybird"); 87 - const res = await tb.endpointStatusPeriod("45d")({ 88 - monitorId: id, 89 - }); 86 + const res = await tb.httpStatus45d({ monitorId: id }); 90 87 91 - if (res === undefined) { 88 + if (!res || res.data.length === 0) { 92 89 throw new HTTPException(404, { message: "Not Found" }); 93 90 } 94 91 await redis.set(`${id}-daily-stats`, res, { ex: 600 }); 95 92 96 - return c.json({ data: res }, 200); 93 + return c.json({ data: res.data }, 200); 97 94 }); 98 95 }
+3 -3
apps/web/src/app/(content)/features/_components/tracker-example.tsx
··· 8 8 export function TrackerWithVisibilityToggle() { 9 9 const [visible, setVisible] = useState(true); 10 10 return ( 11 - <div className="flex flex-col gap-8 my-auto"> 11 + <div className="my-auto flex flex-col gap-8"> 12 12 <div className="items-top flex space-x-2"> 13 13 <Checkbox 14 14 id="visibility" ··· 18 18 <div className="grid gap-1.5 leading-none"> 19 19 <label 20 20 htmlFor="visibility" 21 - className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 21 + className="font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 22 22 > 23 23 Show values 24 24 </label> 25 - <p className="text-sm text-muted-foreground"> 25 + <p className="text-muted-foreground text-sm"> 26 26 Share the uptime and number of requests. 27 27 </p> 28 28 </div>
+1 -2
apps/web/src/app/(content)/features/monitoring/page.tsx
··· 8 8 import { RegionsPreset } from "@/components/monitor-dashboard/region-preset"; 9 9 import { ResponseDetailTabs } from "@/components/ping-response-analysis/response-detail-tabs"; 10 10 import { marketingProductPagesConfig } from "@/config/pages"; 11 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 12 - import type { Region } from "@openstatus/tinybird"; 11 + import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 13 12 import { Button } from "@openstatus/ui/src/components/button"; 14 13 import { Skeleton } from "@openstatus/ui/src/components/skeleton"; 15 14 import { allUnrelateds } from "contentlayer/generated";
-94
apps/web/src/app/_components/event-table.tsx
··· 1 - "use client"; 2 - 3 - import { formatDistance } from "date-fns"; 4 - import React from "react"; 5 - 6 - import type { Ping } from "@openstatus/tinybird"; 7 - import { 8 - Badge, 9 - Button, 10 - Table, 11 - TableBody, 12 - TableCaption, 13 - TableCell, 14 - TableHead, 15 - TableHeader, 16 - TableRow, 17 - } from "@openstatus/ui"; 18 - 19 - import { cn } from "@/lib/utils"; 20 - 21 - export function EventTable({ events }: { events: Ping[] }) { 22 - const [open, toggle] = React.useReducer((state) => !state, false); 23 - return ( 24 - <div className="relative max-h-56 overflow-hidden"> 25 - <div className="relative max-h-56 overflow-y-scroll"> 26 - <Table> 27 - <TableCaption> 28 - A list of the latest {events.length} pings. 29 - </TableCaption> 30 - <TableHeader> 31 - <TableRow className="sticky top-0"> 32 - <TableHead>Time</TableHead> 33 - <TableHead>Status</TableHead> 34 - <TableHead>Latency (ms)</TableHead> 35 - <TableHead className="text-right">Region</TableHead> 36 - </TableRow> 37 - </TableHeader> 38 - <TableBody> 39 - {events.map((event) => { 40 - const isOk = event.statusCode === 200; 41 - return ( 42 - <TableRow key={`${event.timestamp}-${event.region}`}> 43 - <TableCell className="font-medium"> 44 - {formatDistance(new Date(event.timestamp), new Date(), { 45 - addSuffix: true, 46 - includeSeconds: true, 47 - })} 48 - </TableCell> 49 - <TableCell> 50 - <Badge 51 - variant="outline" 52 - className={cn( 53 - "px-2 py-0.5 text-xs", 54 - isOk 55 - ? "border-green-500/20 bg-green-500/10" 56 - : "border-red-500/20 bg-red-500/10", 57 - )} 58 - > 59 - {event.statusCode} 60 - <div 61 - className={cn( 62 - "ml-1 h-1.5 w-1.5 rounded-full bg-foreground", 63 - isOk ? "bg-green-500" : "bg-red-500", 64 - )} 65 - /> 66 - </Badge> 67 - </TableCell> 68 - <TableCell className="font-light text-muted-foreground"> 69 - {event.latency} 70 - </TableCell> 71 - <TableCell className="truncate text-right text-muted-foreground"> 72 - {event.region} 73 - </TableCell> 74 - </TableRow> 75 - ); 76 - })} 77 - </TableBody> 78 - </Table> 79 - </div> 80 - {!open && ( 81 - <div className="absolute inset-0 flex items-end justify-center bg-gradient bg-gradient-to-b from-20% from-transparent to-background"> 82 - <Button 83 - onClick={toggle} 84 - variant="outline" 85 - size="sm" 86 - className="rounded-full backdrop-blur-sm" 87 - > 88 - A total of {events.length} events. 89 - </Button> 90 - </div> 91 - )} 92 - </div> 93 - ); 94 - }
-200
apps/web/src/app/_components/input-search.tsx
··· 1 - /// <reference lib="dom" /> 2 - 3 - "use client"; 4 - 5 - import { Command as CommandPrimitive, useCommandState } from "cmdk"; 6 - import React, { useEffect, useMemo, useRef, useState } from "react"; 7 - 8 - import type { Ping } from "@openstatus/tinybird"; 9 - import { 10 - Command, 11 - CommandEmpty, 12 - CommandGroup, 13 - CommandItem, 14 - CommandList, 15 - } from "@openstatus/ui"; 16 - 17 - // TODO: once stable, use the shallow route to store the search params inside of the search params 18 - 19 - export function InputSearch({ 20 - events, 21 - onSearch, 22 - }: { 23 - onSearch(value: Record<string, string>): void; 24 - events: Ping[]; 25 - }) { 26 - const inputRef = useRef<HTMLInputElement>(null); 27 - const [open, setOpen] = useState<boolean>(false); 28 - const [inputValue, setInputValue] = useState<string>(""); 29 - const [currentWord, setCurrentWord] = useState(""); 30 - 31 - // TODO: check if there is a move efficient way 32 - useEffect(() => { 33 - const searchparams = inputValue 34 - .trim() 35 - .split(" ") 36 - .reduce( 37 - (prev, curr) => { 38 - const [name, value] = curr.split(":"); 39 - if (value && name && curr !== currentWord) { 40 - // TODO: support multiple value with value.split(",") 41 - prev[name] = value; 42 - } 43 - return prev; 44 - }, 45 - {} as Record<string, string>, 46 - ); 47 - onSearch(searchparams); 48 - }, [onSearch, inputValue, currentWord]); 49 - 50 - // DEFINE YOUR SEARCH PARAMETERS 51 - const search = useMemo( 52 - () => 53 - events.reduce( 54 - (prev, curr) => { 55 - return { 56 - // biome-ignore lint/performance/noAccumulatingSpread: <explanation> 57 - ...prev, 58 - status: [...new Set([curr.statusCode, ...(prev.status || [])])], 59 - region: [...new Set([curr.region, ...(prev.region || [])])], 60 - }; 61 - }, 62 - // defaultState 63 - { limit: [10, 25, 50], status: [], region: [] } as { 64 - status: (number | null)[]; 65 - limit: number[]; 66 - region: string[]; 67 - }, 68 - ), 69 - [events], 70 - ); 71 - 72 - type SearchKey = keyof typeof search; 73 - 74 - return ( 75 - <Command 76 - className="overflow-visible bg-transparent" 77 - filter={(value) => { 78 - if (value.includes(currentWord.toLowerCase())) return 1; 79 - return 0; 80 - }} 81 - > 82 - <CommandPrimitive.Input 83 - ref={inputRef} 84 - value={inputValue} 85 - onValueChange={setInputValue} 86 - onKeyDown={(e) => { 87 - if (e.key === "Escape") inputRef?.current?.blur(); 88 - }} 89 - onBlur={() => setOpen(false)} 90 - onFocus={() => setOpen(true)} 91 - onInput={(e) => { 92 - const caretPositionStart = e.currentTarget?.selectionStart || -1; 93 - const inputValue = e.currentTarget?.value || ""; 94 - 95 - let start = caretPositionStart; 96 - let end = caretPositionStart; 97 - 98 - while (start > 0 && inputValue[start - 1] !== " ") { 99 - start--; 100 - } 101 - while (end < inputValue.length && inputValue[end] !== " ") { 102 - end++; 103 - } 104 - 105 - const word = inputValue.substring(start, end); 106 - setCurrentWord(word); 107 - }} 108 - placeholder={`${events.length} total logs found...`} 109 - className="flex-1 rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none ring-offset-background placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:ring-offset-2" 110 - /> 111 - <div className="relative mt-2"> 112 - {open ? ( 113 - <div className="absolute top-0 z-10 w-full animate-in rounded-md border bg-popover text-popover-foreground shadow-md outline-none"> 114 - <CommandList> 115 - <CommandGroup className="max-h-64 overflow-auto"> 116 - {Object.keys(search).map((key) => { 117 - if ( 118 - inputValue.includes(`${key}:`) && 119 - !currentWord.includes(`${key}:`) 120 - ) 121 - return null; 122 - return ( 123 - <React.Fragment key={key}> 124 - <CommandItem 125 - value={key} 126 - onMouseDown={(e) => { 127 - e.preventDefault(); 128 - e.stopPropagation(); 129 - }} 130 - onSelect={(value) => { 131 - setInputValue((prev) => { 132 - if (currentWord.trim() === "") { 133 - const input = `${prev}${value}`; 134 - return `${input}:`; 135 - } 136 - // lots of cheat 137 - const isStarting = currentWord === prev; 138 - const prefix = isStarting ? "" : " "; 139 - const input = prev.replace( 140 - `${prefix}${currentWord}`, 141 - `${prefix}${value}`, 142 - ); 143 - return `${input}:`; 144 - }); 145 - setCurrentWord(`${value}:`); 146 - }} 147 - className="group" 148 - > 149 - {key} 150 - <span className="ml-1 hidden truncate text-muted-foreground/90 group-aria-[selected=true]:block"> 151 - {search[key as SearchKey] 152 - .map((str) => `[${str}]`) 153 - .join(" ")} 154 - </span> 155 - </CommandItem> 156 - {search[key as SearchKey].map((option) => { 157 - return ( 158 - <SubItem 159 - key={option} 160 - value={`${key}:${option}`} 161 - onMouseDown={(e) => { 162 - e.preventDefault(); 163 - e.stopPropagation(); 164 - }} 165 - onSelect={(value) => { 166 - setInputValue((prev) => { 167 - const input = prev.replace(currentWord, value); 168 - return `${input.trim()} `; 169 - }); 170 - setCurrentWord(""); 171 - }} 172 - {...{ currentWord }} 173 - > 174 - {option} 175 - </SubItem> 176 - ); 177 - })} 178 - </React.Fragment> 179 - ); 180 - })} 181 - </CommandGroup> 182 - <CommandEmpty>No results found.</CommandEmpty> 183 - </CommandList> 184 - </div> 185 - ) : null} 186 - </div> 187 - </Command> 188 - ); 189 - } 190 - 191 - interface SubItemProps 192 - extends React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> { 193 - currentWord: string; 194 - } 195 - 196 - const SubItem = ({ currentWord, ...props }: SubItemProps) => { 197 - const search = useCommandState((state) => state.search); 198 - if (!search.includes(":") || !currentWord.includes(":")) return null; 199 - return <CommandItem {...props} />; 200 - };
-33
apps/web/src/app/_components/table-input-container.tsx
··· 1 - "use client"; 2 - 3 - import React from "react"; 4 - 5 - import type { Ping } from "@openstatus/tinybird"; 6 - 7 - import { EventTable } from "./event-table"; 8 - import { InputSearch } from "./input-search"; 9 - 10 - // TODO: once stable, use the shallow route to store the search params inside of the search params 11 - 12 - export function TableInputContainer({ events }: { events: Ping[] }) { 13 - const [search, setSearch] = React.useState<Record<string, string>>({}); 14 - 15 - const filteredEvents = events 16 - .filter((event) => { 17 - if (search?.status && event.statusCode !== Number(search.status)) { 18 - return false; 19 - } 20 - if (search?.region && event.region !== search.region) { 21 - return false; 22 - } 23 - return true; 24 - }) 25 - .slice(0, search.limit ? Number(search.limit) : 100); 26 - 27 - return ( 28 - <> 29 - <InputSearch events={events} onSearch={setSearch} /> 30 - <EventTable events={filteredEvents} /> 31 - </> 32 - ); 33 - }
-44
apps/web/src/app/api/checker/test/route.ts
··· 1 - import { NextResponse } from "next/server"; 2 - import { z } from "zod"; 3 - 4 - import { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 5 - 6 - import { checkRegion } from "@/components/ping-response-analysis/utils"; 7 - import { httpPayloadSchema } from "@openstatus/utils"; 8 - import { isAnInvalidTestUrl } from "../utils"; 9 - 10 - export const runtime = "edge"; 11 - export const preferredRegion = "auto"; 12 - export const dynamic = "force-dynamic"; 13 - export const revalidate = 0; 14 - 15 - export function GET() { 16 - return NextResponse.json({ success: true }); 17 - } 18 - 19 - export async function POST(request: Request) { 20 - try { 21 - const json = await request.json(); 22 - const _valid = httpPayloadSchema 23 - .pick({ url: true, method: true, headers: true, body: true }) 24 - .merge(z.object({ region: monitorFlyRegionSchema.default("ams") })) 25 - .safeParse(json); 26 - 27 - if (!_valid.success) { 28 - return NextResponse.json({ success: false }, { status: 400 }); 29 - } 30 - 31 - const { url, region, method, headers, body } = _valid.data; 32 - // 🧑‍💻 for the smart one who want to create a loop hole 33 - if (isAnInvalidTestUrl(url)) { 34 - return NextResponse.json({ success: true }, { status: 200 }); 35 - } 36 - 37 - const res = await checkRegion(url, region, { method, headers, body }); 38 - 39 - return NextResponse.json(res); 40 - } catch (e) { 41 - console.error(e); 42 - return NextResponse.json({ success: false }, { status: 400 }); 43 - } 44 - }
+2 -2
apps/web/src/app/api/checker/test/tcp/route.ts
··· 21 21 try { 22 22 const json = await request.json(); 23 23 const _valid = tcpPayload 24 + .pick({ url: true }) 24 25 .merge(z.object({ region: monitorFlyRegionSchema.default("ams") })) 25 26 .safeParse(json); 26 27 ··· 40 41 } 41 42 async function checkTCP(url: string, region: MonitorFlyRegion) { 42 43 // 43 - const res = await fetch(`https://checker.openstatus.dev/ping/tcp/${region}`, { 44 + const res = await fetch(`https://checker.openstatus.dev/tcp/${region}`, { 44 45 headers: { 45 46 Authorization: `Basic ${process.env.CRON_SECRET}`, 46 47 "Content-Type": "application/json", ··· 58 59 const data = TCPResponse.safeParse(json); 59 60 60 61 if (!data.success) { 61 - console.log(json); 62 62 console.error( 63 63 `something went wrong with result ${json} request to ${url} error ${data.error.message}`, 64 64 );
+3 -3
apps/web/src/app/api/checker/test/tcp/schema.ts
··· 12 12 }); 13 13 14 14 export const TCPResponse = z.object({ 15 - requestId: z.string().optional(), 16 - workspaceId: z.string(), 17 - monitorId: z.string(), 15 + requestId: z.number().optional(), 16 + workspaceId: z.number().optional(), 17 + monitorId: z.number().optional(), 18 18 timestamp: z.number(), 19 19 timing: z.object({ 20 20 tcpStart: z.number(),
+6 -2
apps/web/src/app/api/og/_components/tracker.tsx
··· 1 - import type { Monitor } from "@openstatus/tinybird"; 2 1 import { Tracker as OSTracker, classNames } from "@openstatus/tracker"; 3 2 3 + import type { ResponseStatusTracker } from "@/lib/tb"; 4 4 import { cn, formatDate } from "@/lib/utils"; 5 5 6 - export function Tracker({ data }: { data: Monitor[] }) { 6 + interface TrackerProps { 7 + data: ResponseStatusTracker[]; 8 + } 9 + 10 + export function Tracker({ data }: TrackerProps) { 7 11 const tracker = new OSTracker({ data }); 8 12 9 13 return (
+9 -9
apps/web/src/app/api/og/monitor/route.tsx
··· 8 8 import { Tracker } from "../_components/tracker"; 9 9 import { SIZE, calSemiBold, interLight, interRegular } from "../utils"; 10 10 11 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 11 + const tb = new OSTinybird(env.TINY_BIRD_API_KEY); 12 12 13 13 export const runtime = "edge"; 14 14 ··· 29 29 const monitorId = 30 30 (searchParams.has("id") && searchParams.get("id")) || undefined; 31 31 32 - const data = 33 - (monitorId && 34 - (await tb.endpointStatusPeriod("45d")({ 35 - monitorId, 36 - }))) || 37 - []; 32 + // TODO: we need to pass the monitor type here 33 + 34 + const res = (monitorId && 35 + (await tb.httpStatus45d({ 36 + monitorId, 37 + }))) || { data: [] }; 38 38 39 39 return new ImageResponse( 40 40 <BasicLayout 41 41 title={title} 42 42 description={description} 43 - tw={data.length === 0 ? "mt-32" : undefined} 43 + tw={res.data.length === 0 ? "mt-32" : undefined} 44 44 > 45 - {data.length ? <Tracker data={data} /> : null} 45 + {res.data.length ? <Tracker data={res.data} /> : null} 46 46 </BasicLayout>, 47 47 { 48 48 ...SIZE,
+10 -18
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/page.tsx
··· 1 1 import Link from "next/link"; 2 2 3 - import { OSTinybird } from "@openstatus/tinybird"; 4 3 import { Button } from "@openstatus/ui/src/components/button"; 5 4 6 5 import { EmptyState } from "@/components/dashboard/empty-state"; 7 6 import { Limit } from "@/components/dashboard/limit"; 8 7 import { columns } from "@/components/data-table/monitor/columns"; 9 8 import { DataTable } from "@/components/data-table/monitor/data-table"; 10 - import { env } from "@/env"; 9 + import { prepareMetricsByPeriod, prepareStatusByPeriod } from "@/lib/tb"; 11 10 import { api } from "@/trpc/server"; 12 11 import { searchParamsCache } from "./search-params"; 13 - 14 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 15 12 16 13 export default async function MonitorPage({ 17 14 searchParams, ··· 46 43 // use Suspense and Client call instead? 47 44 const monitorsWithData = await Promise.all( 48 45 monitors.map(async (monitor) => { 46 + const type = monitor.jobType as "http" | "tcp"; 49 47 const [metrics, data] = await Promise.all([ 50 - tb.endpointMetrics("1d")( 51 - { 52 - monitorId: String(monitor.id), 53 - }, 54 - { cache: "no-store", revalidate: 0 }, 55 - ), 56 - tb.endpointStatusPeriod("7d")( 57 - { 58 - monitorId: String(monitor.id), 59 - }, 60 - { cache: "no-store", revalidate: 0 }, 61 - ), 48 + prepareMetricsByPeriod("1d", type).getData({ 49 + monitorId: String(monitor.id), 50 + }), 51 + prepareStatusByPeriod("7d", type).getData({ 52 + monitorId: String(monitor.id), 53 + }), 62 54 ]); 63 55 64 - const [current] = metrics?.sort((a, b) => 56 + const [current] = metrics.data?.sort((a, b) => 65 57 (a.lastTimestamp || 0) - (b.lastTimestamp || 0) < 0 ? 1 : -1, 66 58 ) || [undefined]; 67 59 ··· 80 72 return { 81 73 monitor, 82 74 metrics: current, 83 - data, 75 + data: data.data, 84 76 incidents, 85 77 maintenances, 86 78 tags,
+11 -14
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/_components/data-table-wrapper.tsx
··· 11 11 import { Suspense, use } from "react"; 12 12 13 13 import * as assertions from "@openstatus/assertions"; 14 - import type { OSTinybird } from "@openstatus/tinybird"; 15 14 16 15 import { CopyToClipboardButton } from "@/components/dashboard/copy-to-clipboard-button"; 17 16 import { columns } from "@/components/data-table/columns"; ··· 23 22 import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 24 23 import type { z } from "zod"; 25 24 26 - // EXAMPLE: get the type of the response of the endpoint 27 - // biome-ignore lint/correctness/noUnusedVariables: <explanation> 28 - type T = Awaited<ReturnType<ReturnType<OSTinybird["endpointList"]>>>; 29 - 30 25 // FIXME: use proper type 31 26 export type Monitor = { 27 + type: "http" | "tcp"; 32 28 monitorId: string; 33 - url: string; 34 29 latency: number; 35 30 region: z.infer<typeof monitorFlyRegionSchema>; 36 - statusCode: number | null; 31 + statusCode?: number | null; 37 32 timestamp: number; 38 33 workspaceId: string; 39 34 cronTimestamp: number | null; 40 35 error: boolean; 41 - assertions?: string | null; 42 36 trigger: Trigger | null; 43 37 }; 44 38 ··· 55 49 <DataTable 56 50 columns={columns} 57 51 data={data} 58 - getRowCanExpand={() => true} 52 + // REMINDER: we currently only support HTTP monitors with more details 53 + getRowCanExpand={(row) => row.original.type === "http"} 59 54 renderSubComponent={renderSubComponent} 60 55 defaultColumnFilters={filters} 61 56 defaultPagination={pagination} 57 + defaultVisibility={ 58 + data.length && data[0].type === "tcp" ? { statusCode: false } : {} 59 + } 62 60 /> 63 61 ); 64 62 } ··· 77 75 ); 78 76 } 79 77 78 + // REMINDER: only HTTP monitors have more details 80 79 function Details({ row }: { row: Row<Monitor> }) { 81 80 const data = use( 82 - api.tinybird.responseDetails.query({ 81 + api.tinybird.httpGetMonthly.query({ 83 82 monitorId: row.original.monitorId, 84 - url: row.original.url, 85 83 region: row.original.region, 86 84 cronTimestamp: row.original.cronTimestamp || undefined, 87 85 }), 88 86 ); 89 87 90 - if (!data || data.length === 0) return <p>Something went wrong</p>; 88 + if (!data.data || data.data.length === 0) return <p>Something went wrong</p>; 91 89 92 - const first = data?.[0]; 90 + const first = data.data?.[0]; 93 91 94 92 // FIXME: ugly hack 95 93 const url = new URL(window.location.href.replace("/data", "/details")); 96 94 url.searchParams.set("monitorId", row.original.monitorId); 97 95 url.searchParams.set("region", row.original.region); 98 96 url.searchParams.set("cronTimestamp", String(row.original.cronTimestamp)); 99 - url.searchParams.set("url", row.original.url); 100 97 101 98 return ( 102 99 <div className="relative">
+10 -9
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/page.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 import * as React from "react"; 3 3 4 - import { OSTinybird } from "@openstatus/tinybird"; 5 - 6 4 import { DatePickerPreset } from "@/components/monitor-dashboard/date-picker-preset"; 7 - import { env } from "@/env"; 5 + import { prepareListByPeriod } from "@/lib/tb"; 8 6 import { api } from "@/trpc/server"; 9 7 import { DataTableWrapper } from "./_components/data-table-wrapper"; 10 8 import { searchParamsCache } from "./search-params"; 11 - 12 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 13 9 14 10 export default async function Page({ 15 11 params, ··· 27 23 28 24 if (!monitor) return notFound(); 29 25 30 - const allowedPeriods = ["1h", "1d", "3d", "7d"] as const; 26 + const type = monitor.jobType as "http" | "tcp"; 27 + 28 + // FIXME: make it dynamic based on the workspace plan 29 + const allowedPeriods = ["1d", "7d", "14d"] as const; 31 30 const period = allowedPeriods.find((i) => i === search.period) || "1d"; 32 31 33 - const data = await tb.endpointList(period)({ monitorId: id }); 32 + const res = await prepareListByPeriod(period, type).getData({ 33 + monitorId: id, 34 + }); 34 35 35 - if (!data) return null; 36 + if (!res.data || res.data.length === 0) return null; 36 37 37 38 return ( 38 39 <div className="grid gap-4"> ··· 47 48 </div> 48 49 {/* FIXME: we display all the regions even though a user might not have all supported in their plan */} 49 50 <DataTableWrapper 50 - data={data} 51 + data={res.data} 51 52 filters={[ 52 53 { id: "statusCode", value: search.statusCode }, 53 54 { id: "region", value: search.regions },
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/details/page.tsx
··· 23 23 await api.monitor.getMonitorById.query({ 24 24 id: Number.parseInt(search.monitorId), 25 25 }); 26 - return <ResponseDetails {...search} />; 26 + return <ResponseDetails type="http" {...search} />; 27 27 } catch (_e) { 28 28 return <PageEmptyState />; 29 29 }
+15 -12
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 import * as React from "react"; 3 3 4 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 5 - import type { Region } from "@openstatus/tinybird"; 6 - import { OSTinybird } from "@openstatus/tinybird"; 4 + import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 7 5 import { Separator } from "@openstatus/ui"; 8 6 9 7 import { CombinedChartWrapper } from "@/components/monitor-charts/combined-chart-wrapper"; 10 8 import { ButtonReset } from "@/components/monitor-dashboard/button-reset"; 11 9 import { DatePickerPreset } from "@/components/monitor-dashboard/date-picker-preset"; 12 10 import { Metrics } from "@/components/monitor-dashboard/metrics"; 13 - import { env } from "@/env"; 14 11 import { getMinutesByInterval, periods } from "@/lib/monitor/utils"; 15 12 import { getPreferredSettings } from "@/lib/preferred-settings/server"; 13 + import { 14 + prepareMetricByIntervalByPeriod, 15 + prepareMetricByRegionByPeriod, 16 + prepareMetricsByPeriod, 17 + } from "@/lib/tb"; 16 18 import { api } from "@/trpc/server"; 17 19 import { 18 20 DEFAULT_INTERVAL, ··· 21 23 searchParamsCache, 22 24 } from "./search-params"; 23 25 24 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 25 - 26 26 export default async function Page({ 27 27 params, 28 28 searchParams, ··· 42 42 if (!monitor) return notFound(); 43 43 44 44 const { period, quantile, interval, regions } = search; 45 + const type = monitor.jobType as "http" | "tcp"; 45 46 46 47 // TODO: work it out easier 47 48 const intervalMinutes = getMinutesByInterval(interval); ··· 51 52 const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 52 53 53 54 const [metrics, data, metricsByRegion] = await Promise.all([ 54 - tb.endpointMetrics(period)({ monitorId: id }), 55 - tb.endpointChart(period)({ 55 + prepareMetricsByPeriod(period, type).getData({ 56 + monitorId: id, 57 + }), 58 + prepareMetricByIntervalByPeriod(period, type).getData({ 56 59 monitorId: id, 57 60 interval: minutes, 58 61 }), 59 - tb.endpointMetricsByRegion(period)({ 62 + prepareMetricByRegionByPeriod(period, type).getData({ 60 63 monitorId: id, 61 64 }), 62 65 ]); ··· 84 87 <DatePickerPreset defaultValue={period} values={periods} /> 85 88 {isDirty ? <ButtonReset /> : null} 86 89 </div> 87 - <Metrics metrics={metrics} period={period} showErrorLink /> 90 + <Metrics metrics={metrics.data} period={period} showErrorLink /> 88 91 <Separator className="my-8" /> 89 92 <CombinedChartWrapper 90 - data={data} 93 + data={data.data} 91 94 period={period} 92 95 quantile={quantile} 93 96 interval={interval} 94 97 regions={regions.length ? (regions as Region[]) : monitor.regions} // FIXME: not properly reseted after filtered 95 98 monitor={monitor} 96 99 isQuantileDisabled={isQuantileDisabled} 97 - metricsByRegion={metricsByRegion} 100 + metricsByRegion={metricsByRegion.data} 98 101 preferredSettings={preferredSettings} 99 102 /> 100 103 </div>
-48
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/_components/refresh-widget.tsx
··· 1 - "use client"; 2 - 3 - import { useRouter } from "next/navigation"; 4 - import { useEffect, useState } from "react"; 5 - 6 - import { Button } from "@openstatus/ui/src/components/button"; 7 - 8 - import { api } from "@/trpc/client"; 9 - 10 - // TODO: instead of setInterval, use staleWhileRevalidate method to fetch latest data 11 - // also fetch data on page focus 12 - 13 - export function RefreshWidget({ defaultValue }: { defaultValue?: number }) { 14 - const [refresh, setRefresh] = useState(false); 15 - const [value, setValue] = useState(defaultValue); 16 - const router = useRouter(); 17 - 18 - useEffect(() => { 19 - const intervalId = setInterval(async () => { 20 - if (refresh) return; 21 - const data = await api.tinybird.lastCronTimestamp.query(); 22 - if (data && data?.length > 0) { 23 - const { cronTimestamp } = data[0]; 24 - setValue(cronTimestamp); 25 - if (value && cronTimestamp > value) setRefresh(true); 26 - } 27 - }, 30_000); 28 - return () => { 29 - clearInterval(intervalId); 30 - }; 31 - }, [refresh, value]); 32 - 33 - if (!refresh) return null; 34 - 35 - return ( 36 - <div> 37 - <Button 38 - onClick={() => { 39 - router.refresh(); 40 - setRefresh(false); 41 - }} 42 - variant="outline" 43 - > 44 - Refresh 45 - </Button> 46 - </div> 47 - ); 48 - }
+2 -6
apps/web/src/app/play/checker/_components/checker-form.tsx
··· 7 7 import * as z from "zod"; 8 8 9 9 import { 10 - Alert, 11 - AlertDescription, 12 - AlertTitle, 13 10 Button, 14 11 Checkbox, 15 12 Form, ··· 27 24 SelectValue, 28 25 Table, 29 26 TableBody, 30 - TableCaption, 31 27 TableCell, 32 28 TableHead, 33 29 TableHeader, ··· 253 249 )} 254 250 /> 255 251 <div className="col-span-full mt-2 sm:col-span-1"> 256 - <Button disabled={isPending} className="h-10 w-full group"> 252 + <Button disabled={isPending} className="group h-10 w-full"> 257 253 {isPending ? ( 258 254 <LoadingAnimation /> 259 255 ) : ( 260 256 <> 261 257 Check{" "} 262 - <Gauge className="ml-1 h-4 w-4 [&>*:first-child]:transition-transform [&>*:first-child]:origin-[12px_14px] [&>*:first-child]:-rotate-90 [&>*:first-child]:group-hover:rotate-0 [&>*:first-child]:duration-500 [&>*:first-child]:ease-out" /> 258 + <Gauge className="[&>*:first-child]:-rotate-90 ml-1 h-4 w-4 [&>*:first-child]:origin-[12px_14px] [&>*:first-child]:transition-transform [&>*:first-child]:duration-500 [&>*:first-child]:ease-out [&>*:first-child]:group-hover:rotate-0" /> 263 259 </> 264 260 )} 265 261 </Button>
+1 -1
apps/web/src/app/play/checker/api/mock.ts
··· 1 1 import type { RegionChecker } from "@/components/ping-response-analysis/utils"; 2 2 import { wait } from "@/lib/utils"; 3 - import type { Region } from "@openstatus/tinybird"; 3 + import type { Region } from "@openstatus/db/src/schema/constants"; 4 4 5 5 export async function mockCheckRegion(region: Region) { 6 6 const response = data.checks.find((check) => check.region === region);
+5 -14
apps/web/src/app/play/status/_components/status-play.tsx
··· 1 - import { OSTinybird } from "@openstatus/tinybird"; 2 - 3 1 import { 4 2 CardContainer, 5 3 CardDescription, ··· 9 7 } from "@/components/marketing/card"; 10 8 import { Tracker } from "@/components/tracker/tracker"; 11 9 import { env } from "@/env"; 10 + import { prepareStatusByPeriod } from "@/lib/tb"; 12 11 import { getServerTimezoneFormat } from "@/lib/timezone"; 13 - 14 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 15 12 16 13 export default async function StatusPlay() { 17 - const data = await tb.endpointStatusPeriod("45d")( 18 - { 19 - monitorId: "1", 20 - }, 21 - { 22 - revalidate: 600, // 10 minutes 23 - }, 24 - ); 25 - 14 + const res = await prepareStatusByPeriod("45d").getData({ monitorId: "1" }); 26 15 const formattedServerDate = getServerTimezoneFormat(); 27 16 28 17 return ( ··· 37 26 </CardHeader> 38 27 <div className="relative grid gap-4"> 39 28 <div className="mx-auto w-full max-w-md"> 40 - {data && <Tracker data={data} name="Ping" description="Pong" />} 29 + {res.data && ( 30 + <Tracker data={res.data} name="Ping" description="Pong" /> 31 + )} 41 32 </div> 42 33 <p className="text-center text-muted-foreground text-sm"> 43 34 {formattedServerDate}
+15 -12
apps/web/src/app/public/monitors/[id]/page.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 import * as React from "react"; 3 3 4 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 5 - import type { Region } from "@openstatus/tinybird"; 6 - import { OSTinybird } from "@openstatus/tinybird"; 4 + import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 7 5 import { Separator } from "@openstatus/ui"; 8 6 9 7 import { Shell } from "@/components/dashboard/shell"; ··· 11 9 import { ButtonReset } from "@/components/monitor-dashboard/button-reset"; 12 10 import { DatePickerPreset } from "@/components/monitor-dashboard/date-picker-preset"; 13 11 import { Metrics } from "@/components/monitor-dashboard/metrics"; 14 - import { env } from "@/env"; 15 12 import { getMinutesByInterval } from "@/lib/monitor/utils"; 16 13 import { getPreferredSettings } from "@/lib/preferred-settings/server"; 14 + import { 15 + prepareMetricByIntervalByPeriod, 16 + prepareMetricByRegionByPeriod, 17 + prepareMetricsByPeriod, 18 + } from "@/lib/tb"; 17 19 import { api } from "@/trpc/server"; 18 20 import { 19 21 DEFAULT_INTERVAL, ··· 23 25 searchParamsCache, 24 26 } from "./search-params"; 25 27 26 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 27 - 28 28 export default async function Page({ 29 29 params, 30 30 searchParams, ··· 43 43 if (!monitor) return notFound(); 44 44 45 45 const { period, quantile, interval, regions } = search; 46 + const type = monitor.jobType as "http" | "tcp"; 46 47 47 48 // TODO: work it out easier 48 49 const intervalMinutes = getMinutesByInterval(interval); ··· 52 53 const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 53 54 54 55 const [metrics, data, metricsByRegion] = await Promise.all([ 55 - tb.endpointMetrics(period)({ monitorId: id }), 56 - await tb.endpointChart(period)({ 56 + prepareMetricsByPeriod(period, type).getData({ 57 + monitorId: id, 58 + }), 59 + prepareMetricByIntervalByPeriod(period, type).getData({ 57 60 monitorId: id, 58 61 interval: minutes, 59 62 }), 60 - tb.endpointMetricsByRegion(period)({ 63 + prepareMetricByRegionByPeriod(period, type).getData({ 61 64 monitorId: id, 62 65 }), 63 66 ]); ··· 90 93 </div> 91 94 </Shell> 92 95 <Shell className="grid gap-4"> 93 - <Metrics metrics={metrics} period={period} /> 96 + <Metrics metrics={metrics.data} period={period} /> 94 97 <Separator className="my-8" /> 95 98 <CombinedChartWrapper 96 - data={data} 99 + data={data.data} 97 100 period={period} 98 101 quantile={quantile} 99 102 interval={interval} 100 103 regions={regions.length ? (regions as Region[]) : monitor.regions} // FIXME: not properly reseted after filtered 101 104 monitor={monitor} 102 105 isQuantileDisabled={isQuantileDisabled} 103 - metricsByRegion={metricsByRegion} 106 + metricsByRegion={metricsByRegion.data} 104 107 preferredSettings={preferredSettings} 105 108 /> 106 109 </Shell>
+15 -12
apps/web/src/app/status-page/[domain]/monitors/[id]/page.tsx
··· 1 1 import { notFound } from "next/navigation"; 2 2 import * as React from "react"; 3 3 4 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 5 - import type { Region } from "@openstatus/tinybird"; 6 - import { OSTinybird } from "@openstatus/tinybird"; 4 + import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 7 5 import { Separator } from "@openstatus/ui/src/components/separator"; 8 6 9 7 import { Header } from "@/components/dashboard/header"; ··· 11 9 import { ButtonReset } from "@/components/monitor-dashboard/button-reset"; 12 10 import { DatePickerPreset } from "@/components/monitor-dashboard/date-picker-preset"; 13 11 import { Metrics } from "@/components/monitor-dashboard/metrics"; 14 - import { env } from "@/env"; 15 12 import { getMinutesByInterval } from "@/lib/monitor/utils"; 16 13 import { getPreferredSettings } from "@/lib/preferred-settings/server"; 14 + import { 15 + prepareMetricByIntervalByPeriod, 16 + prepareMetricByRegionByPeriod, 17 + prepareMetricsByPeriod, 18 + } from "@/lib/tb"; 17 19 import { api } from "@/trpc/server"; 18 20 import { 19 21 DEFAULT_INTERVAL, ··· 22 24 periods, 23 25 searchParamsCache, 24 26 } from "./search-params"; 25 - 26 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 27 27 28 28 export const revalidate = 120; 29 29 ··· 46 46 if (!monitor) return notFound(); 47 47 48 48 const { period, quantile, interval, regions } = search; 49 + const type = monitor.jobType as "http" | "tcp"; 49 50 50 51 // TODO: work it out easier 51 52 const intervalMinutes = getMinutesByInterval(interval); ··· 55 56 const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 56 57 57 58 const [metrics, data, metricsByRegion] = await Promise.all([ 58 - tb.endpointMetrics(period)({ monitorId: id }), 59 - tb.endpointChart(period)({ 59 + prepareMetricsByPeriod(period, type).getData({ monitorId: id }), 60 + prepareMetricByIntervalByPeriod(period, type).getData({ 60 61 monitorId: id, 61 62 interval: minutes, 62 63 }), 63 - tb.endpointMetricsByRegion(period)({ 64 + prepareMetricByRegionByPeriod(period, type).getData({ 64 65 monitorId: id, 65 66 }), 66 67 ]); ··· 72 73 quantile !== DEFAULT_QUANTILE || 73 74 interval !== DEFAULT_INTERVAL || 74 75 flyRegions.length !== regions.length; 76 + 77 + console.log({ metrics: metrics.data }); 75 78 76 79 return ( 77 80 <div className="relative flex w-full flex-col gap-6"> ··· 80 83 <DatePickerPreset defaultValue={period} values={periods} /> 81 84 {isDirty ? <ButtonReset /> : null} 82 85 </div> 83 - <Metrics metrics={metrics} period={period} /> 86 + <Metrics metrics={metrics.data} period={period} /> 84 87 <Separator className="my-8" /> 85 88 <CombinedChartWrapper 86 - data={data} 89 + data={data.data} 87 90 period={period} 88 91 quantile={quantile} 89 92 interval={interval} 90 93 regions={regions.length ? (regions as Region[]) : monitor.regions} // FIXME: not properly reseted after filtered 91 94 monitor={monitor} 92 95 isQuantileDisabled={isQuantileDisabled} 93 - metricsByRegion={metricsByRegion} 96 + metricsByRegion={metricsByRegion.data} 94 97 preferredSettings={preferredSettings} 95 98 /> 96 99 </div>
+1 -1
apps/web/src/app/status-page/[domain]/monitors/[id]/search-params.ts
··· 11 11 export const DEFAULT_INTERVAL = "30m"; 12 12 export const DEFAULT_PERIOD = "1d"; 13 13 14 - export const periods = ["1d", "7d"] as const; 14 + export const periods = ["1d", "7d", "14d"] as const; 15 15 16 16 export const searchParamsParsers = { 17 17 statusCode: parseAsInteger,
+8 -11
apps/web/src/app/status-page/[domain]/monitors/page.tsx
··· 2 2 import Link from "next/link"; 3 3 import { notFound } from "next/navigation"; 4 4 5 - import { OSTinybird } from "@openstatus/tinybird"; 6 5 import { Button } from "@openstatus/ui/src/components/button"; 7 6 8 7 import { EmptyState } from "@/components/dashboard/empty-state"; 9 8 import { Header } from "@/components/dashboard/header"; 10 9 import { SimpleChart } from "@/components/monitor-charts/simple-chart"; 11 10 import { groupDataByTimestamp } from "@/components/monitor-charts/utils"; 12 - import { env } from "@/env"; 11 + import { prepareMetricByIntervalByPeriod } from "@/lib/tb"; 13 12 import { api } from "@/trpc/server"; 14 13 import { searchParamsCache } from "./search-params"; 15 14 16 15 // Add loading page 17 - 18 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 19 16 20 17 export const revalidate = 120; 21 18 ··· 39 36 publicMonitors.length > 0 40 37 ? await Promise.all( 41 38 publicMonitors?.map(async (monitor) => { 42 - const data = await tb.endpointChartAllRegions(period)({ 39 + const type = monitor.jobType as "http" | "tcp"; 40 + const data = await prepareMetricByIntervalByPeriod( 41 + period, 42 + type, 43 + ).getData({ 43 44 monitorId: String(monitor.id), 45 + interval: 60, 44 46 }); 45 47 46 48 return { monitor, data }; ··· 70 72 <ul className="grid gap-6"> 71 73 {monitorsWithData?.map(({ monitor, data }) => { 72 74 const group = 73 - data && 74 - groupDataByTimestamp( 75 - data.map((data) => ({ ...data, region: "ams" })), 76 - period, 77 - quantile, 78 - ); 75 + data.data && groupDataByTimestamp(data.data, period, quantile); 79 76 return ( 80 77 <li key={monitor.id} className="grid gap-2"> 81 78 <div className="flex w-full min-w-0 items-center justify-between gap-3">
+1 -1
apps/web/src/components/billing/pro-banner.tsx
··· 52 52 variant="ghost" 53 53 size="icon" 54 54 onClick={onClick} 55 - className="w-7 h-7" 55 + className="h-7 w-7" 56 56 > 57 57 <X className="h-4 w-4" /> 58 58 </Button>
+1 -1
apps/web/src/components/billing/pro-feature-hover-card.tsx
··· 37 37 <HoverCard openDelay={0} open={open} onOpenChange={setOpen}> 38 38 <HoverCardTrigger 39 39 onClick={() => setOpen(true)} 40 - className="opacity-70 cursor-not-allowed relative" 40 + className="relative cursor-not-allowed opacity-70" 41 41 asChild 42 42 > 43 43 {children}
+22 -27
apps/web/src/components/data-table/columns.tsx
··· 2 2 3 3 import type { ColumnDef } from "@tanstack/react-table"; 4 4 import { format } from "date-fns"; 5 - import * as z from "zod"; 5 + import type * as z from "zod"; 6 6 7 - import type { Ping } from "@openstatus/tinybird"; 8 - import { 9 - Tooltip, 10 - TooltipContent, 11 - TooltipProvider, 12 - TooltipTrigger, 13 - } from "@openstatus/ui"; 14 7 import { flyRegionsDict } from "@openstatus/utils"; 15 8 16 9 import type { Trigger } from "@/lib/monitor/utils"; 10 + import type { monitorFlyRegionSchema } from "@openstatus/db/src/schema/constants"; 17 11 import { TriggerIconWithTooltip } from "../monitor/trigger-icon-with-tooltip"; 18 12 import { DataTableColumnHeader } from "./data-table-column-header"; 19 13 import { DataTableStatusBadge } from "./data-table-status-badge"; 20 14 21 - export const columns: ColumnDef<Ping>[] = [ 15 + export type Check = { 16 + type: "http" | "tcp"; 17 + monitorId: string; 18 + latency: number; 19 + region: z.infer<typeof monitorFlyRegionSchema>; 20 + statusCode?: number | null; 21 + timestamp: number; 22 + workspaceId: string; 23 + cronTimestamp: number | null; 24 + error: boolean; 25 + trigger: Trigger | null; 26 + }; 27 + 28 + export const columns: ColumnDef<Check>[] = [ 22 29 { 23 30 accessorKey: "error", 24 31 header: () => null, ··· 48 55 <DataTableColumnHeader column={column} title="Status" /> 49 56 ), 50 57 cell: ({ row }) => { 51 - const unsafe_StatusCode = row.getValue("statusCode"); 52 - const statusCode = z.number().nullable().parse(unsafe_StatusCode); 53 - const message = row.original.message; 58 + const statusCode = row.getValue("statusCode") as 59 + | number 60 + | null 61 + | undefined; 54 62 55 - if (statusCode !== null) { 63 + if (statusCode) { 56 64 return <DataTableStatusBadge {...{ statusCode }} />; 57 65 } 58 66 59 - return ( 60 - <TooltipProvider> 61 - <Tooltip> 62 - <TooltipTrigger> 63 - <DataTableStatusBadge {...{ statusCode }} /> 64 - </TooltipTrigger> 65 - <TooltipContent> 66 - <p className="max-w-xs text-muted-foreground sm:max-w-sm"> 67 - {message} 68 - </p> 69 - </TooltipContent> 70 - </Tooltip> 71 - </TooltipProvider> 72 - ); 67 + return <div className="text-muted-foreground">N/A</div>; 73 68 }, 74 69 filterFn: (row, id, value) => { 75 70 // get the first digit of the status code
+8 -2
apps/web/src/components/data-table/data-table-row-action.tsx
··· 4 4 import { MoreHorizontal } from "lucide-react"; 5 5 import Link from "next/link"; 6 6 7 - import { tbBuildResponseList } from "@openstatus/tinybird"; 8 7 import { 9 8 Button, 10 9 DropdownMenu, ··· 12 11 DropdownMenuItem, 13 12 DropdownMenuTrigger, 14 13 } from "@openstatus/ui"; 14 + import { z } from "zod"; 15 15 16 16 interface DataTableRowActionsProps<TData> { 17 17 row: Row<TData>; ··· 21 21 row, 22 22 }: DataTableRowActionsProps<TData>) { 23 23 // FIXME: DRY - this is a duplicate of the OSTinybird endpoint 24 - const ping = tbBuildResponseList.parse(row.original); 24 + const ping = z 25 + .object({ 26 + monitorId: z.string(), 27 + cronTimestamp: z.number(), 28 + region: z.string(), 29 + }) 30 + .parse(row.original); 25 31 return ( 26 32 <DropdownMenu> 27 33 <DropdownMenuTrigger asChild>
+2 -3
apps/web/src/components/data-table/data-table-status-badge.tsx
··· 1 - import type { Ping } from "@openstatus/tinybird"; 2 1 import { Badge } from "@openstatus/ui/src/components/badge"; 3 2 4 - import { StatusCodeBadge } from "../monitor/status-code-badge"; 3 + import { StatusCodeBadge } from "@/components/monitor/status-code-badge"; 5 4 6 5 export function DataTableStatusBadge({ 7 6 statusCode, 8 7 }: { 9 - statusCode: Ping["statusCode"]; 8 + statusCode?: number | null; 10 9 }) { 11 10 if (!statusCode) { 12 11 return <Badge variant="destructive">Error</Badge>;
+1 -1
apps/web/src/components/data-table/data-table-toolbar.tsx
··· 27 27 return ( 28 28 <div className="flex flex-wrap items-center justify-between gap-3"> 29 29 <div className="flex flex-1 flex-wrap items-center gap-2"> 30 - {table.getColumn("statusCode") && ( 30 + {table.getColumn("statusCode")?.getIsVisible() && ( 31 31 <DataTableFacetedFilter 32 32 column={table.getColumn("statusCode")} 33 33 title="Status Code"
+11 -1
apps/web/src/components/data-table/data-table.tsx
··· 7 7 PaginationState, 8 8 Row, 9 9 SortingState, 10 + VisibilityState, 10 11 } from "@tanstack/react-table"; 11 12 import { 12 13 flexRender, ··· 28 29 TableRow, 29 30 } from "@openstatus/ui"; 30 31 32 + import { cn } from "@/lib/utils"; 31 33 import { DataTablePagination } from "./data-table-pagination"; 32 34 import { DataTableToolbar } from "./data-table-toolbar"; 33 35 ··· 39 41 autoResetExpanded?: boolean; 40 42 defaultColumnFilters?: ColumnFiltersState; 41 43 defaultPagination?: PaginationState; 44 + defaultVisibility?: VisibilityState; 45 + // allowedPeriods?: Period[]; REMIDNER: disabled unallowed periods 42 46 } 43 47 44 48 export function DataTable<TData, TValue>({ ··· 49 53 autoResetExpanded, 50 54 defaultColumnFilters = [], 51 55 defaultPagination = { pageIndex: 0, pageSize: 10 }, 56 + defaultVisibility = {}, 52 57 }: DataTableProps<TData, TValue>) { 53 58 const [sorting, setSorting] = React.useState<SortingState>([]); 54 59 const [columnFilters, setColumnFilters] = ··· 56 61 const [expanded, setExpanded] = React.useState<ExpandedState>({}); 57 62 const [pagination, setPagination] = 58 63 React.useState<PaginationState>(defaultPagination); 64 + const [columnVisibility, setColumnVisibility] = 65 + React.useState<VisibilityState>(defaultVisibility); 59 66 60 67 const table = useReactTable({ 61 68 data, ··· 69 76 getFilteredRowModel: getFilteredRowModel(), 70 77 onExpandedChange: setExpanded, 71 78 getExpandedRowModel: getExpandedRowModel(), 79 + onColumnVisibilityChange: setColumnVisibility, 72 80 getRowCanExpand, 73 81 autoResetExpanded, 74 82 state: { ··· 76 84 columnFilters, 77 85 expanded, 78 86 pagination, 87 + columnVisibility, 79 88 }, 80 89 }); 81 90 ··· 111 120 (row.getIsSelected() || row.getIsExpanded()) && "selected" 112 121 } 113 122 onClick={() => { 123 + if (!row.getCanExpand()) return; 114 124 // REMINDER: this is a workaround for single row expansion 115 125 if (!row.getIsExpanded()) table.resetExpanded(); 116 126 row.toggleExpanded(); 117 127 }} 118 - className="cursor-pointer" 128 + className={cn(row.getCanExpand() && "cursor-pointer")} 119 129 > 120 130 {row.getVisibleCells().map((cell) => ( 121 131 <TableCell key={cell.id}>
+4 -5
apps/web/src/components/data-table/monitor/columns.tsx
··· 10 10 Monitor, 11 11 MonitorTag, 12 12 } from "@openstatus/db/src/schema"; 13 - import type { 14 - Monitor as MonitorTracker, 15 - ResponseTimeMetrics, 16 - } from "@openstatus/tinybird"; 17 13 import { Tracker } from "@openstatus/tracker"; 18 14 import { 19 15 Badge, ··· 29 25 import { Bar } from "@/components/tracker/tracker"; 30 26 import { isActiveMaintenance } from "@/lib/maintenances/utils"; 31 27 28 + import type { ResponseStatusTracker, ResponseTimeMetrics } from "@/lib/tb"; 32 29 import { Eye, EyeOff, Radio, View } from "lucide-react"; 33 30 import type { ReactNode } from "react"; 34 31 import { DataTableColumnHeader } from "./data-table-column-header"; 35 32 import { DataTableRowActions } from "./data-table-row-actions"; 33 + 34 + // EXAMPLE: get the type of the response of the endpoint 36 35 37 36 export const columns: ColumnDef<{ 38 37 monitor: Monitor; 39 38 metrics?: ResponseTimeMetrics; 40 - data?: MonitorTracker[]; 39 + data?: ResponseStatusTracker[]; 41 40 incidents?: Incident[]; 42 41 maintenances?: Maintenance[]; 43 42 tags?: MonitorTag[];
-83
apps/web/src/components/data-table/rum/columns.tsx
··· 1 - "use client"; 2 - 3 - import type { ColumnDef } from "@tanstack/react-table"; 4 - import Link from "next/link"; 5 - 6 - import type { responseRumPageQuery } from "@openstatus/tinybird/src/validation"; 7 - import type { z } from "zod"; 8 - 9 - export const columns: ColumnDef<z.infer<typeof responseRumPageQuery>>[] = [ 10 - { 11 - accessorKey: "path", 12 - header: "Page", 13 - cell: ({ row }) => { 14 - return ( 15 - <Link 16 - href={`./rum/overview?path=${encodeURIComponent(row.original.path)}`} 17 - className="w-8 max-w-8 hover:underline" 18 - > 19 - <span className="truncate">{row.getValue("path")}</span> 20 - </Link> 21 - ); 22 - }, 23 - }, 24 - { 25 - accessorKey: "totalSession", 26 - header: "Total Session", 27 - cell: ({ row }) => { 28 - return <>{row.original.totalSession}</>; 29 - }, 30 - }, 31 - { 32 - accessorKey: "cls", 33 - header: "CLS", 34 - cell: ({ row }) => { 35 - return ( 36 - <code>{row.original.cls ? row.original.cls.toFixed(2) : "-"}</code> 37 - ); 38 - }, 39 - }, 40 - { 41 - accessorKey: "fcp", 42 - header: "FCP", 43 - cell: ({ row }) => { 44 - return ( 45 - <code>{row.original.fcp ? row.original.fcp.toFixed(0) : "-"} </code> 46 - ); 47 - }, 48 - }, 49 - { 50 - accessorKey: "inp", 51 - header: "INP", 52 - cell: ({ row }) => { 53 - return ( 54 - <code>{row.original.inp ? row.original.inp.toFixed(0) : "-"}</code> 55 - ); 56 - }, 57 - }, 58 - { 59 - accessorKey: "lcp", 60 - header: "LCP", 61 - cell: ({ row }) => { 62 - return ( 63 - <code>{row.original.lcp ? row.original.lcp.toFixed(0) : "-"}</code> 64 - ); 65 - }, 66 - }, 67 - { 68 - accessorKey: "ttfb", 69 - header: "TTFB", 70 - cell: ({ row }) => { 71 - return ( 72 - <code>{row.original.ttfb ? row.original.ttfb.toFixed(0) : "-"}</code> 73 - ); 74 - }, 75 - }, 76 - // { 77 - // accessorKey: "updatedAt", 78 - // header: "Last Updated", 79 - // cell: ({ row }) => { 80 - // return <span>{formatDate(row.getValue("updatedAt"))}</span>; 81 - // }, 82 - // }, 83 - ];
-81
apps/web/src/components/data-table/rum/data-table.tsx
··· 1 - "use client"; 2 - 3 - import type { ColumnDef } from "@tanstack/react-table"; 4 - import { 5 - flexRender, 6 - getCoreRowModel, 7 - useReactTable, 8 - } from "@tanstack/react-table"; 9 - import * as React from "react"; 10 - 11 - import { 12 - Table, 13 - TableBody, 14 - TableCell, 15 - TableHead, 16 - TableHeader, 17 - TableRow, 18 - } from "@openstatus/ui"; 19 - 20 - interface DataTableProps<TData, TValue> { 21 - columns: ColumnDef<TData, TValue>[]; 22 - data: TData[]; 23 - } 24 - 25 - export function DataTable<TData, TValue>({ 26 - columns, 27 - data, 28 - }: DataTableProps<TData, TValue>) { 29 - const table = useReactTable({ 30 - data, 31 - columns, 32 - getCoreRowModel: getCoreRowModel(), 33 - }); 34 - 35 - return ( 36 - <div className="rounded-md border"> 37 - <Table> 38 - <TableHeader className="bg-muted/50"> 39 - {table.getHeaderGroups().map((headerGroup) => ( 40 - <TableRow key={headerGroup.id} className="hover:bg-transparent"> 41 - {headerGroup.headers.map((header) => { 42 - return ( 43 - <TableHead key={header.id}> 44 - {header.isPlaceholder 45 - ? null 46 - : flexRender( 47 - header.column.columnDef.header, 48 - header.getContext(), 49 - )} 50 - </TableHead> 51 - ); 52 - })} 53 - </TableRow> 54 - ))} 55 - </TableHeader> 56 - <TableBody> 57 - {table.getRowModel().rows?.length ? ( 58 - table.getRowModel().rows.map((row) => ( 59 - <TableRow 60 - key={row.id} 61 - data-state={row.getIsSelected() && "selected"} 62 - > 63 - {row.getVisibleCells().map((cell) => ( 64 - <TableCell key={cell.id}> 65 - {flexRender(cell.column.columnDef.cell, cell.getContext())} 66 - </TableCell> 67 - ))} 68 - </TableRow> 69 - )) 70 - ) : ( 71 - <TableRow> 72 - <TableCell colSpan={columns.length} className="h-24 text-center"> 73 - No results. 74 - </TableCell> 75 - </TableRow> 76 - )} 77 - </TableBody> 78 - </Table> 79 - </div> 80 - ); 81 - }
-69
apps/web/src/components/data-table/session/columns.tsx
··· 1 - "use client"; 2 - 3 - import type { ColumnDef } from "@tanstack/react-table"; 4 - import Link from "next/link"; 5 - 6 - import type { sessionRumPageQuery } from "@openstatus/tinybird/src/validation"; 7 - import type { z } from "zod"; 8 - 9 - export const columns: ColumnDef<z.infer<typeof sessionRumPageQuery>>[] = [ 10 - { 11 - accessorKey: "session", 12 - header: "Session", 13 - cell: ({ row }) => { 14 - return <>{row.original.session_id}</>; 15 - }, 16 - }, 17 - { 18 - accessorKey: "cls", 19 - header: "CLS", 20 - cell: ({ row }) => { 21 - return ( 22 - <code>{row.original.cls ? row.original.cls.toFixed(2) : "-"}</code> 23 - ); 24 - }, 25 - }, 26 - { 27 - accessorKey: "fcp", 28 - header: "FCP", 29 - cell: ({ row }) => { 30 - return ( 31 - <code>{row.original.fcp ? row.original.fcp.toFixed(0) : "-"} </code> 32 - ); 33 - }, 34 - }, 35 - { 36 - accessorKey: "inp", 37 - header: "INP", 38 - cell: ({ row }) => { 39 - return ( 40 - <code>{row.original.inp ? row.original.inp.toFixed(0) : "-"}</code> 41 - ); 42 - }, 43 - }, 44 - { 45 - accessorKey: "lcp", 46 - header: "LCP", 47 - cell: ({ row }) => { 48 - return ( 49 - <code>{row.original.lcp ? row.original.lcp.toFixed(0) : "-"}</code> 50 - ); 51 - }, 52 - }, 53 - { 54 - accessorKey: "ttfb", 55 - header: "TTFB", 56 - cell: ({ row }) => { 57 - return ( 58 - <code>{row.original.ttfb ? row.original.ttfb.toFixed(0) : "-"}</code> 59 - ); 60 - }, 61 - }, 62 - // { 63 - // accessorKey: "updatedAt", 64 - // header: "Last Updated", 65 - // cell: ({ row }) => { 66 - // return <span>{formatDate(row.getValue("updatedAt"))}</span>; 67 - // }, 68 - // }, 69 - ];
-81
apps/web/src/components/data-table/session/data-table.tsx
··· 1 - "use client"; 2 - 3 - import type { ColumnDef } from "@tanstack/react-table"; 4 - import { 5 - flexRender, 6 - getCoreRowModel, 7 - useReactTable, 8 - } from "@tanstack/react-table"; 9 - import * as React from "react"; 10 - 11 - import { 12 - Table, 13 - TableBody, 14 - TableCell, 15 - TableHead, 16 - TableHeader, 17 - TableRow, 18 - } from "@openstatus/ui"; 19 - 20 - interface DataTableProps<TData, TValue> { 21 - columns: ColumnDef<TData, TValue>[]; 22 - data: TData[]; 23 - } 24 - 25 - export function DataTable<TData, TValue>({ 26 - columns, 27 - data, 28 - }: DataTableProps<TData, TValue>) { 29 - const table = useReactTable({ 30 - data, 31 - columns, 32 - getCoreRowModel: getCoreRowModel(), 33 - }); 34 - 35 - return ( 36 - <div className="rounded-md border"> 37 - <Table> 38 - <TableHeader className="bg-muted/50"> 39 - {table.getHeaderGroups().map((headerGroup) => ( 40 - <TableRow key={headerGroup.id} className="hover:bg-transparent"> 41 - {headerGroup.headers.map((header) => { 42 - return ( 43 - <TableHead key={header.id}> 44 - {header.isPlaceholder 45 - ? null 46 - : flexRender( 47 - header.column.columnDef.header, 48 - header.getContext(), 49 - )} 50 - </TableHead> 51 - ); 52 - })} 53 - </TableRow> 54 - ))} 55 - </TableHeader> 56 - <TableBody> 57 - {table.getRowModel().rows?.length ? ( 58 - table.getRowModel().rows.map((row) => ( 59 - <TableRow 60 - key={row.id} 61 - data-state={row.getIsSelected() && "selected"} 62 - > 63 - {row.getVisibleCells().map((cell) => ( 64 - <TableCell key={cell.id}> 65 - {flexRender(cell.column.columnDef.cell, cell.getContext())} 66 - </TableCell> 67 - ))} 68 - </TableRow> 69 - )) 70 - ) : ( 71 - <TableRow> 72 - <TableCell colSpan={columns.length} className="h-24 text-center"> 73 - No results. 74 - </TableCell> 75 - </TableRow> 76 - )} 77 - </TableBody> 78 - </Table> 79 - </div> 80 - ); 81 - }
+2 -1
apps/web/src/components/data-table/single-region/columns.tsx
··· 2 2 3 3 import { SimpleChart } from "@/components/monitor-charts/simple-chart"; 4 4 import { formatNumber } from "@/components/monitor-dashboard/metrics-card"; 5 - import type { Region, ResponseTimeMetricsByRegion } from "@openstatus/tinybird"; 5 + import type { ResponseTimeMetricsByRegion } from "@/lib/tb"; 6 + import type { Region } from "@openstatus/db/src/schema/constants"; 6 7 import { flyRegionsDict } from "@openstatus/utils"; 7 8 import type { ColumnDef } from "@tanstack/react-table"; 8 9 import { DataTableColumnHeader } from "./data-table-column-header";
+3 -3
apps/web/src/components/forms/monitor/form.tsx
··· 185 185 } = form.getValues(); 186 186 187 187 // FIXME: add support for TCP 188 - if (jobType !== "http") 189 - return { error: "Only HTTP tests are supported. Coming soon..." }; 188 + // if (jobType !== "http") 189 + // return { error: "Only HTTP tests are supported. Coming soon..." }; 190 190 191 191 if ( 192 192 body && ··· 201 201 } 202 202 } 203 203 204 - const res = await fetch("/api/checker/test", { 204 + const res = await fetch(`/api/checker/test/${jobType}`, { 205 205 method: "POST", 206 206 headers: new Headers({ 207 207 "Content-Type": "application/json",
+1 -2
apps/web/src/components/forms/monitor/select-region.tsx
··· 3 3 import { Check, ChevronsUpDown, Globe2 } from "lucide-react"; 4 4 import * as React from "react"; 5 5 6 - import type { Region } from "@openstatus/tinybird"; 7 6 import { Button, type ButtonProps } from "@openstatus/ui/src/components/button"; 8 7 import { 9 8 Command, ··· 26 25 } from "@openstatus/utils"; 27 26 28 27 import { cn } from "@/lib/utils"; 29 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 28 + import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 30 29 31 30 interface SelectRegionProps extends Omit<ButtonProps, "onChange"> { 32 31 allowedRegions: Region[];
+5 -5
apps/web/src/components/forms/status-page/section-advanced.tsx
··· 161 161 </FormItem> 162 162 )} 163 163 /> 164 - <div className="grid w-full gap-4 md:grid-rows-2 md:grid-cols-3 md:col-span-full"> 164 + <div className="grid w-full gap-4 md:col-span-full md:grid-cols-3 md:grid-rows-2"> 165 165 <SectionHeader 166 166 title="Monitor Values Visibility" 167 167 description={ ··· 177 177 } 178 178 className="md:col-span-2" 179 179 /> 180 - <div className="group md:row-span-2 flex flex-col justify-center gap-1 border border-dashed rounded-md p-3"> 181 - <div className="flex flex-row gap-2 items-center justify-center text-muted-foreground group-hover:text-foreground"> 180 + <div className="group flex flex-col justify-center gap-1 rounded-md border border-dashed p-3 md:row-span-2"> 181 + <div className="flex flex-row items-center justify-center gap-2 text-muted-foreground group-hover:text-foreground"> 182 182 <MousePointer2 className="h-3 w-3" /> 183 183 <p className="text-sm">Hover State</p> 184 184 </div> 185 - <div className="max-w-[15rem] mx-auto"> 185 + <div className="mx-auto max-w-[15rem]"> 186 186 <BarDescription 187 187 label="Operational" 188 188 day={new Date().toISOString()} ··· 190 190 ok={5569} 191 191 showValues={!!form.getValues("showMonitorValues")} 192 192 barClassName="bg-status-operational" 193 - className="md:col-span-1 bg-popover text-popover-foreground rounded-md border p-2 shadow-md" 193 + className="rounded-md border bg-popover p-2 text-popover-foreground shadow-md md:col-span-1" 194 194 /> 195 195 </div> 196 196 </div>
+3 -3
apps/web/src/components/layout/header/user-nav.tsx
··· 74 74 <DropdownMenuGroup> 75 75 <DropdownMenuSub> 76 76 {/* REMINDER: consider using that the data-state styles as default */} 77 - <DropdownMenuSubTrigger className="gap-1 [&_svg]:data-[state=open]:text-foreground [&_svg]:data-[highlighted]:text-foreground [&_svg]:text-muted-foreground"> 78 - <div className="w-full flex flex-row items-center justify-between"> 77 + <DropdownMenuSubTrigger className="gap-1 [&_svg]:text-muted-foreground [&_svg]:data-[highlighted]:text-foreground [&_svg]:data-[state=open]:text-foreground"> 78 + <div className="flex w-full flex-row items-center justify-between"> 79 79 <span>Switch theme</span> 80 80 <ThemeIcon theme={theme} /> 81 81 </div> ··· 88 88 key={option} 89 89 checked={theme === option} 90 90 onClick={() => setTheme(option)} 91 - className="capitalize justify-between [&_svg]:data-[state=open]:text-foreground [&_svg]:data-[highlighted]:text-foreground [&_svg]:text-muted-foreground" 91 + className="justify-between capitalize [&_svg]:text-muted-foreground [&_svg]:data-[highlighted]:text-foreground [&_svg]:data-[state=open]:text-foreground" 92 92 > 93 93 {option} 94 94 <ThemeIcon theme={option} />
+1 -1
apps/web/src/components/marketing/pricing/pricing-slider.tsx
··· 58 58 max={MAX_REGIONS} 59 59 step={1} 60 60 trailing="regions" 61 - className="text-right font-mono bg-background" 61 + className="bg-background text-right font-mono" 62 62 value={inputValue} 63 63 onChange={(e) => 64 64 setInputValue(Number.parseInt(e.target.value) || 0)
+2 -2
apps/web/src/components/marketing/pricing/pricing-table.tsx
··· 123 123 return ( 124 124 <TableRow key={key + label}> 125 125 <TableCell> 126 - <div className="flex gap-2 items-center"> 126 + <div className="flex items-center gap-2"> 127 127 {label} 128 128 {badge ? ( 129 129 <Badge variant="secondary">{badge}</Badge> ··· 132 132 <TooltipProvider delayDuration={200}> 133 133 <Tooltip> 134 134 <TooltipTrigger className="ml-auto data-[state=closed]:text-muted-foreground"> 135 - <Info className="w-4 h-4" /> 135 + <Info className="h-4 w-4" /> 136 136 </TooltipTrigger> 137 137 <TooltipContent className="w-64"> 138 138 {description}
+2 -2
apps/web/src/components/marketing/speed-checker-button.tsx
··· 5 5 6 6 export function SpeedCheckerButton({ className, ...props }: ButtonProps) { 7 7 return ( 8 - <Button className={cn("rounded-full group", className)} asChild {...props}> 8 + <Button className={cn("group rounded-full", className)} asChild {...props}> 9 9 <Link href="/play/checker"> 10 10 Speed Checker{" "} 11 - <Icons.gauge className="ml-1 h-4 w-4 [&>*:first-child]:transition-transform [&>*:first-child]:origin-[12px_14px] [&>*:first-child]:-rotate-90 [&>*:first-child]:group-hover:rotate-0 [&>*:first-child]:duration-500 [&>*:first-child]:ease-out" /> 11 + <Icons.gauge className="[&>*:first-child]:-rotate-90 ml-1 h-4 w-4 [&>*:first-child]:origin-[12px_14px] [&>*:first-child]:transition-transform [&>*:first-child]:duration-500 [&>*:first-child]:ease-out [&>*:first-child]:group-hover:rotate-0" /> 12 12 </Link> 13 13 </Button> 14 14 );
+9 -6
apps/web/src/components/marketing/stats.tsx
··· 1 1 import { Shell } from "@/components/dashboard/shell"; 2 - import { getHomeStatsData } from "@/lib/tb"; 2 + import { env } from "@/env"; 3 3 import { numberFormatter } from "@/lib/utils"; 4 + import { OSTinybird } from "@openstatus/tinybird"; 5 + 6 + const tb = new OSTinybird(env.TINY_BIRD_API_KEY); 4 7 5 8 export async function Stats() { 6 - const tbTotalStats = await getHomeStatsData({}); 7 - const tbLastHourStats = await getHomeStatsData({ period: "1h" }); 9 + const tbTotalStats = await tb.homeStats({}); 10 + const tbLastHourStats = await tb.homeStats({ period: "1h" }); 8 11 // const totalActiveMonitors = await api.monitor.getTotalActiveMonitors.query(); 9 12 10 13 return ( ··· 12 15 <div className="grid grid-cols-1 gap-8 sm:grid-cols-3 sm:gap-16"> 13 16 <div className="text-center"> 14 17 <h3 className="font-cal text-3xl"> 15 - {numberFormatter(tbTotalStats?.[0].count || 0)} 18 + {numberFormatter(tbTotalStats?.data?.[0]?.count || 0)} 16 19 </h3> 17 20 <p className="font-light text-muted-foreground">Total pings</p> 18 21 </div> 19 22 <div className="text-center"> 20 23 <h3 className="font-cal text-3xl"> 21 - {numberFormatter(tbLastHourStats?.[0].count || 62000000)} 24 + {numberFormatter(tbLastHourStats?.data?.[0]?.count || 0)} 22 25 </h3> 23 26 <p className="font-light text-muted-foreground"> 24 27 Pings in the last hour ··· 27 30 <div className="text-center"> 28 31 <h3 className="font-cal text-3xl"> 29 32 {/* {numberFormatter(totalActiveMonitors)} */} 30 - 2400+ 33 + 3400+ 31 34 </h3> 32 35 <p className="font-light text-muted-foreground">Active monitors</p> 33 36 </div>
+6 -14
apps/web/src/components/marketing/status-page/tracker-example.tsx
··· 1 1 import Link from "next/link"; 2 2 import { Suspense } from "react"; 3 3 4 - import { OSTinybird } from "@openstatus/tinybird"; 5 4 import { Button } from "@openstatus/ui/src/components/button"; 6 5 7 6 import { Tracker } from "@/components/tracker/tracker"; 8 - import { env } from "@/env"; 9 - 10 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 7 + import { prepareStatusByPeriod } from "@/lib/tb"; 11 8 12 9 export async function TrackerExample() { 13 10 return ( ··· 29 26 } 30 27 31 28 async function ExampleTracker() { 32 - const data = await tb.endpointStatusPeriod("45d")( 33 - { 34 - monitorId: "1", 35 - }, 36 - { 37 - revalidate: 600, // 10 minutes 38 - }, 39 - ); 29 + const res = await prepareStatusByPeriod("45d").getData({ 30 + monitorId: "1", 31 + }); 40 32 41 - if (!data) return null; 42 - return <Tracker data={data} name="Ping" description="Pong" />; 33 + if (!res.data) return null; 34 + return <Tracker data={res.data} name="Ping" description="Pong" />; 43 35 }
+1 -2
apps/web/src/components/monitor-charts/chart-wrapper.tsx
··· 1 - import type { ResponseGraph } from "@openstatus/tinybird"; 2 - 3 1 import type { Period, Quantile } from "@/lib/monitor/utils"; 2 + import type { ResponseGraph } from "@/lib/tb"; 4 3 import { Chart } from "./chart"; 5 4 import { groupDataByTimestamp } from "./utils"; 6 5
+1 -2
apps/web/src/components/monitor-charts/chart.tsx
··· 4 4 import { LineChart } from "@tremor/react"; 5 5 import { useState } from "react"; 6 6 7 - import type { Region } from "@openstatus/tinybird"; 8 - 9 7 import { cn } from "@/lib/utils"; 8 + import type { Region } from "@openstatus/db/src/schema/constants"; 10 9 import { dataFormatter, regionFormatter } from "./utils"; 11 10 12 11 interface ChartProps {
+3 -5
apps/web/src/components/monitor-charts/combined-chart-wrapper.tsx
··· 4 4 import { useMemo } from "react"; 5 5 6 6 import type { Monitor, PublicMonitor } from "@openstatus/db/src/schema"; 7 - import type { 8 - Region, 9 - ResponseGraph, 10 - ResponseTimeMetricsByRegion, 11 - } from "@openstatus/tinybird"; 7 + 12 8 import { Toggle } from "@openstatus/ui"; 13 9 14 10 import { columns } from "@/components/data-table/single-region/columns"; ··· 19 15 import type { Interval, Period, Quantile } from "@/lib/monitor/utils"; 20 16 import { usePreferredSettings } from "@/lib/preferred-settings/client"; 21 17 import type { PreferredSettings } from "@/lib/preferred-settings/server"; 18 + import type { ResponseGraph, ResponseTimeMetricsByRegion } from "@/lib/tb"; 19 + import type { Region } from "@openstatus/db/src/schema/constants"; 22 20 import { Chart } from "./chart"; 23 21 import { groupDataByTimestamp } from "./utils"; 24 22
+2 -1
apps/web/src/components/monitor-charts/region-table.tsx
··· 1 - import type { Region, ResponseTimeMetricsByRegion } from "@openstatus/tinybird"; 2 1 import { 3 2 Table, 4 3 TableBody, ··· 11 10 import { flyRegionsDict } from "@openstatus/utils"; 12 11 13 12 import { formatNumber } from "@/components/monitor-dashboard/metrics-card"; 13 + import type { ResponseTimeMetricsByRegion } from "@/lib/tb"; 14 + import type { Region } from "@openstatus/db/src/schema/constants"; 14 15 import { SimpleChart } from "./simple-chart"; 15 16 16 17 export interface RegionTableProps {
+2 -2
apps/web/src/components/monitor-charts/simple-chart-wrapper.tsx
··· 1 - import type { Region, ResponseGraph } from "@openstatus/tinybird"; 2 - 3 1 import type { Period, Quantile } from "@/lib/monitor/utils"; 2 + import type { ResponseGraph } from "@/lib/tb"; 3 + import type { Region } from "@openstatus/db/src/schema/constants"; 4 4 import { SimpleChart } from "./simple-chart"; 5 5 import { groupDataByTimestamp } from "./utils"; 6 6
+2 -1
apps/web/src/components/monitor-charts/utils.tsx
··· 1 1 import { format } from "date-fns"; 2 2 3 - import type { Region, ResponseGraph } from "@openstatus/tinybird"; 4 3 import { flyRegionsDict } from "@openstatus/utils"; 5 4 6 5 import type { Period, Quantile } from "@/lib/monitor/utils"; 6 + import type { ResponseGraph } from "@/lib/tb"; 7 + import type { Region } from "@openstatus/db/src/schema/constants"; 7 8 8 9 /** 9 10 *
+2 -3
apps/web/src/components/monitor-dashboard/metrics.tsx
··· 1 1 import { formatDistanceToNowStrict } from "date-fns"; 2 2 import Link from "next/link"; 3 3 4 - import type { LatencyMetric, ResponseTimeMetrics } from "@openstatus/tinybird"; 5 - 6 4 import { periodFormatter } from "@/lib/monitor/utils"; 7 5 import type { Period } from "@/lib/monitor/utils"; 6 + import type { ResponseTimeMetrics } from "@/lib/tb"; 8 7 import { MetricsCard } from "./metrics-card"; 9 8 10 9 const metricsOrder = [ ··· 13 12 "p90Latency", 14 13 "p95Latency", 15 14 "p99Latency", 16 - ] satisfies LatencyMetric[]; 15 + ] as const; 17 16 18 17 export function Metrics({ 19 18 metrics,
+1 -2
apps/web/src/components/monitor-dashboard/region-preset.tsx
··· 3 3 import { Check, ChevronsUpDown, Globe2 } from "lucide-react"; 4 4 import * as React from "react"; 5 5 6 - import type { Region } from "@openstatus/tinybird"; 7 6 import { Button, type ButtonProps } from "@openstatus/ui/src/components/button"; 8 7 import { 9 8 Command, ··· 26 25 } from "@openstatus/utils"; 27 26 28 27 import { cn } from "@/lib/utils"; 29 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 28 + import { type Region, flyRegions } from "@openstatus/db/src/schema/constants"; 30 29 import { parseAsArrayOf, parseAsStringLiteral, useQueryState } from "nuqs"; 31 30 32 31 interface RegionsPresetProps extends ButtonProps {
+20 -9
apps/web/src/components/monitor-dashboard/response-details.tsx
··· 1 1 // TODO: move to `ping-response-analysis` 2 2 3 - import { OSTinybird } from "@openstatus/tinybird"; 4 - import type { ResponseDetailsParams } from "@openstatus/tinybird"; 5 - 6 3 import { 7 4 Tabs, 8 5 TabsContent, ··· 12 9 import { RegionInfo } from "@/components/ping-response-analysis/region-info"; 13 10 import { ResponseHeaderTable } from "@/components/ping-response-analysis/response-header-table"; 14 11 import { ResponseTimingTable } from "@/components/ping-response-analysis/response-timing-table"; 15 - import { env } from "@/env"; 12 + import { prepareGetByPeriod } from "@/lib/tb"; 13 + import type { Region } from "@openstatus/db/src/schema/constants"; 14 + 15 + interface ResponseDetailsProps { 16 + monitorId: string; 17 + url?: string | undefined; 18 + region?: Region; 19 + cronTimestamp?: number | undefined; 20 + type: "http" | "tcp"; 21 + } 16 22 17 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 23 + export async function ResponseDetails({ 24 + type, 25 + ...props 26 + }: ResponseDetailsProps) { 27 + // FIXME: this has to be dynamic 28 + const details = await prepareGetByPeriod("30d", type).getData(props); 18 29 19 - export async function ResponseDetails(props: ResponseDetailsParams) { 20 - const details = await tb.endpointResponseDetails("45d")(props); 30 + if (!details.data || details.data.length === 0) return null; 21 31 22 - if (!details || details?.length === 0) return null; 32 + const response = details.data[0]; 23 33 24 - const response = details[0]; 34 + // FIXME: return the proper infos regarding TCP - but there are non right now anyways 35 + if (response.type === "tcp") return null; 25 36 26 37 const { timing, headers, message, statusCode } = response; 27 38
+1 -1
apps/web/src/components/ping-response-analysis/multi-region-tabs.tsx
··· 2 2 3 3 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@openstatus/ui"; 4 4 5 - import type { Region } from "@openstatus/tinybird"; 5 + import type { Region } from "@openstatus/db/src/schema/constants"; 6 6 import type { Row } from "@tanstack/react-table"; 7 7 import { RegionsPreset } from "../monitor-dashboard/region-preset"; 8 8 import { columns } from "./columns";
+7 -7
apps/web/src/components/status-page/monitor.tsx
··· 6 6 PublicMonitor, 7 7 selectPublicStatusReportSchemaWithRelation, 8 8 } from "@openstatus/db/src/schema"; 9 - import { OSTinybird } from "@openstatus/tinybird"; 10 9 11 10 import { Tracker } from "@/components/tracker/tracker"; 12 - import { env } from "@/env"; 13 - 14 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 11 + import { prepareStatusByPeriod } from "@/lib/tb"; 15 12 16 13 export const Monitor = async ({ 17 14 monitor, ··· 26 23 maintenances: Maintenance[]; 27 24 showValues?: boolean; 28 25 }) => { 29 - const data = await tb.endpointStatusPeriod("45d")({ 26 + const res = await prepareStatusByPeriod( 27 + "45d", 28 + monitor.jobType as "http" | "tcp", 29 + ).getData({ 30 30 monitorId: String(monitor.id), 31 31 }); 32 32 33 33 // TODO: we could handle the `statusReports` here instead of passing it down to the tracker 34 34 35 - if (!data) return <div>Something went wrong</div>; 35 + if (!res.data) return <div>Something went wrong</div>; 36 36 37 37 return ( 38 38 <Tracker 39 - data={data} 39 + data={res.data} 40 40 reports={statusReports} 41 41 incidents={incidents} 42 42 maintenances={maintenances}
+2 -2
apps/web/src/components/tracker/tracker.tsx
··· 12 12 StatusReport, 13 13 StatusReportUpdate, 14 14 } from "@openstatus/db/src/schema"; 15 - import type { Monitor } from "@openstatus/tinybird"; 16 15 import { 17 16 Tracker as OSTracker, 18 17 classNames, ··· 26 25 TooltipTrigger, 27 26 } from "@openstatus/ui/src/components/tooltip"; 28 27 28 + import type { ResponseStatusTracker } from "@/lib/tb"; 29 29 import { cn } from "@/lib/utils"; 30 30 import { 31 31 HoverCard, ··· 57 57 }); 58 58 59 59 interface TrackerProps { 60 - data: Monitor[]; 60 + data: ResponseStatusTracker[]; 61 61 name: string; 62 62 description?: string; 63 63 reports?: (StatusReport & { statusReportUpdates: StatusReportUpdate[] })[];
+31 -3
apps/web/src/components/workspace/select-workspace.tsx
··· 1 1 "use client"; 2 2 3 - import { Check, ChevronsUpDown, Plus } from "lucide-react"; 3 + import { Check, ChevronsUpDown, Copy, CopyCheck, Plus } from "lucide-react"; 4 4 import Link from "next/link"; 5 5 import { usePathname } from "next/navigation"; 6 6 import * as React from "react"; ··· 17 17 } from "@openstatus/ui/src/components/dropdown-menu"; 18 18 import { Skeleton } from "@openstatus/ui/src/components/skeleton"; 19 19 20 + import { copyToClipboard } from "@/lib/utils"; 20 21 import { api } from "@/trpc/client"; 21 22 22 23 export function SelectWorkspace() { 23 24 const [workspaces, setWorkspaces] = React.useState<Workspace[]>([]); 24 25 const [active, setActive] = React.useState<string>(); 25 26 const pathname = usePathname(); 27 + const [hasCopied, setHasCopied] = React.useState(false); 28 + 29 + React.useEffect(() => { 30 + if (hasCopied) { 31 + setTimeout(() => { 32 + setHasCopied(false); 33 + }, 2000); 34 + } 35 + }, [hasCopied]); 26 36 27 37 React.useEffect(() => { 28 38 if (pathname?.split("/")?.[2] && workspaces.length > 0) { ··· 63 73 <DropdownMenuItem key={workspace.id} asChild> 64 74 <a 65 75 href={`/app/${workspace.slug}/monitors`} 66 - className="justify-between" 76 + className="group justify-between" 67 77 > 68 78 <span className="truncate">{workspace.slug}</span> 79 + <Button 80 + type="button" 81 + variant="ghost" 82 + size="icon" 83 + className="mx-0.5 hidden h-5 w-5 group-hover:block" 84 + onClick={(e) => { 85 + e.stopPropagation(); 86 + e.preventDefault(); 87 + copyToClipboard(workspace.slug); 88 + setHasCopied(true); 89 + }} 90 + > 91 + {!hasCopied ? ( 92 + <Copy className="h-3 w-3" /> 93 + ) : ( 94 + <CopyCheck className="h-3 w-3" /> 95 + )} 96 + </Button> 69 97 {active === workspace.slug ? ( 70 - <Check className="ml-2 h-4 w-4" /> 98 + <Check className="ml-2 h-4 w-4 shrink-0" /> 71 99 ) : null} 72 100 </a> 73 101 </DropdownMenuItem>
+1 -1
apps/web/src/config/pricing-table.tsx
··· 12 12 {changelog?.description}{" "} 13 13 <Link 14 14 href={`/changelog/${changelog.slug}`} 15 - className="underline underline-offset-4 text-nowrap" 15 + className="text-nowrap underline underline-offset-4" 16 16 > 17 17 Learn more 18 18 </Link>
+1 -19
apps/web/src/lib/monitor/utils.ts
··· 9 9 10 10 import type { MonitorPeriodicity } from "@openstatus/db/src/schema"; 11 11 12 - export const periods = ["1h", "1d", "3d", "7d", "14d"] as const; // If neeeded (e.g. Pro plans), "7d", "30d" 12 + export const periods = ["1d", "7d", "14d"] as const; // If neeeded (e.g. Pro plans), "7d", "30d" 13 13 export const quantiles = ["p99", "p95", "p90", "p75", "p50"] as const; 14 14 export const intervals = ["1m", "10m", "30m", "1h"] as const; 15 15 export const triggers = ["cron", "api"] as const; ··· 21 21 22 22 export function getDateByPeriod(period: Period) { 23 23 switch (period) { 24 - case "1h": 25 - return { 26 - from: subHours(startOfHour(new Date()), 1), 27 - to: endOfHour(new Date()), 28 - }; 29 24 case "1d": 30 25 return { 31 26 from: subDays(startOfHour(new Date()), 1), 32 27 to: endOfDay(new Date()), 33 28 }; 34 - case "3d": 35 - return { 36 - from: subDays(startOfDay(new Date()), 3), 37 - to: endOfDay(new Date()), 38 - }; 39 29 case "7d": 40 30 return { 41 31 from: subDays(startOfDay(new Date()), 7), ··· 55 45 56 46 export function getHoursByPeriod(period: Period) { 57 47 switch (period) { 58 - case "1h": 59 - return 1; 60 48 case "1d": 61 49 return 24; 62 - case "3d": 63 - return 72; 64 50 case "7d": 65 51 return 168; 66 52 case "14d": ··· 74 60 75 61 export function periodFormatter(period: Period) { 76 62 switch (period) { 77 - case "1h": 78 - return "Last hour"; 79 63 case "1d": 80 64 return "Last day"; 81 - case "3d": 82 - return "Last 3 days"; 83 65 case "7d": 84 66 return "Last 7 days"; 85 67 case "14d":
+213 -11
apps/web/src/lib/tb.ts
··· 1 - import type { HomeStatsParams } from "@openstatus/tinybird"; 2 - import { Tinybird, getHomeStats } from "@openstatus/tinybird"; 1 + import { OSTinybird } from "@openstatus/tinybird"; 3 2 4 3 import { env } from "@/env"; 5 4 6 - // @depreciated in favor to use the OSTinybird client directly 7 - const tb = new Tinybird({ token: env.TINY_BIRD_API_KEY }); 5 + const tb = new OSTinybird(env.TINY_BIRD_API_KEY); 8 6 9 - export async function getHomeStatsData(props: Partial<HomeStatsParams>) { 10 - try { 11 - const res = await getHomeStats(tb)(props); 12 - return res.data; 13 - } catch (e) { 14 - console.error(e); 7 + // REMINDER: we could extend the limits (WorkspacePlan) by 8 + // knowing which plan the user is on and disable some periods 9 + const periods = ["1d", "7d", "14d"] as const; 10 + const types = ["http", "tcp"] as const; 11 + 12 + // FIXME: check we we can also use Period from elswhere 13 + type Period = (typeof periods)[number]; 14 + // FIMXE: use JobType instead! 15 + type Type = (typeof types)[number]; 16 + 17 + // REMINDER: extend if needed 18 + export function prepareListByPeriod(period: Period, type: Type = "http") { 19 + switch (period) { 20 + case "1d": { 21 + const getData = { 22 + http: tb.httpListDaily, 23 + tcp: tb.tcpListDaily, 24 + } as const; 25 + return { getData: getData[type] }; 26 + } 27 + case "7d": { 28 + const getData = { 29 + http: tb.httpListWeekly, 30 + tcp: tb.tcpListWeekly, 31 + } as const; 32 + return { getData: getData[type] }; 33 + } 34 + case "14d": { 35 + const getData = { 36 + http: tb.httpListBiweekly, 37 + tcp: tb.tcpListBiweekly, 38 + } as const; 39 + return { getData: getData[type] }; 40 + } 41 + default: { 42 + const getData = { 43 + http: tb.httpListDaily, 44 + tcp: tb.tcpListDaily, 45 + } as const; 46 + return { getData: getData[type] }; 47 + } 15 48 } 16 - return; 17 49 } 50 + 51 + export function prepareMetricsByPeriod(period: Period, type: Type = "http") { 52 + switch (period) { 53 + case "1d": { 54 + const getData = { 55 + http: tb.httpMetricsDaily, 56 + tcp: tb.tcpMetricsDaily, 57 + } as const; 58 + return { getData: getData[type] }; 59 + } 60 + case "7d": { 61 + const getData = { 62 + http: tb.httpMetricsWeekly, 63 + tcp: tb.tcpMetricsWeekly, 64 + } as const; 65 + return { getData: getData[type] }; 66 + } 67 + case "14d": { 68 + const getData = { 69 + http: tb.httpMetricsBiweekly, 70 + tcp: tb.tcpMetricsBiweekly, 71 + } as const; 72 + return { getData: getData[type] }; 73 + } 74 + default: { 75 + const getData = { 76 + http: tb.httpMetricsDaily, 77 + tcp: tb.tcpMetricsDaily, 78 + } as const; 79 + return { getData: getData[type] }; 80 + } 81 + } 82 + } 83 + 84 + export function prepareMetricByRegionByPeriod( 85 + period: Period, 86 + type: Type = "http", 87 + ) { 88 + switch (period) { 89 + case "1d": { 90 + const getData = { 91 + http: tb.httpMetricsByRegionDaily, 92 + tcp: tb.tcpMetricsByRegionDaily, 93 + } as const; 94 + return { getData: getData[type] }; 95 + } 96 + case "7d": { 97 + const getData = { 98 + http: tb.httpMetricsByRegionWeekly, 99 + tcp: tb.tcpMetricsByRegionWeekly, 100 + } as const; 101 + return { getData: getData[type] }; 102 + } 103 + case "14d": { 104 + const getData = { 105 + http: tb.httpMetricsByRegionBiweekly, 106 + tcp: tb.tcpMetricsByRegionBiweekly, 107 + } as const; 108 + return { getData: getData[type] }; 109 + } 110 + default: { 111 + const getData = { 112 + http: tb.httpMetricsByRegionDaily, 113 + tcp: tb.tcpMetricsByRegionDaily, 114 + } as const; 115 + return { getData: getData[type] }; 116 + } 117 + } 118 + } 119 + 120 + export function prepareMetricByIntervalByPeriod( 121 + period: Period, 122 + type: Type = "http", 123 + ) { 124 + switch (period) { 125 + case "1d": { 126 + const getData = { 127 + http: tb.httpMetricsByIntervalDaily, 128 + tcp: tb.tcpMetricsByIntervalDaily, 129 + } as const; 130 + return { getData: getData[type] }; 131 + } 132 + case "7d": { 133 + const getData = { 134 + http: tb.httpMetricsByIntervalWeekly, 135 + tcp: tb.tcpMetricsByIntervalWeekly, 136 + } as const; 137 + return { getData: getData[type] }; 138 + } 139 + case "14d": { 140 + const getData = { 141 + http: tb.httpMetricsByIntervalBiweekly, 142 + tcp: tb.tcpMetricsByIntervalBiweekly, 143 + } as const; 144 + return { getData: getData[type] }; 145 + } 146 + default: { 147 + const getData = { 148 + http: tb.httpMetricsByIntervalDaily, 149 + tcp: tb.tcpMetricsByIntervalDaily, 150 + } as const; 151 + return { getData: getData[type] }; 152 + } 153 + } 154 + } 155 + 156 + export function prepareStatusByPeriod( 157 + period: "7d" | "45d", 158 + type: Type = "http", 159 + ) { 160 + switch (period) { 161 + case "7d": { 162 + const getData = { 163 + http: tb.httpStatusWeekly, 164 + tcp: tb.tcpStatusWeekly, 165 + } as const; 166 + return { getData: getData[type] }; 167 + } 168 + case "45d": { 169 + const getData = { 170 + http: tb.httpStatus45d, 171 + tcp: tb.tcpStatus45d, 172 + } as const; 173 + return { getData: getData[type] }; 174 + } 175 + default: { 176 + const getData = { 177 + http: tb.httpStatusWeekly, 178 + tcp: tb.tcpStatusWeekly, 179 + } as const; 180 + return { getData: getData[type] }; 181 + } 182 + } 183 + } 184 + 185 + export function prepareGetByPeriod(period: "30d", type: Type = "http") { 186 + switch (period) { 187 + case "30d": { 188 + const getData = { 189 + http: tb.httpGetMonthly, 190 + tcp: tb.tcpGetMonthly, 191 + } as const; 192 + return { getData: getData[type] }; 193 + } 194 + default: { 195 + const getData = { 196 + http: tb.httpGetMonthly, 197 + tcp: tb.tcpGetMonthly, 198 + } as const; 199 + return { getData: getData[type] }; 200 + } 201 + } 202 + } 203 + 204 + // FOR MIGRATION 205 + export type ResponseTimeMetrics = Awaited< 206 + ReturnType<OSTinybird["httpMetricsDaily"]> 207 + >["data"][number]; 208 + 209 + export type ResponseTimeMetricsByRegion = Awaited< 210 + ReturnType<OSTinybird["httpMetricsByRegionDaily"]> 211 + >["data"][number]; 212 + 213 + export type ResponseGraph = Awaited< 214 + ReturnType<OSTinybird["httpMetricsByIntervalDaily"]> 215 + >["data"][number]; 216 + 217 + export type ResponseStatusTracker = Awaited< 218 + ReturnType<OSTinybird["httpStatusWeekly"]> 219 + >["data"][number];
+1 -1
apps/web/src/trpc/rq-server.ts
··· 9 9 import { makeQueryClient } from "./query-client"; 10 10 11 11 const createContextCached = cache( 12 - async (...args: unknown[]): Promise<Context> => { 12 + async (..._args: unknown[]): Promise<Context> => { 13 13 const session = await auth(); 14 14 15 15 return {
+4 -50
packages/api/src/router/tinybird/index.ts
··· 6 6 import { env } from "../../env"; 7 7 import { createTRPCRouter, protectedProcedure } from "../../trpc"; 8 8 9 - const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 9 + const tb = new OSTinybird(env.TINY_BIRD_API_KEY); 10 10 11 11 // WORK IN PROGRESS - we can create a tb router to call it via TRPC server and client 12 12 13 13 export const tinybirdRouter = createTRPCRouter({ 14 - lastCronTimestamp: protectedProcedure.query(async (opts) => { 15 - const workspaceId = String(opts.ctx.workspace.id); 16 - return await tb.endpointLastCronTimestamp("workspace")({ workspaceId }); 17 - }), 18 - 19 - monitorMetricsFromWorkspace: protectedProcedure 20 - .input(z.object({ period: z.string() })) 21 - .query(async (opts) => { 22 - const _workspaceId = String(opts.ctx.workspace.id); 23 - }), 24 - 25 - responseDetails: protectedProcedure 14 + httpGetMonthly: protectedProcedure 26 15 .input( 27 16 z.object({ 28 - monitorId: z.string().default("").optional(), 29 - url: z.string().url().optional(), 17 + monitorId: z.string(), 30 18 region: z.enum(flyRegions).optional(), 31 19 cronTimestamp: z.number().int().optional(), 32 20 }), 33 21 ) 34 22 .query(async (opts) => { 35 - return await tb.endpointResponseDetails("7d")(opts.input); 36 - }), 37 - 38 - totalRumMetricsForApplication: protectedProcedure 39 - .input(z.object({ dsn: z.string(), period: z.enum(["24h", "7d", "30d"]) })) 40 - .query(async (opts) => { 41 - return await tb.applicationRUMMetrics()(opts.input); 42 - }), 43 - rumMetricsForApplicationPerPage: protectedProcedure 44 - .input(z.object({ dsn: z.string(), period: z.enum(["24h", "7d", "30d"]) })) 45 - .query(async (opts) => { 46 - return await tb.applicationRUMMetricsPerPage()(opts.input); 47 - }), 48 - 49 - rumMetricsForPath: protectedProcedure 50 - .input( 51 - z.object({ 52 - dsn: z.string(), 53 - path: z.string(), 54 - period: z.enum(["24h", "7d", "30d"]), 55 - }), 56 - ) 57 - .query(async (opts) => { 58 - return await tb.applicationRUMMetricsForPath()(opts.input); 59 - }), 60 - sessionRumMetricsForPath: protectedProcedure 61 - .input( 62 - z.object({ 63 - dsn: z.string(), 64 - path: z.string(), 65 - period: z.enum(["24h", "7d", "30d"]), 66 - }), 67 - ) 68 - .query(async (opts) => { 69 - return await tb.applicationSessionMetricsPerPath()(opts.input); 23 + return await tb.httpGetMonthly(opts.input); 70 24 }), 71 25 });
+2
packages/db/src/schema/constants.ts
··· 52 52 export const monitorPeriodicitySchema = z.enum(monitorPeriodicity); 53 53 export const monitorRegionSchema = z.enum(monitorRegions); 54 54 export const monitorFlyRegionSchema = z.enum(flyRegions); 55 + 55 56 export type MonitorFlyRegion = z.infer<typeof monitorFlyRegionSchema>; 57 + export type Region = z.infer<typeof monitorFlyRegionSchema>;
+17
packages/tinybird/README.md
··· 33 33 34 34 Link to the [issue](https://github.com/openstatusHQ/openstatus/issues/278) from 35 35 Gonzalo as reference. 36 + 37 + 38 + <!-- FIXME: more content --> 39 + 40 + ```bash 41 + python3 -m venv .venv 42 + source .venv/bin/activate 43 + pip install tinybird-cli 44 + tb auth -i 45 + ``` 46 + 47 + ```bash 48 + tb pull 49 + tb push aggregate_*.pipe --populate 50 + tb push endpoint_*.pipe 51 + ... 52 + ```
+28
packages/tinybird/pipes/aggregate__http_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + statusCode, 15 + timestamp, 16 + cronTimestamp, 17 + monitorId, 18 + workspaceId 19 + FROM ping_response__v8 20 + 21 + TYPE materialized 22 + DATASOURCE mv__http_14d__v0 23 + ENGINE "MergeTree" 24 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 25 + ENGINE_SORTING_KEY "monitorId, time" 26 + ENGINE_TTL "time + toIntervalDay(14)" 27 + 28 +
+28
packages/tinybird/pipes/aggregate__http_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + statusCode, 15 + timestamp, 16 + cronTimestamp, 17 + monitorId, 18 + workspaceId 19 + FROM ping_response__v8 20 + 21 + TYPE materialized 22 + DATASOURCE mv__http_1d__v0 23 + ENGINE "MergeTree" 24 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 25 + ENGINE_SORTING_KEY "monitorId, time" 26 + ENGINE_TTL "time + toIntervalDay(1)" 27 + 28 +
+28
packages/tinybird/pipes/aggregate__http_30d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + statusCode, 15 + timestamp, 16 + cronTimestamp, 17 + monitorId, 18 + workspaceId 19 + FROM ping_response__v8 20 + 21 + TYPE materialized 22 + DATASOURCE mv__http_30d__v0 23 + ENGINE "MergeTree" 24 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 25 + ENGINE_SORTING_KEY "monitorId, time" 26 + ENGINE_TTL "time + toIntervalDay(30)" 27 + 28 +
+28
packages/tinybird/pipes/aggregate__http_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + statusCode, 15 + timestamp, 16 + cronTimestamp, 17 + monitorId, 18 + workspaceId 19 + FROM ping_response__v8 20 + 21 + TYPE materialized 22 + DATASOURCE mv__http_7d__v0 23 + ENGINE "MergeTree" 24 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 25 + ENGINE_SORTING_KEY "monitorId, time" 26 + ENGINE_TTL "time + toIntervalDay(7)" 27 + 28 +
+23
packages/tinybird/pipes/aggregate__http_full_30d.pipe
··· 1 + VERSION 0 2 + DESCRIPTION > 3 + Stores all the data from the http_response table for the last 30 days, mainly used for accessing the data details. 4 + 5 + 6 + TAGS http, full 7 + 8 + NODE aggregate 9 + SQL > 10 + 11 + SELECT 12 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 13 + * 14 + FROM ping_response__v8 15 + 16 + TYPE materialized 17 + DATASOURCE mv__http_full_30d__v0 18 + ENGINE "MergeTree" 19 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 20 + ENGINE_SORTING_KEY "monitorId, time" 21 + ENGINE_TTL "time + toIntervalDay(30)" 22 + 23 +
+24
packages/tinybird/pipes/aggregate__http_status_45d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http, statuspage 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, 10 + monitorId, 11 + countState() AS count, 12 + countState(if(error = 0, 1, NULL)) AS ok 13 + FROM ping_response__v8 14 + GROUP BY 15 + time, 16 + monitorId 17 + 18 + TYPE materialized 19 + DATASOURCE mv__http_status_45d__v0 20 + ENGINE "AggregatingMergeTree" 21 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 22 + ENGINE_SORTING_KEY "monitorId, time" 23 + ENGINE_TTL "time + toIntervalDay(45)" 24 +
+24
packages/tinybird/pipes/aggregate__http_status_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http, statuspage 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, 10 + monitorId, 11 + countState() AS count, 12 + countState(if(error = 0, 1, NULL)) AS ok 13 + FROM ping_response__v8 14 + GROUP BY 15 + time, 16 + monitorId 17 + 18 + TYPE materialized 19 + DATASOURCE mv__http_status_7d__v0 20 + ENGINE "AggregatingMergeTree" 21 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 22 + ENGINE_SORTING_KEY "monitorId, time" 23 + ENGINE_TTL "time + toIntervalDay(7)" 24 +
+27
packages/tinybird/pipes/aggregate__tcp_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + timestamp, 15 + cronTimestamp, 16 + monitorId, 17 + workspaceId 18 + FROM tcp_response__v0 19 + 20 + TYPE materialized 21 + DATASOURCE mv__tcp_14d__v0 22 + ENGINE "MergeTree" 23 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 24 + ENGINE_SORTING_KEY "monitorId, time" 25 + ENGINE_TTL "time + toIntervalDay(14)" 26 + 27 +
+27
packages/tinybird/pipes/aggregate__tcp_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + timestamp, 15 + cronTimestamp, 16 + monitorId, 17 + workspaceId 18 + FROM tcp_response__v0 19 + 20 + TYPE materialized 21 + DATASOURCE mv__tcp_1d__v0 22 + ENGINE "MergeTree" 23 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 24 + ENGINE_SORTING_KEY "monitorId, time" 25 + ENGINE_TTL "time + toIntervalDay(1)" 26 + 27 +
+27
packages/tinybird/pipes/aggregate__tcp_30d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + timestamp, 15 + cronTimestamp, 16 + monitorId, 17 + workspaceId 18 + FROM tcp_response__v0 19 + 20 + TYPE materialized 21 + DATASOURCE mv__tcp_30d__v0 22 + ENGINE "MergeTree" 23 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 24 + ENGINE_SORTING_KEY "monitorId, time" 25 + ENGINE_TTL "time + toIntervalDay(30)" 26 + 27 +
+27
packages/tinybird/pipes/aggregate__tcp_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 10 + latency, 11 + error, 12 + region, 13 + trigger, 14 + timestamp, 15 + cronTimestamp, 16 + monitorId, 17 + workspaceId 18 + FROM tcp_response__v0 19 + 20 + TYPE materialized 21 + DATASOURCE mv__tcp_7d__v0 22 + ENGINE "MergeTree" 23 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 24 + ENGINE_SORTING_KEY "monitorId, time" 25 + ENGINE_TTL "time + toIntervalDay(7)" 26 + 27 +
+23
packages/tinybird/pipes/aggregate__tcp_full_30d.pipe
··· 1 + VERSION 0 2 + DESCRIPTION > 3 + Stores all the data from the http_response table for the last 30 days, mainly used for accessing the data details. 4 + 5 + 6 + TAGS tcp, full 7 + 8 + NODE aggregate 9 + SQL > 10 + 11 + SELECT 12 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 13 + * 14 + FROM tcp_response__v0 15 + 16 + TYPE materialized 17 + DATASOURCE mv__tcp_full_30d__v0 18 + ENGINE "MergeTree" 19 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 20 + ENGINE_SORTING_KEY "monitorId, time" 21 + ENGINE_TTL "time + toIntervalDay(30)" 22 + 23 +
+24
packages/tinybird/pipes/aggregate__tcp_status_45d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp, statuspage 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, 10 + monitorId, 11 + countState() AS count, 12 + countState(if(error = 0, 1, NULL)) AS ok 13 + FROM tcp_response__v0 14 + GROUP BY 15 + time, 16 + monitorId 17 + 18 + TYPE materialized 19 + DATASOURCE mv__tcp_status_45d__v0 20 + ENGINE "AggregatingMergeTree" 21 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 22 + ENGINE_SORTING_KEY "monitorId, time" 23 + ENGINE_TTL "time + toIntervalDay(45)" 24 +
+24
packages/tinybird/pipes/aggregate__tcp_status_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp, statuspage 4 + 5 + NODE aggregate 6 + SQL > 7 + 8 + SELECT 9 + toStartOfDay(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, 10 + monitorId, 11 + countState() AS count, 12 + countState(if(error = 0, 1, NULL)) AS ok 13 + FROM tcp_response__v0 14 + GROUP BY 15 + time, 16 + monitorId 17 + 18 + TYPE materialized 19 + DATASOURCE mv__tcp_status_7d__v0 20 + ENGINE "AggregatingMergeTree" 21 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 22 + ENGINE_SORTING_KEY "monitorId, time" 23 + ENGINE_TTL "time + toIntervalDay(7)" 24 +
-17
packages/tinybird/pipes/dashboard_request.pipe
··· 1 - VERSION 0 2 - NODE dashboard_request_0 3 - SQL > 4 - 5 - % 6 - SELECT * FROM monitor_request_count_7d_mv 7 - WHERE monitorId = {{ String(monitorId, '1') }} 8 - {% if defined(url) %} AND url = {{ String(url) }} {% end %} 9 - 10 - 11 - 12 - NODE dashboard_request_1 13 - SQL > 14 - 15 - SELECT * FROM dashboard_request_0 16 - 17 -
+18
packages/tinybird/pipes/endpoint__http_get_30d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * 10 + FROM mv__http_full_30d__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1', required=True) }} 13 + AND cronTimestamp = {{ Int64(cronTimestamp, 1709477432205, required=True) }} 14 + AND region = {{ String(region, 'ams', required=True) }} 15 + ORDER BY cronTimestamp DESC 16 + 17 + 18 +
+14
packages/tinybird/pipes/endpoint__http_list_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * FROM mv__http_14d__v0 10 + WHERE 11 + monitorId = {{ String(monitorId, '1', required=True) }} 12 + ORDER BY cronTimestamp DESC 13 + 14 +
+14
packages/tinybird/pipes/endpoint__http_list_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * FROM mv__http_1d__v0 10 + WHERE 11 + monitorId = {{ String(monitorId, '1', required=True) }} 12 + ORDER BY cronTimestamp DESC 13 + 14 +
+14
packages/tinybird/pipes/endpoint__http_list_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * FROM mv__http_7d__v0 10 + WHERE 11 + monitorId = {{ String(monitorId, '1', required=True) }} 12 + ORDER BY cronTimestamp DESC 13 + 14 +
+38
packages/tinybird/pipes/endpoint__http_metrics_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.50)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.90)(latency)) as p90Latency, 13 + round(quantile(0.95)(latency)) as p95Latency, 14 + round(quantile(0.99)(latency)) as p99Latency, 15 + count() as count, 16 + count(if(error = 0, 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM mv__http_14d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3) 22 + UNION ALL 23 + SELECT 24 + round(quantile(0.50)(latency)) AS p50Latency, 25 + round(quantile(0.75)(latency)) AS p75Latency, 26 + round(quantile(0.90)(latency)) AS p90Latency, 27 + round(quantile(0.95)(latency)) AS p95Latency, 28 + round(quantile(0.99)(latency)) AS p99Latency, 29 + count() as count, 30 + count(if(error = 0, 1, NULL)) AS ok, 31 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 32 + FROM mv__http_30d__v0 33 + WHERE 34 + monitorId = {{ String(monitorId, '1', required=True) }} 35 + AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3) 36 + AND time < toDateTime64(now() - INTERVAL 14 DAY, 3) 37 + 38 +
+38
packages/tinybird/pipes/endpoint__http_metrics_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.50)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.90)(latency)) as p90Latency, 13 + round(quantile(0.95)(latency)) as p95Latency, 14 + round(quantile(0.99)(latency)) as p99Latency, 15 + count() as count, 16 + count(if(error = 0, 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM mv__http_1d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3) 22 + UNION ALL 23 + SELECT 24 + round(quantile(0.50)(latency)) AS p50Latency, 25 + round(quantile(0.75)(latency)) AS p75Latency, 26 + round(quantile(0.90)(latency)) AS p90Latency, 27 + round(quantile(0.95)(latency)) AS p95Latency, 28 + round(quantile(0.99)(latency)) AS p99Latency, 29 + count() as count, 30 + count(if(error = 0, 1, NULL)) AS ok, 31 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 32 + FROM mv__http_7d__v0 -- REMINDER: this will increase the processed data compared to 3d 33 + WHERE 34 + monitorId = {{ String(monitorId, '1', required=True) }} 35 + AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3) 36 + AND time < toDateTime64(now() - INTERVAL 1 DAY, 3) 37 + 38 +
+38
packages/tinybird/pipes/endpoint__http_metrics_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.50)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.90)(latency)) as p90Latency, 13 + round(quantile(0.95)(latency)) as p95Latency, 14 + round(quantile(0.99)(latency)) as p99Latency, 15 + count() as count, 16 + count(if(error = 0, 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM mv__http_7d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3) 22 + UNION ALL 23 + SELECT 24 + round(quantile(0.50)(latency)) AS p50Latency, 25 + round(quantile(0.75)(latency)) AS p75Latency, 26 + round(quantile(0.90)(latency)) AS p90Latency, 27 + round(quantile(0.95)(latency)) AS p95Latency, 28 + round(quantile(0.99)(latency)) AS p99Latency, 29 + count() as count, 30 + count(if(error = 0, 1, NULL)) AS ok, 31 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 32 + FROM mv__http_14d__v0 33 + WHERE 34 + monitorId = {{ String(monitorId, '1', required=True) }} 35 + AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3) 36 + AND time < toDateTime64(now() - INTERVAL 7 DAY, 3) 37 + 38 +
+27
packages/tinybird/pipes/endpoint__http_metrics_by_interval_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.50)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.90)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM mv__http_14d__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1', required=True) }} 24 + GROUP BY h, region 25 + ORDER BY h DESC 26 + 27 +
+27
packages/tinybird/pipes/endpoint__http_metrics_by_interval_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.50)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.90)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM mv__http_1d__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1', required=True) }} 24 + GROUP BY h, region 25 + ORDER BY h DESC 26 + 27 +
+27
packages/tinybird/pipes/endpoint__http_metrics_by_interval_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.50)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.90)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM mv__http_7d__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1', required=True) }} 24 + GROUP BY h, region 25 + ORDER BY h DESC 26 + 27 +
+27
packages/tinybird/pipes/endpoint__http_metrics_by_region_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + round(quantile(0.5)(latency)) as p50Latency, 12 + round(quantile(0.75)(latency)) as p75Latency, 13 + round(quantile(0.9)(latency)) as p90Latency, 14 + round(quantile(0.95)(latency)) as p95Latency, 15 + round(quantile(0.99)(latency)) as p99Latency, 16 + count() as count, 17 + count(if(error = 0, 1, NULL)) AS ok 18 + FROM mv__http_14d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + GROUP BY region 22 + 23 + 24 + 25 + 26 + 27 +
+24
packages/tinybird/pipes/endpoint__http_metrics_by_region_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + round(quantile(0.5)(latency)) as p50Latency, 12 + round(quantile(0.75)(latency)) as p75Latency, 13 + round(quantile(0.9)(latency)) as p90Latency, 14 + round(quantile(0.95)(latency)) as p95Latency, 15 + round(quantile(0.99)(latency)) as p99Latency, 16 + count() as count, 17 + count(if(error = 0, 1, NULL)) AS ok 18 + FROM mv__http_1d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + GROUP BY region 22 + 23 + 24 +
+24
packages/tinybird/pipes/endpoint__http_metrics_by_region_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + round(quantile(0.5)(latency)) as p50Latency, 12 + round(quantile(0.75)(latency)) as p75Latency, 13 + round(quantile(0.9)(latency)) as p90Latency, 14 + round(quantile(0.95)(latency)) as p95Latency, 15 + round(quantile(0.99)(latency)) as p99Latency, 16 + count() as count, 17 + count(if(error = 0, 1, NULL)) AS ok 18 + FROM mv__http_7d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + GROUP BY region 22 + 23 + 24 +
+22
packages/tinybird/pipes/endpoint__http_status_45d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT time as day, countMerge(count) as count, countMerge(ok) as ok 10 + FROM mv__http_status_45d__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1', required=True) }} 13 + GROUP BY day 14 + ORDER BY day DESC 15 + WITH FILL 16 + FROM 17 + toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC'))) 18 + TO toStartOfDay( 19 + date_sub(DAY, 45, now()) 20 + ) STEP INTERVAL -1 DAY 21 + 22 +
+22
packages/tinybird/pipes/endpoint__http_status_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS http 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT time as day, countMerge(count) as count, countMerge(ok) as ok 10 + FROM mv__http_status_7d__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1', required=True) }} 13 + GROUP BY day 14 + ORDER BY day DESC 15 + WITH FILL 16 + FROM 17 + toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC'))) 18 + TO toStartOfDay( 19 + date_sub(DAY, 7, now()) 20 + ) STEP INTERVAL -1 DAY 21 + 22 +
+18
packages/tinybird/pipes/endpoint__stats_global.pipe
··· 1 + VERSION 0 2 + 3 + NODE endpoint 4 + SQL > 5 + 6 + % 7 + SELECT COUNT(*) as count 8 + FROM ping_response__v8 9 + {% if defined(period) %} 10 + {% if String(period) == "1h" %} 11 + WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000 12 + {% elif String(period) == "10m" %} 13 + WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 10 MINUTE) * 1000 14 + {% else %} 15 + WHERE cronTimestamp > 0 16 + {% end %} 17 + {% end %} 18 +
+18
packages/tinybird/pipes/endpoint__tcp_get_30d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * 10 + FROM mv__tcp_full_30d__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1', required=True) }} 13 + AND cronTimestamp = {{ Int64(cronTimestamp, 1709477432205, required=True) }} 14 + AND region = {{ String(region, 'ams', required=True) }} 15 + ORDER BY cronTimestamp DESC 16 + 17 + 18 +
+14
packages/tinybird/pipes/endpoint__tcp_list_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * FROM mv__tcp_14d__v0 10 + WHERE 11 + monitorId = {{ String(monitorId, '1', required=True) }} 12 + ORDER BY cronTimestamp DESC 13 + 14 +
+14
packages/tinybird/pipes/endpoint__tcp_list_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * FROM mv__tcp_1d__v0 10 + WHERE 11 + monitorId = {{ String(monitorId, '1', required=True) }} 12 + ORDER BY cronTimestamp DESC 13 + 14 +
+14
packages/tinybird/pipes/endpoint__tcp_list_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT * FROM mv__tcp_7d__v0 10 + WHERE 11 + monitorId = {{ String(monitorId, '1', required=True) }} 12 + ORDER BY cronTimestamp DESC 13 + 14 +
+41
packages/tinybird/pipes/endpoint__tcp_metrics_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.50)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.90)(latency)) as p90Latency, 13 + round(quantile(0.95)(latency)) as p95Latency, 14 + round(quantile(0.99)(latency)) as p99Latency, 15 + count() as count, 16 + count(if(error = 0, 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM mv__tcp_14d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + -- FIXME: we can reduce the data processed by using removing it entirely 22 + -- because the query is useless as we are in the 14d context 23 + -- TODO: check where we can reduce the data processed 24 + AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3) 25 + UNION ALL 26 + SELECT 27 + round(quantile(0.50)(latency)) AS p50Latency, 28 + round(quantile(0.75)(latency)) AS p75Latency, 29 + round(quantile(0.90)(latency)) AS p90Latency, 30 + round(quantile(0.95)(latency)) AS p95Latency, 31 + round(quantile(0.99)(latency)) AS p99Latency, 32 + count() as count, 33 + count(if(error = 0, 1, NULL)) AS ok, 34 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 35 + FROM mv__tcp_30d__v0 -- REMINDER: this will increase the processed data compared to 3d 36 + WHERE 37 + monitorId = {{ String(monitorId, '1', required=True) }} 38 + AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3) 39 + AND time < toDateTime64(now() - INTERVAL 14 DAY, 3) 40 + 41 +
+38
packages/tinybird/pipes/endpoint__tcp_metrics_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.50)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.90)(latency)) as p90Latency, 13 + round(quantile(0.95)(latency)) as p95Latency, 14 + round(quantile(0.99)(latency)) as p99Latency, 15 + count() as count, 16 + count(if(error = 0, 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM mv__tcp_1d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3) 22 + UNION ALL 23 + SELECT 24 + round(quantile(0.50)(latency)) AS p50Latency, 25 + round(quantile(0.75)(latency)) AS p75Latency, 26 + round(quantile(0.90)(latency)) AS p90Latency, 27 + round(quantile(0.95)(latency)) AS p95Latency, 28 + round(quantile(0.99)(latency)) AS p99Latency, 29 + count() as count, 30 + count(if(error = 0, 1, NULL)) AS ok, 31 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 32 + FROM mv__tcp_7d__v0 -- REMINDER: this will increase the processed data compared to 3d 33 + WHERE 34 + monitorId = {{ String(monitorId, '1', required=True) }} 35 + AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3) 36 + AND time < toDateTime64(now() - INTERVAL 1 DAY, 3) 37 + 38 +
+38
packages/tinybird/pipes/endpoint__tcp_metrics_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.50)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.90)(latency)) as p90Latency, 13 + round(quantile(0.95)(latency)) as p95Latency, 14 + round(quantile(0.99)(latency)) as p99Latency, 15 + count() as count, 16 + count(if(error = 0, 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM mv__tcp_7d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3) 22 + UNION ALL 23 + SELECT 24 + round(quantile(0.50)(latency)) AS p50Latency, 25 + round(quantile(0.75)(latency)) AS p75Latency, 26 + round(quantile(0.90)(latency)) AS p90Latency, 27 + round(quantile(0.95)(latency)) AS p95Latency, 28 + round(quantile(0.99)(latency)) AS p99Latency, 29 + count() as count, 30 + count(if(error = 0, 1, NULL)) AS ok, 31 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 32 + FROM mv__tcp_14d__v0 -- REMINDER: this will increase the processed data compared to 3d 33 + WHERE 34 + monitorId = {{ String(monitorId, '1', required=True) }} 35 + AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3) 36 + AND time < toDateTime64(now() - INTERVAL 7 DAY, 3) 37 + 38 +
+27
packages/tinybird/pipes/endpoint__tcp_metrics_by_interval_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.50)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.90)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM mv__tcp_14d__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1', required=True) }} 24 + GROUP BY h, region 25 + ORDER BY h DESC 26 + 27 +
+27
packages/tinybird/pipes/endpoint__tcp_metrics_by_interval_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.50)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.90)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM mv__tcp_1d__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1', required=True) }} 24 + GROUP BY h, region 25 + ORDER BY h DESC 26 + 27 +
+27
packages/tinybird/pipes/endpoint__tcp_metrics_by_interval_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE -- use 2880 (2d) in case you want the 1d summary for the regions 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.50)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.90)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM mv__tcp_7d__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1', required=True) }} 24 + GROUP BY h, region 25 + ORDER BY h DESC 26 + 27 +
+24
packages/tinybird/pipes/endpoint__tcp_metrics_by_region_14d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + round(quantile(0.5)(latency)) as p50Latency, 12 + round(quantile(0.75)(latency)) as p75Latency, 13 + round(quantile(0.9)(latency)) as p90Latency, 14 + round(quantile(0.95)(latency)) as p95Latency, 15 + round(quantile(0.99)(latency)) as p99Latency, 16 + count() as count, 17 + count(if(error = 0, 1, NULL)) AS ok 18 + FROM mv__tcp_14d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + GROUP BY region 22 + 23 + 24 +
+24
packages/tinybird/pipes/endpoint__tcp_metrics_by_region_1d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + round(quantile(0.5)(latency)) as p50Latency, 12 + round(quantile(0.75)(latency)) as p75Latency, 13 + round(quantile(0.9)(latency)) as p90Latency, 14 + round(quantile(0.95)(latency)) as p95Latency, 15 + round(quantile(0.99)(latency)) as p99Latency, 16 + count() as count, 17 + count(if(error = 0, 1, NULL)) AS ok 18 + FROM mv__tcp_1d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + GROUP BY region 22 + 23 + 24 +
+24
packages/tinybird/pipes/endpoint__tcp_metrics_by_region_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + round(quantile(0.5)(latency)) as p50Latency, 12 + round(quantile(0.75)(latency)) as p75Latency, 13 + round(quantile(0.9)(latency)) as p90Latency, 14 + round(quantile(0.95)(latency)) as p95Latency, 15 + round(quantile(0.99)(latency)) as p99Latency, 16 + count() as count, 17 + count(if(error = 0, 1, NULL)) AS ok 18 + FROM mv__tcp_7d__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1', required=True) }} 21 + GROUP BY region 22 + 23 + 24 +
+22
packages/tinybird/pipes/endpoint__tcp_status_45d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT time as day, countMerge(count) as count, countMerge(ok) as ok 10 + FROM mv__tcp_status_45d__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1', required=True) }} 13 + GROUP BY day 14 + ORDER BY day DESC 15 + WITH FILL 16 + FROM 17 + toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC'))) 18 + TO toStartOfDay( 19 + date_sub(DAY, 45, now()) 20 + ) STEP INTERVAL -1 DAY 21 + 22 +
+22
packages/tinybird/pipes/endpoint__tcp_status_7d.pipe
··· 1 + VERSION 0 2 + 3 + TAGS tcp 4 + 5 + NODE endpoint 6 + SQL > 7 + 8 + % 9 + SELECT time as day, countMerge(count) as count, countMerge(ok) as ok 10 + FROM mv__tcp_status_7d__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1', required=True) }} 13 + GROUP BY day 14 + ORDER BY day DESC 15 + WITH FILL 16 + FROM 17 + toStartOfDay(toStartOfDay(toTimeZone(now(), 'UTC'))) 18 + TO toStartOfDay( 19 + date_sub(DAY, 7, now()) 20 + ) STEP INTERVAL -1 DAY 21 + 22 +
+1 -1
packages/tinybird/pipes/home_stats.pipe
··· 5 5 6 6 % 7 7 SELECT COUNT(*) as count 8 - FROM ping_response__v7 8 + FROM ping_response__v8 9 9 {% if defined(period) %} 10 10 {% if String(period) == "1h" %} 11 11 WHERE cronTimestamp > toUnixTimestamp(now() - INTERVAL 1 HOUR) * 1000
+675 -53
packages/tinybird/src/client.ts
··· 1 - import { Tinybird } from "@chronark/zod-bird"; 1 + import { Tinybird as Client, NoopTinybird } from "@chronark/zod-bird"; 2 + import { z } from "zod"; 3 + import { flyRegions } from "../../db/src/schema/constants"; 4 + import { headersSchema, timingSchema, triggers } from "./schema"; 2 5 3 - import { 4 - tbBuildHomeStats, 5 - tbBuildMonitorList, 6 - tbBuildPublicStatus, 7 - tbIngestPingResponse, 8 - tbParameterHomeStats, 9 - tbParameterMonitorList, 10 - tbParameterPublicStatus, 11 - } from "./validation"; 6 + export class OSTinybird { 7 + private readonly tb: Client; 12 8 13 - // REMINDER: 14 - const tb = new Tinybird({ token: process.env.TINY_BIRD_API_KEY || "" }); 15 - 16 - export const publishPingResponse = tb.buildIngestEndpoint({ 17 - datasource: "ping_response__v6", 18 - event: tbIngestPingResponse, 19 - }); 9 + constructor(token: string) { 10 + // if (process.env.NODE_ENV === "development") { 11 + // this.tb = new NoopTinybird(); 12 + // } else { 13 + this.tb = new Client({ token }); 14 + // } 15 + } 20 16 21 - /** 22 - * @deprecated but still used in server - please use OSTinybird.endpointStatusPeriod 23 - */ 24 - export function getMonitorList(tb: Tinybird) { 25 - return tb.buildPipe({ 26 - pipe: "status_timezone__v1", 27 - parameters: tbParameterMonitorList, 28 - data: tbBuildMonitorList, 29 - opts: { 30 - // cache: "no-store", 31 - next: { 32 - revalidate: 600, // 10 min cache 17 + public get homeStats() { 18 + return this.tb.buildPipe({ 19 + pipe: "endpoint__stats_global__v0", 20 + parameters: z.object({ 21 + cronTimestamp: z.number().int().optional(), 22 + period: z.enum(["total", "1h", "10m"]).optional(), 23 + }), 24 + data: z.object({ 25 + count: z.number().int(), 26 + }), 27 + opts: { 28 + next: { 29 + revalidate: 43200, // 60 * 60 * 24 = 86400s = 12h 30 + }, 33 31 }, 34 - }, 35 - }); 36 - } 32 + }); 33 + } 37 34 38 - /** 39 - * Homepage stats used for our marketing page 40 - */ 41 - export function getHomeStats(tb: Tinybird) { 42 - return tb.buildPipe({ 43 - pipe: "home_stats__v0", 44 - parameters: tbParameterHomeStats, 45 - data: tbBuildHomeStats, 46 - opts: { 47 - next: { 48 - revalidate: 43200, // 60 * 60 * 24 = 86400s = 12h 49 - }, 50 - }, 51 - }); 52 - } 35 + public get httpListDaily() { 36 + return this.tb.buildPipe({ 37 + pipe: "endpoint__http_list_1d__v0", 38 + parameters: z.object({ 39 + monitorId: z.string(), 40 + }), 41 + data: z.object({ 42 + type: z.literal("http").default("http"), 43 + latency: z.number().int(), 44 + statusCode: z.number().int().nullable(), 45 + monitorId: z.string(), 46 + error: z.coerce.boolean(), 47 + region: z.enum(flyRegions), 48 + cronTimestamp: z.number().int(), 49 + trigger: z.enum(triggers).nullable().default("cron"), 50 + timestamp: z.number(), 51 + workspaceId: z.string(), 52 + }), 53 + opts: { cache: "no-store" }, 54 + }); 55 + } 53 56 54 - export function getPublicStatus(tb: Tinybird) { 55 - return tb.buildPipe({ 56 - pipe: "public_status__v0", 57 - parameters: tbParameterPublicStatus, 58 - data: tbBuildPublicStatus, 59 - }); 57 + public get httpListWeekly() { 58 + return this.tb.buildPipe({ 59 + pipe: "endpoint__http_list_7d__v0", 60 + parameters: z.object({ 61 + monitorId: z.string(), 62 + }), 63 + data: z.object({ 64 + type: z.literal("http").default("http"), 65 + latency: z.number().int(), 66 + statusCode: z.number().int().nullable(), 67 + monitorId: z.string(), 68 + error: z.coerce.boolean(), 69 + region: z.enum(flyRegions), 70 + cronTimestamp: z.number().int(), 71 + trigger: z.enum(triggers).nullable().default("cron"), 72 + timestamp: z.number(), 73 + workspaceId: z.string(), 74 + }), 75 + opts: { cache: "no-store" }, 76 + }); 77 + } 78 + 79 + public get httpListBiweekly() { 80 + return this.tb.buildPipe({ 81 + pipe: "endpoint__http_list_14d__v0", 82 + parameters: z.object({ 83 + monitorId: z.string(), 84 + }), 85 + data: z.object({ 86 + type: z.literal("http").default("http"), 87 + latency: z.number().int(), 88 + statusCode: z.number().int().nullable(), 89 + monitorId: z.string(), 90 + error: z.coerce.boolean(), 91 + region: z.enum(flyRegions), 92 + cronTimestamp: z.number().int(), 93 + trigger: z.enum(triggers).nullable().default("cron"), 94 + timestamp: z.number(), 95 + workspaceId: z.string(), 96 + }), 97 + opts: { cache: "no-store" }, 98 + }); 99 + } 100 + 101 + public get httpMetricsDaily() { 102 + return this.tb.buildPipe({ 103 + pipe: "endpoint__http_metrics_1d__v0", 104 + parameters: z.object({ 105 + interval: z.number().int().optional(), 106 + monitorId: z.string(), 107 + }), 108 + data: z.object({ 109 + p50Latency: z.number().nullable().default(0), 110 + p75Latency: z.number().nullable().default(0), 111 + p90Latency: z.number().nullable().default(0), 112 + p95Latency: z.number().nullable().default(0), 113 + p99Latency: z.number().nullable().default(0), 114 + count: z.number().int(), 115 + ok: z.number().int(), 116 + lastTimestamp: z.number().int().nullable(), 117 + }), 118 + opts: { cache: "no-store" }, 119 + }); 120 + } 121 + 122 + public get httpMetricsWeekly() { 123 + return this.tb.buildPipe({ 124 + pipe: "endpoint__http_metrics_7d__v0", 125 + parameters: z.object({ 126 + interval: z.number().int().optional(), 127 + monitorId: z.string(), 128 + }), 129 + data: z.object({ 130 + p50Latency: z.number().nullable().default(0), 131 + p75Latency: z.number().nullable().default(0), 132 + p90Latency: z.number().nullable().default(0), 133 + p95Latency: z.number().nullable().default(0), 134 + p99Latency: z.number().nullable().default(0), 135 + count: z.number().int(), 136 + ok: z.number().int(), 137 + lastTimestamp: z.number().int().nullable(), 138 + }), 139 + opts: { cache: "no-store" }, 140 + }); 141 + } 142 + 143 + public get httpMetricsBiweekly() { 144 + return this.tb.buildPipe({ 145 + pipe: "endpoint__http_metrics_14d__v0", 146 + parameters: z.object({ 147 + interval: z.number().int().optional(), 148 + monitorId: z.string(), 149 + }), 150 + data: z.object({ 151 + p50Latency: z.number().nullable().default(0), 152 + p75Latency: z.number().nullable().default(0), 153 + p90Latency: z.number().nullable().default(0), 154 + p95Latency: z.number().nullable().default(0), 155 + p99Latency: z.number().nullable().default(0), 156 + count: z.number().int(), 157 + ok: z.number().int(), 158 + lastTimestamp: z.number().int().nullable(), 159 + }), 160 + opts: { cache: "no-store" }, 161 + }); 162 + } 163 + 164 + public get httpMetricsByIntervalDaily() { 165 + return this.tb.buildPipe({ 166 + pipe: "endpoint__http_metrics_by_interval_1d__v0", 167 + parameters: z.object({ 168 + interval: z.number().int().optional(), 169 + monitorId: z.string(), 170 + }), 171 + data: z.object({ 172 + region: z.enum(flyRegions), 173 + timestamp: z.number().int(), 174 + p50Latency: z.number().nullable().default(0), 175 + p75Latency: z.number().nullable().default(0), 176 + p90Latency: z.number().nullable().default(0), 177 + p95Latency: z.number().nullable().default(0), 178 + p99Latency: z.number().nullable().default(0), 179 + }), 180 + opts: { cache: "no-store" }, 181 + }); 182 + } 183 + 184 + public get httpMetricsByIntervalWeekly() { 185 + return this.tb.buildPipe({ 186 + pipe: "endpoint__http_metrics_by_interval_7d__v0", 187 + parameters: z.object({ 188 + interval: z.number().int().optional(), 189 + monitorId: z.string(), 190 + }), 191 + data: z.object({ 192 + region: z.enum(flyRegions), 193 + timestamp: z.number().int(), 194 + p50Latency: z.number().nullable().default(0), 195 + p75Latency: z.number().nullable().default(0), 196 + p90Latency: z.number().nullable().default(0), 197 + p95Latency: z.number().nullable().default(0), 198 + p99Latency: z.number().nullable().default(0), 199 + }), 200 + opts: { cache: "no-store" }, 201 + }); 202 + } 203 + 204 + public get httpMetricsByIntervalBiweekly() { 205 + return this.tb.buildPipe({ 206 + pipe: "endpoint__http_metrics_by_interval_14d__v0", 207 + parameters: z.object({ 208 + interval: z.number().int().optional(), 209 + monitorId: z.string(), 210 + }), 211 + data: z.object({ 212 + region: z.enum(flyRegions), 213 + timestamp: z.number().int(), 214 + p50Latency: z.number().nullable().default(0), 215 + p75Latency: z.number().nullable().default(0), 216 + p90Latency: z.number().nullable().default(0), 217 + p95Latency: z.number().nullable().default(0), 218 + p99Latency: z.number().nullable().default(0), 219 + }), 220 + opts: { cache: "no-store" }, 221 + }); 222 + } 223 + 224 + public get httpMetricsByRegionDaily() { 225 + return this.tb.buildPipe({ 226 + pipe: "endpoint__http_metrics_by_region_1d__v0", 227 + parameters: z.object({ 228 + monitorId: z.string(), 229 + }), 230 + data: z.object({ 231 + region: z.enum(flyRegions), 232 + count: z.number().int(), 233 + ok: z.number().int(), 234 + p50Latency: z.number().nullable().default(0), 235 + p75Latency: z.number().nullable().default(0), 236 + p90Latency: z.number().nullable().default(0), 237 + p95Latency: z.number().nullable().default(0), 238 + p99Latency: z.number().nullable().default(0), 239 + }), 240 + opts: { cache: "no-store" }, 241 + }); 242 + } 243 + 244 + public get httpMetricsByRegionWeekly() { 245 + return this.tb.buildPipe({ 246 + pipe: "endpoint__http_metrics_by_region_7d__v0", 247 + parameters: z.object({ 248 + monitorId: z.string(), 249 + }), 250 + data: z.object({ 251 + region: z.enum(flyRegions), 252 + count: z.number().int(), 253 + ok: z.number().int(), 254 + p50Latency: z.number().nullable().default(0), 255 + p75Latency: z.number().nullable().default(0), 256 + p90Latency: z.number().nullable().default(0), 257 + p95Latency: z.number().nullable().default(0), 258 + p99Latency: z.number().nullable().default(0), 259 + }), 260 + opts: { cache: "no-store" }, 261 + }); 262 + } 263 + 264 + public get httpMetricsByRegionBiweekly() { 265 + return this.tb.buildPipe({ 266 + pipe: "endpoint__http_metrics_by_region_14d__v0", 267 + parameters: z.object({ 268 + monitorId: z.string(), 269 + }), 270 + data: z.object({ 271 + region: z.enum(flyRegions), 272 + count: z.number().int(), 273 + ok: z.number().int(), 274 + p50Latency: z.number().nullable().default(0), 275 + p75Latency: z.number().nullable().default(0), 276 + p90Latency: z.number().nullable().default(0), 277 + p95Latency: z.number().nullable().default(0), 278 + p99Latency: z.number().nullable().default(0), 279 + }), 280 + opts: { cache: "no-store" }, 281 + }); 282 + } 283 + 284 + public get httpStatusWeekly() { 285 + return this.tb.buildPipe({ 286 + pipe: "endpoint__http_status_7d__v0", 287 + parameters: z.object({ 288 + monitorId: z.string(), 289 + }), 290 + data: z.object({ 291 + day: z.string().transform((val) => { 292 + // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 293 + return new Date(`${val} GMT`).toISOString(); 294 + }), 295 + count: z.number().default(0), 296 + ok: z.number().default(0), 297 + }), 298 + opts: { cache: "no-store" }, 299 + }); 300 + } 301 + 302 + public get httpStatus45d() { 303 + return this.tb.buildPipe({ 304 + pipe: "endpoint__http_status_45d__v0", 305 + parameters: z.object({ 306 + monitorId: z.string(), 307 + }), 308 + data: z.object({ 309 + day: z.string().transform((val) => { 310 + // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 311 + return new Date(`${val} GMT`).toISOString(); 312 + }), 313 + count: z.number().default(0), 314 + ok: z.number().default(0), 315 + }), 316 + opts: { cache: "no-store" }, 317 + }); 318 + } 319 + 320 + public get httpGetMonthly() { 321 + return this.tb.buildPipe({ 322 + pipe: "endpoint__http_get_30d__v0", 323 + parameters: z.object({ 324 + monitorId: z.string(), 325 + region: z.enum(flyRegions).optional(), 326 + cronTimestamp: z.number().int().optional(), 327 + }), 328 + data: z.object({ 329 + type: z.literal("http").default("http"), 330 + latency: z.number().int(), 331 + statusCode: z.number().int().nullable(), 332 + monitorId: z.string(), 333 + url: z.string().url(), 334 + error: z.coerce.boolean(), 335 + region: z.enum(flyRegions), 336 + cronTimestamp: z.number().int(), 337 + message: z.string().nullable(), 338 + headers: headersSchema, 339 + timing: timingSchema, 340 + assertions: z.string().nullable(), 341 + trigger: z.enum(triggers).nullable().default("cron"), 342 + timestamp: z.number(), 343 + workspaceId: z.string(), 344 + }), 345 + opts: { cache: "no-store" }, 346 + }); 347 + } 348 + 349 + // FIXME: rename to same convension 350 + public get getResultForOnDemandCheckHttp() { 351 + return this.tb.buildPipe({ 352 + pipe: "get_result_for_on_demand_check_http", 353 + parameters: z.object({ 354 + monitorId: z.number().int(), 355 + timestamp: z.number(), 356 + url: z.string(), 357 + }), 358 + data: z.object({ 359 + latency: z.number().int(), // in ms 360 + statusCode: z.number().int().nullable().default(null), 361 + monitorId: z.string().default(""), 362 + url: z.string().url().optional(), 363 + error: z 364 + .number() 365 + .default(0) 366 + .transform((val) => val !== 0), 367 + region: z.enum(flyRegions), 368 + timestamp: z.number().int().optional(), 369 + message: z.string().nullable().optional(), 370 + timing: timingSchema, 371 + // TODO: make sure to include all data! 372 + }), 373 + opts: { cache: "no-store" }, 374 + }); 375 + } 376 + // TODO: add tcpChartDaily, tcpChartWeekly 377 + 378 + public get tcpListDaily() { 379 + return this.tb.buildPipe({ 380 + pipe: "endpoint__tcp_list_1d__v0", 381 + parameters: z.object({ 382 + monitorId: z.string(), 383 + }), 384 + data: z.object({ 385 + type: z.literal("tcp").default("tcp"), 386 + latency: z.number().int(), 387 + monitorId: z.coerce.string(), 388 + error: z.coerce.boolean(), 389 + region: z.enum(flyRegions), 390 + cronTimestamp: z.number().int(), 391 + trigger: z.enum(triggers).nullable().default("cron"), 392 + timestamp: z.number(), 393 + workspaceId: z.coerce.string(), 394 + }), 395 + opts: { cache: "no-store" }, 396 + }); 397 + } 398 + 399 + public get tcpListWeekly() { 400 + return this.tb.buildPipe({ 401 + pipe: "endpoint__tcp_list_7d__v0", 402 + parameters: z.object({ 403 + monitorId: z.string(), 404 + }), 405 + data: z.object({ 406 + type: z.literal("tcp").default("tcp"), 407 + latency: z.number().int(), 408 + monitorId: z.coerce.string(), 409 + error: z.coerce.boolean(), 410 + region: z.enum(flyRegions), 411 + cronTimestamp: z.number().int(), 412 + trigger: z.enum(triggers).nullable().default("cron"), 413 + timestamp: z.number(), 414 + workspaceId: z.coerce.string(), 415 + }), 416 + opts: { cache: "no-store" }, 417 + }); 418 + } 419 + 420 + public get tcpListBiweekly() { 421 + return this.tb.buildPipe({ 422 + pipe: "endpoint__tcp_list_14d__v0", 423 + parameters: z.object({ 424 + monitorId: z.string(), 425 + }), 426 + data: z.object({ 427 + type: z.literal("tcp").default("tcp"), 428 + latency: z.number().int(), 429 + monitorId: z.coerce.string(), 430 + error: z.coerce.boolean(), 431 + region: z.enum(flyRegions), 432 + cronTimestamp: z.number().int(), 433 + trigger: z.enum(triggers).nullable().default("cron"), 434 + timestamp: z.number(), 435 + workspaceId: z.coerce.string(), 436 + }), 437 + opts: { cache: "no-store" }, 438 + }); 439 + } 440 + 441 + public get tcpMetricsDaily() { 442 + return this.tb.buildPipe({ 443 + pipe: "endpoint__tcp_metrics_1d__v0", 444 + parameters: z.object({ 445 + interval: z.number().int().optional(), 446 + monitorId: z.string(), 447 + }), 448 + data: z.object({ 449 + p50Latency: z.number().nullable().default(0), 450 + p75Latency: z.number().nullable().default(0), 451 + p90Latency: z.number().nullable().default(0), 452 + p95Latency: z.number().nullable().default(0), 453 + p99Latency: z.number().nullable().default(0), 454 + count: z.number().int(), 455 + ok: z.number().int(), 456 + lastTimestamp: z.number().int().nullable(), 457 + }), 458 + opts: { cache: "no-store" }, 459 + }); 460 + } 461 + 462 + public get tcpMetricsWeekly() { 463 + return this.tb.buildPipe({ 464 + pipe: "endpoint__tcp_metrics_7d__v0", 465 + parameters: z.object({ 466 + interval: z.number().int().optional(), 467 + monitorId: z.string(), 468 + }), 469 + data: z.object({ 470 + p50Latency: z.number().nullable().default(0), 471 + p75Latency: z.number().nullable().default(0), 472 + p90Latency: z.number().nullable().default(0), 473 + p95Latency: z.number().nullable().default(0), 474 + p99Latency: z.number().nullable().default(0), 475 + count: z.number().int(), 476 + ok: z.number().int(), 477 + lastTimestamp: z.number().int().nullable(), 478 + }), 479 + opts: { cache: "no-store" }, 480 + }); 481 + } 482 + 483 + public get tcpMetricsBiweekly() { 484 + return this.tb.buildPipe({ 485 + pipe: "endpoint__tcp_metrics_14d__v0", 486 + parameters: z.object({ 487 + interval: z.number().int().optional(), 488 + monitorId: z.string(), 489 + }), 490 + data: z.object({ 491 + p50Latency: z.number().nullable().default(0), 492 + p75Latency: z.number().nullable().default(0), 493 + p90Latency: z.number().nullable().default(0), 494 + p95Latency: z.number().nullable().default(0), 495 + p99Latency: z.number().nullable().default(0), 496 + count: z.number().int(), 497 + ok: z.number().int(), 498 + lastTimestamp: z.number().int().nullable(), 499 + }), 500 + opts: { cache: "no-store" }, 501 + }); 502 + } 503 + 504 + public get tcpMetricsByIntervalDaily() { 505 + return this.tb.buildPipe({ 506 + pipe: "endpoint__tcp_metrics_by_interval_1d__v0", 507 + parameters: z.object({ 508 + interval: z.number().int().optional(), 509 + monitorId: z.string(), 510 + }), 511 + data: z.object({ 512 + region: z.enum(flyRegions), 513 + timestamp: z.number().int(), 514 + p50Latency: z.number().nullable().default(0), 515 + p75Latency: z.number().nullable().default(0), 516 + p90Latency: z.number().nullable().default(0), 517 + p95Latency: z.number().nullable().default(0), 518 + p99Latency: z.number().nullable().default(0), 519 + }), 520 + opts: { cache: "no-store" }, 521 + }); 522 + } 523 + 524 + public get tcpMetricsByIntervalWeekly() { 525 + return this.tb.buildPipe({ 526 + pipe: "endpoint__tcp_metrics_by_interval_7d__v0", 527 + parameters: z.object({ 528 + interval: z.number().int().optional(), 529 + monitorId: z.string(), 530 + }), 531 + data: z.object({ 532 + region: z.enum(flyRegions), 533 + timestamp: z.number().int(), 534 + p50Latency: z.number().nullable().default(0), 535 + p75Latency: z.number().nullable().default(0), 536 + p90Latency: z.number().nullable().default(0), 537 + p95Latency: z.number().nullable().default(0), 538 + p99Latency: z.number().nullable().default(0), 539 + }), 540 + opts: { cache: "no-store" }, 541 + }); 542 + } 543 + 544 + public get tcpMetricsByIntervalBiweekly() { 545 + return this.tb.buildPipe({ 546 + pipe: "endpoint__tcp_metrics_by_interval_14d__v0", 547 + parameters: z.object({ 548 + interval: z.number().int().optional(), 549 + monitorId: z.string(), 550 + }), 551 + data: z.object({ 552 + region: z.enum(flyRegions), 553 + timestamp: z.number().int(), 554 + p50Latency: z.number().nullable().default(0), 555 + p75Latency: z.number().nullable().default(0), 556 + p90Latency: z.number().nullable().default(0), 557 + p95Latency: z.number().nullable().default(0), 558 + p99Latency: z.number().nullable().default(0), 559 + }), 560 + opts: { cache: "no-store" }, 561 + }); 562 + } 563 + 564 + public get tcpMetricsByRegionDaily() { 565 + return this.tb.buildPipe({ 566 + pipe: "endpoint__tcp_metrics_by_region_1d__v0", 567 + parameters: z.object({ 568 + monitorId: z.string(), 569 + }), 570 + data: z.object({ 571 + region: z.enum(flyRegions), 572 + count: z.number().int(), 573 + ok: z.number().int(), 574 + p50Latency: z.number().nullable().default(0), 575 + p75Latency: z.number().nullable().default(0), 576 + p90Latency: z.number().nullable().default(0), 577 + p95Latency: z.number().nullable().default(0), 578 + p99Latency: z.number().nullable().default(0), 579 + }), 580 + opts: { cache: "no-store" }, 581 + }); 582 + } 583 + 584 + public get tcpMetricsByRegionWeekly() { 585 + return this.tb.buildPipe({ 586 + pipe: "endpoint__tcp_metrics_by_region_7d__v0", 587 + parameters: z.object({ 588 + monitorId: z.string(), 589 + }), 590 + data: z.object({ 591 + region: z.enum(flyRegions), 592 + count: z.number().int(), 593 + ok: z.number().int(), 594 + p50Latency: z.number().nullable().default(0), 595 + p75Latency: z.number().nullable().default(0), 596 + p90Latency: z.number().nullable().default(0), 597 + p95Latency: z.number().nullable().default(0), 598 + p99Latency: z.number().nullable().default(0), 599 + }), 600 + opts: { cache: "no-store" }, 601 + }); 602 + } 603 + 604 + public get tcpMetricsByRegionBiweekly() { 605 + return this.tb.buildPipe({ 606 + pipe: "endpoint__tcp_metrics_by_region_14d__v0", 607 + parameters: z.object({ 608 + monitorId: z.string(), 609 + }), 610 + data: z.object({ 611 + region: z.enum(flyRegions), 612 + count: z.number().int(), 613 + ok: z.number().int(), 614 + p50Latency: z.number().nullable().default(0), 615 + p75Latency: z.number().nullable().default(0), 616 + p90Latency: z.number().nullable().default(0), 617 + p95Latency: z.number().nullable().default(0), 618 + p99Latency: z.number().nullable().default(0), 619 + }), 620 + opts: { cache: "no-store" }, 621 + }); 622 + } 623 + 624 + public get tcpStatusWeekly() { 625 + return this.tb.buildPipe({ 626 + pipe: "endpoint__tcp_status_7d__v0", 627 + parameters: z.object({ 628 + monitorId: z.string(), 629 + }), 630 + data: z.object({ 631 + day: z.string().transform((val) => { 632 + // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 633 + return new Date(`${val} GMT`).toISOString(); 634 + }), 635 + count: z.number().default(0), 636 + ok: z.number().default(0), 637 + }), 638 + opts: { cache: "no-store" }, 639 + }); 640 + } 641 + 642 + public get tcpStatus45d() { 643 + return this.tb.buildPipe({ 644 + pipe: "endpoint__tcp_status_45d__v0", 645 + parameters: z.object({ 646 + monitorId: z.string(), 647 + }), 648 + data: z.object({ 649 + day: z.string().transform((val) => { 650 + // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 651 + return new Date(`${val} GMT`).toISOString(); 652 + }), 653 + count: z.number().default(0), 654 + ok: z.number().default(0), 655 + }), 656 + opts: { cache: "no-store" }, 657 + }); 658 + } 659 + 660 + public get tcpGetMonthly() { 661 + return this.tb.buildPipe({ 662 + pipe: "endpoint__tcp_get_30d__v0", 663 + parameters: z.object({ 664 + monitorId: z.string(), 665 + region: z.enum(flyRegions).optional(), 666 + cronTimestamp: z.number().int().optional(), 667 + }), 668 + data: z.object({ 669 + type: z.literal("tcp").default("tcp"), 670 + latency: z.number().int(), 671 + monitorId: z.string(), 672 + error: z.coerce.boolean(), 673 + region: z.enum(flyRegions), 674 + cronTimestamp: z.number().int(), 675 + trigger: z.enum(triggers).nullable().default("cron"), 676 + timestamp: z.number(), 677 + workspaceId: z.string(), 678 + }), 679 + opts: { cache: "no-store" }, 680 + }); 681 + } 60 682 }
-4
packages/tinybird/src/index.ts
··· 1 1 export * from "./client"; 2 - export * from "./validation"; 3 2 export * from "./audit-log"; 4 3 export * from "@chronark/zod-bird"; 5 - 6 - // OSTinybird: for specific pipes 7 - export * from "./os-client";
-514
packages/tinybird/src/os-client.ts
··· 1 - import { NoopTinybird, Tinybird } from "@chronark/zod-bird"; 2 - import { z } from "zod"; 3 - 4 - import { flyRegions } from "../../db/src/schema/constants"; 5 - 6 - import type { tbIngestWebVitalsArray } from "./validation"; 7 - import { 8 - responseRumPageQuery, 9 - sessionRumPageQuery, 10 - tbIngestWebVitals, 11 - } from "./validation"; 12 - 13 - const isProd = process.env.NODE_ENV === "production"; 14 - 15 - const DEV_CACHE = 3_600; // 1h 16 - 17 - const MIN_CACHE = isProd ? 60 : DEV_CACHE; // 60s 18 - const DEFAULT_CACHE = isProd ? 120 : DEV_CACHE; // 2min 19 - const _MAX_CACHE = 86_400; // 1d 20 - 21 - const VERSION = "v1"; 22 - 23 - export const latencySchema = z.object({ 24 - p50Latency: z.number().int().nullable(), 25 - p75Latency: z.number().int().nullable(), 26 - p90Latency: z.number().int().nullable(), 27 - p95Latency: z.number().int().nullable(), 28 - p99Latency: z.number().int().nullable(), 29 - }); 30 - 31 - export const timingSchema = z.object({ 32 - dnsStart: z.number(), 33 - dnsDone: z.number(), 34 - connectStart: z.number(), 35 - connectDone: z.number(), 36 - tlsHandshakeStart: z.number(), 37 - tlsHandshakeDone: z.number(), 38 - firstByteStart: z.number(), 39 - firstByteDone: z.number(), 40 - transferStart: z.number(), 41 - transferDone: z.number(), 42 - }); 43 - 44 - export class OSTinybird { 45 - private tb: Tinybird; 46 - 47 - // FIXME: use Tinybird instead with super(args) maybe 48 - // how about passing here the `opts: {revalidate}` to access it within the functions? 49 - constructor(private args: { token: string; baseUrl?: string | undefined }) { 50 - if (process.env.NODE_ENV === "development") { 51 - this.tb = new NoopTinybird(); 52 - } else { 53 - this.tb = new Tinybird(args); 54 - } 55 - } 56 - 57 - endpointChart(period: "1h" | "1d" | "3d" | "7d" | "14d") { 58 - const parameters = z.object({ 59 - interval: z.number().int().optional(), 60 - monitorId: z.string(), 61 - }); 62 - 63 - return async (props: z.infer<typeof parameters>) => { 64 - try { 65 - const res = await this.tb.buildPipe({ 66 - pipe: `__ttl_${period}_chart_get__${VERSION}`, 67 - parameters, 68 - data: z 69 - .object({ 70 - region: z.enum(flyRegions), 71 - timestamp: z.number().int(), 72 - }) 73 - .merge(latencySchema), 74 - opts: { 75 - cache: "no-store", 76 - // next: { 77 - // revalidate: DEFAULT_CACHE, 78 - // }, 79 - }, 80 - })(props); 81 - return res.data; 82 - } catch (e) { 83 - console.error(e); 84 - } 85 - }; 86 - } 87 - 88 - endpointChartAllRegions(period: "7d" | "14d") { 89 - const parameters = z.object({ 90 - monitorId: z.string(), 91 - }); 92 - 93 - return async (props: z.infer<typeof parameters>) => { 94 - try { 95 - const res = await this.tb.buildPipe({ 96 - pipe: `__ttl_${period}_chart_all_regions_get__${VERSION}`, // TODO: add pipe to @openstatus/tinybird 97 - parameters, 98 - data: z.object({ timestamp: z.number().int() }).merge(latencySchema), 99 - opts: { 100 - cache: "no-store", 101 - // next: { 102 - // revalidate: DEFAULT_CACHE, 103 - // }, 104 - }, 105 - })(props); 106 - return res.data; 107 - } catch (e) { 108 - console.error(e); 109 - } 110 - }; 111 - } 112 - 113 - endpointMetrics(period: "1h" | "1d" | "3d" | "7d" | "14d") { 114 - const parameters = z.object({ monitorId: z.string() }); 115 - 116 - return async ( 117 - props: z.infer<typeof parameters>, 118 - opts?: { 119 - cache?: RequestCache | undefined; 120 - revalidate: number | undefined; 121 - }, // RETHINK: not the best way to handle it 122 - ) => { 123 - try { 124 - const res = await this.tb.buildPipe({ 125 - pipe: `__ttl_${period}_metrics_get__${VERSION}`, 126 - parameters, 127 - data: z 128 - .object({ 129 - region: z.enum(flyRegions).default("ams"), // FIXME: default 130 - count: z.number().default(0), 131 - ok: z.number().default(0), 132 - lastTimestamp: z.number().int().nullable(), 133 - }) 134 - .merge(latencySchema), 135 - opts: { 136 - cache: opts?.cache, 137 - next: { 138 - revalidate: opts?.revalidate || DEFAULT_CACHE, 139 - }, 140 - }, 141 - })(props); 142 - return res.data; 143 - } catch (e) { 144 - console.error(e); 145 - } 146 - }; 147 - } 148 - 149 - endpointMetricsByRegion(period: "1h" | "1d" | "3d" | "7d" | "14d") { 150 - const parameters = z.object({ monitorId: z.string() }); 151 - 152 - return async (props: z.infer<typeof parameters>) => { 153 - try { 154 - const res = await this.tb.buildPipe({ 155 - pipe: `__ttl_${period}_metrics_get_by_region__${VERSION}`, 156 - parameters, 157 - data: z 158 - .object({ 159 - region: z.enum(flyRegions), 160 - count: z.number().default(0), 161 - ok: z.number().default(0), 162 - lastTimestamp: z.number().int().optional(), // FIXME: optional 163 - }) 164 - .merge(latencySchema), 165 - opts: { 166 - cache: "no-store", 167 - // next: { 168 - // revalidate: DEFAULT_CACHE, 169 - // }, 170 - }, 171 - })(props); 172 - return res.data; 173 - } catch (e) { 174 - console.error(e); 175 - } 176 - }; 177 - } 178 - 179 - endpointStatusPeriod( 180 - period: "7d" | "45d", 181 - timezone: "UTC" = "UTC", // "EST" | "PST" | "CET" 182 - ) { 183 - const parameters = z.object({ monitorId: z.string() }); 184 - 185 - return async ( 186 - props: z.infer<typeof parameters>, 187 - opts?: { 188 - cache?: RequestCache | undefined; 189 - revalidate: number | undefined; 190 - }, // RETHINK: not the best way to handle it 191 - ) => { 192 - try { 193 - const res = await this.tb.buildPipe({ 194 - pipe: `__ttl_${period}_count_${timezone.toLowerCase()}_get__${VERSION}`, 195 - parameters, 196 - data: z.object({ 197 - day: z.string().transform((val) => { 198 - // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 199 - return new Date(`${val} GMT`).toISOString(); 200 - }), 201 - count: z.number().default(0), 202 - ok: z.number().default(0), 203 - }), 204 - opts: { 205 - cache: opts?.cache, 206 - next: { 207 - revalidate: opts?.revalidate || DEFAULT_CACHE, 208 - }, 209 - }, 210 - })(props); 211 - return res.data; 212 - } catch (e) { 213 - console.error(e); 214 - } 215 - }; 216 - } 217 - 218 - endpointList(period: "1h" | "1d" | "3d" | "7d") { 219 - const parameters = z.object({ 220 - monitorId: z.string(), 221 - }); 222 - 223 - return async (props: z.infer<typeof parameters>) => { 224 - try { 225 - const res = await this.tb.buildPipe({ 226 - pipe: `__ttl_${period}_list_get__${VERSION}`, 227 - parameters, 228 - data: z.object({ 229 - latency: z.number().int(), // in ms 230 - monitorId: z.string(), 231 - region: z.enum(flyRegions), 232 - error: z 233 - .number() 234 - .default(0) 235 - .transform((val) => val !== 0), 236 - statusCode: z.number().int().nullable().default(null), 237 - timestamp: z.number().int(), 238 - url: z.string().url(), 239 - workspaceId: z.string(), 240 - cronTimestamp: z.number().int().nullable().default(Date.now()), 241 - assertions: z.string().nullable().optional(), 242 - trigger: z 243 - .enum(["cron", "api"]) 244 - .optional() 245 - .nullable() 246 - .default("cron"), 247 - }), 248 - opts: { 249 - cache: "no-store", 250 - // next: { 251 - // revalidate: DEFAULT_CACHE, 252 - // }, 253 - }, 254 - })(props); 255 - return res.data; 256 - } catch (e) { 257 - console.error(e); 258 - } 259 - }; 260 - } 261 - 262 - // FEATURE: include a simple Widget for the user to refresh the page or display on the top of the page 263 - // type: "workspace" | "monitor" 264 - endpointLastCronTimestamp(type: "workspace") { 265 - const parameters = z.object({ workspaceId: z.string() }); 266 - 267 - return async (props: z.infer<typeof parameters>) => { 268 - try { 269 - const res = await this.tb.buildPipe({ 270 - pipe: `__ttl_1h_last_timestamp_${type}_get__${VERSION}`, 271 - parameters, 272 - data: z.object({ cronTimestamp: z.number().int() }), 273 - opts: { 274 - next: { 275 - revalidate: MIN_CACHE, 276 - }, 277 - }, 278 - })(props); 279 - return res.data; 280 - } catch (e) { 281 - console.error(e); 282 - } 283 - }; 284 - } 285 - 286 - endpointResponseDetails(period: "7d" | "45d") { 287 - const parameters = z.object({ 288 - monitorId: z.string().default("").optional(), 289 - region: z.enum(flyRegions).optional(), 290 - cronTimestamp: z.number().int().optional(), 291 - }); 292 - 293 - return async (props: z.infer<typeof parameters>) => { 294 - try { 295 - const res = await this.tb.buildPipe({ 296 - pipe: `__ttl_${period}_all_details_get__${VERSION}`, 297 - parameters, 298 - data: z.object({ 299 - latency: z.number().int(), // in ms 300 - statusCode: z.number().int().nullable().default(null), 301 - monitorId: z.string().default(""), 302 - url: z.string().url().optional(), 303 - error: z 304 - .number() 305 - .default(0) 306 - .transform((val) => val !== 0), 307 - region: z.enum(flyRegions), 308 - cronTimestamp: z.number().int().optional(), 309 - message: z.string().nullable().optional(), 310 - headers: z 311 - .string() 312 - .nullable() 313 - .optional() 314 - .transform((val) => { 315 - if (!val) return null; 316 - const value = z 317 - .record(z.string(), z.string()) 318 - .safeParse(JSON.parse(val)); 319 - if (value.success) return value.data; 320 - return null; 321 - }), 322 - timing: z 323 - .string() 324 - .nullable() 325 - .optional() 326 - .transform((val) => { 327 - if (!val) return null; 328 - const value = timingSchema.safeParse(JSON.parse(val)); 329 - if (value.success) return value.data; 330 - return null; 331 - }), 332 - assertions: z.string().nullable().optional(), // REMINDER: maybe include Assertions.serialize here 333 - }), 334 - opts: { 335 - cache: "no-store", 336 - // next: { 337 - // revalidate: MAX_CACHE, 338 - // }, 339 - }, 340 - })(props); 341 - return res.data; 342 - } catch (e) { 343 - console.error(e); 344 - } 345 - }; 346 - } 347 - ingestWebVitals(data: z.infer<typeof tbIngestWebVitalsArray>) { 348 - return this.tb.buildIngestEndpoint({ 349 - datasource: "web_vitals__v0", 350 - event: tbIngestWebVitals, 351 - })(data); 352 - } 353 - 354 - applicationRUMMetrics() { 355 - const parameters = z.object({ 356 - dsn: z.string(), 357 - period: z.enum(["24h", "7d", "30d"]), 358 - }); 359 - 360 - return async (props: z.infer<typeof parameters>) => { 361 - try { 362 - const res = await this.tb.buildPipe({ 363 - pipe: "rum_total_query", 364 - parameters, 365 - data: z.object({ 366 - cls: z.number(), 367 - fcp: z.number(), 368 - // fid: z.number(), 369 - lcp: z.number(), 370 - inp: z.number(), 371 - ttfb: z.number(), 372 - }), 373 - opts: { 374 - next: { 375 - revalidate: MIN_CACHE, 376 - }, 377 - }, 378 - })(props); 379 - return res.data[0]; 380 - } catch (e) { 381 - console.error(e); 382 - } 383 - }; 384 - } 385 - applicationRUMMetricsPerPage() { 386 - const parameters = z.object({ 387 - dsn: z.string(), 388 - period: z.enum(["24h", "7d", "30d"]), 389 - }); 390 - return async (props: z.infer<typeof parameters>) => { 391 - try { 392 - const res = await this.tb.buildPipe({ 393 - pipe: "rum_page_query", 394 - parameters, 395 - data: responseRumPageQuery, 396 - opts: { 397 - next: { 398 - revalidate: MIN_CACHE, 399 - }, 400 - }, 401 - })(props); 402 - return res.data; 403 - } catch (e) { 404 - console.error(e); 405 - } 406 - }; 407 - } 408 - applicationSessionMetricsPerPath() { 409 - const parameters = z.object({ 410 - dsn: z.string(), 411 - period: z.enum(["24h", "7d", "30d"]), 412 - path: z.string(), 413 - }); 414 - return async (props: z.infer<typeof parameters>) => { 415 - try { 416 - const res = await this.tb.buildPipe({ 417 - pipe: "rum_page_query_per_path", 418 - parameters, 419 - data: sessionRumPageQuery, 420 - opts: { 421 - next: { 422 - revalidate: MIN_CACHE, 423 - }, 424 - }, 425 - })(props); 426 - return res.data; 427 - } catch (e) { 428 - console.error(e); 429 - } 430 - }; 431 - } 432 - applicationRUMMetricsForPath() { 433 - const parameters = z.object({ 434 - dsn: z.string(), 435 - path: z.string(), 436 - period: z.enum(["24h", "7d", "30d"]), 437 - }); 438 - return async (props: z.infer<typeof parameters>) => { 439 - try { 440 - const res = await this.tb.buildPipe({ 441 - pipe: "rum_total_query_per_path", 442 - parameters, 443 - data: z.object({ 444 - cls: z.number(), 445 - fcp: z.number(), 446 - // fid: z.number(), 447 - lcp: z.number(), 448 - inp: z.number(), 449 - ttfb: z.number(), 450 - }), 451 - opts: { 452 - next: { 453 - revalidate: MIN_CACHE, 454 - }, 455 - }, 456 - })(props); 457 - return res.data[0]; 458 - } catch (e) { 459 - console.error(e); 460 - } 461 - }; 462 - } 463 - 464 - getResultForOnDemandCheckHttp() { 465 - const parameters = z.object({ 466 - monitorId: z.number().int(), 467 - timestamp: z.number(), 468 - url: z.string(), 469 - }); 470 - return async (props: z.infer<typeof parameters>) => { 471 - try { 472 - const res = await this.tb.buildPipe({ 473 - pipe: "get_result_for_on_demand_check_http", 474 - parameters, 475 - data: z.object({ 476 - latency: z.number().int(), // in ms 477 - statusCode: z.number().int().nullable().default(null), 478 - monitorId: z.string().default(""), 479 - url: z.string().url().optional(), 480 - error: z 481 - .number() 482 - .default(0) 483 - .transform((val) => val !== 0), 484 - region: z.enum(flyRegions), 485 - timestamp: z.number().int().optional(), 486 - message: z.string().nullable().optional(), 487 - timing: z 488 - .string() 489 - .nullable() 490 - .optional() 491 - .transform((val) => { 492 - if (!val) return null; 493 - const value = timingSchema.safeParse(JSON.parse(val)); 494 - if (value.success) return value.data; 495 - return null; 496 - }), 497 - }), 498 - opts: { 499 - next: { 500 - revalidate: MIN_CACHE, 501 - }, 502 - }, 503 - })(props); 504 - return res.data; 505 - } catch (e) { 506 - console.error(e); 507 - } 508 - }; 509 - } 510 - } 511 - 512 - /** 513 - * TODO: if it would be possible to... 514 - */
+48
packages/tinybird/src/schema.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const jobTypes = ["http", "tcp", "imcp", "udp", "dns", "ssl"] as const; 4 + export const jobTypeEnum = z.enum(jobTypes); 5 + export type JobType = z.infer<typeof jobTypeEnum>; 6 + 7 + export const periods = ["1h", "1d", "3d", "7d", "14d", "45d"] as const; 8 + export const periodEnum = z.enum(periods); 9 + export type Period = z.infer<typeof periodEnum>; 10 + 11 + export const triggers = ["cron", "api"] as const; 12 + export const triggerEnum = z.enum(triggers); 13 + export type Trigger = z.infer<typeof triggerEnum>; 14 + 15 + export const headersSchema = z 16 + .string() 17 + .nullable() 18 + .optional() 19 + .transform((val) => { 20 + if (!val) return null; 21 + const value = z.record(z.string(), z.string()).safeParse(JSON.parse(val)); 22 + if (value.success) return value.data; 23 + return null; 24 + }); 25 + 26 + export const httpTimingSchema = z.object({ 27 + dnsStart: z.number(), 28 + dnsDone: z.number(), 29 + connectStart: z.number(), 30 + connectDone: z.number(), 31 + tlsHandshakeStart: z.number(), 32 + tlsHandshakeDone: z.number(), 33 + firstByteStart: z.number(), 34 + firstByteDone: z.number(), 35 + transferStart: z.number(), 36 + transferDone: z.number(), 37 + }); 38 + 39 + export const timingSchema = z 40 + .string() 41 + .nullable() 42 + .optional() 43 + .transform((val) => { 44 + if (!val) return null; 45 + const value = httpTimingSchema.safeParse(JSON.parse(val)); 46 + if (value.success) return value.data; 47 + return null; 48 + });
-308
packages/tinybird/src/validation.ts
··· 1 - import * as z from "zod"; 2 - import { monitorFlyRegionSchema } from "../../db/src/schema/constants"; 3 - import type { flyRegions } from "../../db/src/schema/constants"; 4 - 5 - export const tbIngestWebVitals = z.object({ 6 - dsn: z.string(), 7 - href: z.string(), 8 - speed: z.string(), 9 - path: z.string(), 10 - screen: z.string(), 11 - name: z.string(), 12 - rating: z.string().optional(), 13 - value: z.number(), 14 - id: z.string(), 15 - session_id: z.string(), 16 - browser: z.string().default(""), 17 - city: z.string().default(""), 18 - country: z.string().default(""), 19 - continent: z.string().default(""), 20 - device: z.string().default(""), 21 - region_code: z.string().default(""), 22 - timezone: z.string().default(""), 23 - os: z.string(), 24 - timestamp: z.number().int(), 25 - }); 26 - 27 - export const responseRumPageQuery = z.object({ 28 - path: z.string(), 29 - totalSession: z.number(), 30 - cls: z.number(), 31 - fcp: z.number(), 32 - // fid: z.number(), 33 - inp: z.number(), 34 - lcp: z.number(), 35 - ttfb: z.number(), 36 - }); 37 - 38 - export const sessionRumPageQuery = z.object({ 39 - session_id: z.string(), 40 - cls: z.number(), 41 - fcp: z.number(), 42 - // fid: z.number(), 43 - inp: z.number(), 44 - lcp: z.number(), 45 - ttfb: z.number(), 46 - }); 47 - 48 - export const tbIngestWebVitalsArray = z.array(tbIngestWebVitals); 49 - /** 50 - * Values for the datasource ping_response 51 - */ 52 - export const tbIngestPingResponse = z.object({ 53 - workspaceId: z.string(), 54 - monitorId: z.string(), 55 - timestamp: z.number().int(), 56 - statusCode: z.number().int().nullable().optional(), 57 - latency: z.number(), // in ms 58 - cronTimestamp: z.number().int().optional().nullable().default(Date.now()), 59 - url: z.string().url(), 60 - region: z.string().min(3).max(4), // REMINDER: won't work on fy 61 - message: z.string().nullable().optional(), 62 - headers: z.record(z.string(), z.string()).nullable().optional(), 63 - timing: z 64 - .object({ 65 - dnsStart: z.number().int(), 66 - dnsDone: z.number().int(), 67 - connectStart: z.number().int(), 68 - connectDone: z.number().int(), 69 - tlsHandshakeStart: z.number().int(), 70 - tlsHandshakeDone: z.number().int(), 71 - firstByteStart: z.number().int(), 72 - firstByteDone: z.number().int(), 73 - transferStart: z.number().int(), 74 - transferDone: z.number().int(), 75 - }) 76 - .nullable() 77 - .optional(), 78 - }); 79 - 80 - /** 81 - * Values from the pipe response_list 82 - */ 83 - export const tbBuildResponseList = z.object({ 84 - workspaceId: z.string(), 85 - monitorId: z.string(), 86 - timestamp: z.number().int(), 87 - error: z 88 - .number() 89 - .default(0) 90 - .transform((val) => val !== 0), 91 - statusCode: z.number().int().nullable().default(null), 92 - latency: z.number().int(), // in ms 93 - cronTimestamp: z.number().int().nullable().default(Date.now()), 94 - url: z.string().url(), 95 - region: monitorFlyRegionSchema, 96 - message: z.string().nullable().optional(), 97 - assertions: z.string().nullable().optional(), 98 - trigger: z.enum(["cron", "api"]).optional().nullable().default("cron"), 99 - }); 100 - 101 - /** 102 - * Params for pipe response_list 103 - */ 104 - export const tbParameterResponseList = z.object({ 105 - monitorId: z.string().default(""), // REMINDER: remove default once alpha 106 - url: z.string().url().optional(), 107 - fromDate: z.number().int().default(0), // always start from a date 108 - toDate: z.number().int().optional(), 109 - limit: z.number().int().optional().default(7500), // one day has 2448 pings (17 (regions) * 6 (per hour) * 24) * 3 days for historical data 110 - region: monitorFlyRegionSchema.optional(), 111 - cronTimestamp: z.number().int().optional(), 112 - }); 113 - 114 - /** 115 - * Params for pipe response_details 116 - */ 117 - export const tbParameterResponseDetails = tbParameterResponseList.pick({ 118 - monitorId: true, 119 - url: true, 120 - cronTimestamp: true, 121 - region: true, 122 - }); 123 - 124 - export const responseHeadersSchema = z.record(z.string(), z.string()); 125 - export const responseTimingSchema = z.object({ 126 - dnsStart: z.number(), 127 - dnsDone: z.number(), 128 - connectStart: z.number(), 129 - connectDone: z.number(), 130 - tlsHandshakeStart: z.number(), 131 - tlsHandshakeDone: z.number(), 132 - firstByteStart: z.number(), 133 - firstByteDone: z.number(), 134 - transferStart: z.number(), 135 - transferDone: z.number(), 136 - }); 137 - 138 - /** 139 - * Values from the pipe response_details 140 - */ 141 - export const tbBuildResponseDetails = tbBuildResponseList.extend({ 142 - message: z.string().nullable().optional(), 143 - headers: z 144 - .string() 145 - .nullable() 146 - .optional() 147 - .transform((val) => { 148 - if (!val) return null; 149 - const value = responseHeadersSchema.safeParse(JSON.parse(val)); 150 - if (value.success) return value.data; 151 - return null; 152 - }), 153 - timing: z 154 - .string() 155 - .nullable() 156 - .optional() 157 - .transform((val) => { 158 - if (!val) return null; 159 - const value = responseTimingSchema.safeParse(JSON.parse(val)); 160 - if (value.success) return value.data; 161 - return null; 162 - }), 163 - }); 164 - 165 - export const latencyMetrics = z.object({ 166 - p50Latency: z.number().int().nullable(), 167 - p75Latency: z.number().int().nullable(), 168 - p90Latency: z.number().int().nullable(), 169 - p95Latency: z.number().int().nullable(), 170 - p99Latency: z.number().int().nullable(), 171 - }); 172 - 173 - /** 174 - * Values from pipe response_graph 175 - */ 176 - export const tbBuildResponseGraph = z 177 - .object({ 178 - region: monitorFlyRegionSchema, 179 - timestamp: z.number().int(), 180 - }) 181 - .merge(latencyMetrics); 182 - 183 - /** 184 - * Params for pipe response_graph 185 - */ 186 - export const tbParameterResponseGraph = z.object({ 187 - monitorId: z.string().default(""), 188 - url: z.string().url().optional(), 189 - interval: z.number().int().default(10), 190 - fromDate: z.number().int().default(0), 191 - toDate: z.number().int().optional(), 192 - }); 193 - 194 - /** 195 - * Params for pipe status_timezone 196 - */ 197 - export const tbParameterMonitorList = z.object({ 198 - monitorId: z.string(), 199 - url: z.string().url().optional(), 200 - timezone: z.string().optional(), 201 - limit: z.number().int().default(46).optional(), // 46 days 202 - }); 203 - 204 - /** 205 - * Values from the pipe status_timezone 206 - */ 207 - export const tbBuildMonitorList = z.object({ 208 - count: z.number().int(), 209 - ok: z.number().int(), 210 - day: z.string().transform((val) => { 211 - // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 212 - return new Date(`${val} GMT`).toISOString(); 213 - }), 214 - }); 215 - 216 - /** 217 - * Params for pipe home_stats 218 - */ 219 - export const tbParameterHomeStats = z.object({ 220 - cronTimestamp: z.number().int().optional(), 221 - period: z.enum(["total", "1h", "10m"]).optional(), 222 - }); 223 - 224 - /** 225 - * Values from the pipe home_stats 226 - */ 227 - export const tbBuildHomeStats = z.object({ 228 - count: z.number().int(), 229 - }); 230 - 231 - /** 232 - * Params for pipe public_status (used for our API /public/status/[slug]) 233 - */ 234 - export const tbParameterPublicStatus = z.object({ 235 - monitorId: z.string(), 236 - url: z.string().url().optional(), 237 - limit: z.number().int().default(5).optional(), // 5 last cronTimestamps 238 - }); 239 - 240 - /** 241 - * Values from the pipe public_status (used for our API /public/status/[slug]) 242 - */ 243 - export const tbBuildPublicStatus = z.object({ 244 - ok: z.number().int(), 245 - count: z.number().int(), 246 - cronTimestamp: z.number().int(), 247 - }); 248 - 249 - /** 250 - * Params for pipe response_time_metrics 251 - */ 252 - export const tbParameterResponseTimeMetrics = z.object({ 253 - monitorId: z.string(), 254 - url: z.string().url().optional(), 255 - interval: z.number().int().default(24), // 24 hours 256 - }); 257 - 258 - /** 259 - * Values from the pipe response_time_metrics 260 - */ 261 - export const tbBuildResponseTimeMetrics = z 262 - .object({ 263 - count: z.number().int(), 264 - ok: z.number().int(), 265 - lastTimestamp: z.number().int().nullable().optional(), 266 - }) 267 - .merge(latencyMetrics); 268 - 269 - /** 270 - * Params for pipe response_time_metrics_by_region 271 - */ 272 - export const tbParameterResponseTimeMetricsByRegion = z.object({ 273 - monitorId: z.string(), 274 - url: z.string().url().optional(), 275 - interval: z.number().int().default(24), // 24 hours 276 - }); 277 - 278 - /** 279 - * Values from the pipe response_time_metrics_by_region 280 - */ 281 - export const tbBuildResponseTimeMetricsByRegion = z 282 - .object({ 283 - region: monitorFlyRegionSchema, 284 - }) 285 - .merge(latencyMetrics); 286 - 287 - export type Ping = z.infer<typeof tbBuildResponseList>; 288 - export type Region = (typeof flyRegions)[number]; // TODO: rename type AvailabeRegion 289 - export type Monitor = z.infer<typeof tbBuildMonitorList>; 290 - export type HomeStats = z.infer<typeof tbBuildHomeStats>; 291 - export type ResponseGraph = z.infer<typeof tbBuildResponseGraph>; // TODO: rename to ResponseQuantileChart 292 - export type ResponseListParams = z.infer<typeof tbParameterResponseList>; 293 - export type ResponseGraphParams = z.infer<typeof tbParameterResponseGraph>; 294 - export type MonitorListParams = z.infer<typeof tbParameterMonitorList>; 295 - export type HomeStatsParams = z.infer<typeof tbParameterHomeStats>; 296 - export type ResponseDetails = z.infer<typeof tbBuildResponseDetails>; 297 - export type ResponseDetailsParams = z.infer<typeof tbParameterResponseDetails>; 298 - export type LatencyMetric = keyof z.infer<typeof latencyMetrics>; 299 - export type ResponseTimeMetrics = z.infer<typeof tbBuildResponseTimeMetrics>; 300 - export type ResponseTimeMetricsParams = z.infer< 301 - typeof tbParameterResponseTimeMetrics 302 - >; 303 - export type ResponseTimeMetricsByRegion = z.infer< 304 - typeof tbBuildResponseTimeMetricsByRegion 305 - >; 306 - export type ResponseTimeMetricsByRegionParams = z.infer< 307 - typeof tbParameterResponseTimeMetricsByRegion 308 - >;
+7 -4
packages/tracker/src/tracker.ts
··· 4 4 StatusReport, 5 5 StatusReportUpdate, 6 6 } from "@openstatus/db/src/schema"; 7 - import type { Monitor } from "@openstatus/tinybird"; 8 7 9 8 import { isInBlacklist } from "./blacklist"; 10 9 import { classNames, statusDetails } from "./config"; ··· 12 11 import { Status } from "./types"; 13 12 import { endOfDay, isSameDay, startOfDay } from "./utils"; 14 13 15 - type Monitors = Monitor[]; 14 + type Monitor = { 15 + count: number; 16 + ok: number; 17 + day: string; 18 + }; 16 19 type StatusReports = (StatusReport & { 17 20 statusReportUpdates?: StatusReportUpdate[]; 18 21 })[]; ··· 26 29 * StatusPage with multiple Monitors. 27 30 */ 28 31 export class Tracker { 29 - private data: Monitors = []; 32 + private data: Monitor[] = []; 30 33 private statusReports: StatusReports = []; 31 34 private incidents: Incidents = []; 32 35 private maintenances: Maintenances = []; 33 36 34 37 constructor(arg: { 35 - data?: Monitors; 38 + data?: Monitor[]; 36 39 statusReports?: StatusReports; 37 40 incidents?: Incidents; 38 41 maintenances?: Maintenance[];