Openstatus www.openstatus.dev

feat: build tb pipes (#697)

* feat: build new pipes

* refactor: remove unused interception route

* feat: improve data handling on pipe

* wip: we are slowly getting there

* wip: allow user to click for response details

* feat: add last timestamp endpoint

* chore: add dev script

* wip: tinybird trpc router

* random: refresh-widget

* chore: add search params preset component

* feat: add tb pipes and datasources

authored by

Maximilian Kaske and committed by
GitHub
2fa8d9ff 4d888799

+1434 -462
+19 -12
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/page.tsx
··· 1 1 import * as React from "react"; 2 2 import Link from "next/link"; 3 3 4 + import { OSTinybird } from "@openstatus/tinybird"; 4 5 import { Button } from "@openstatus/ui"; 5 6 6 7 import { EmptyState } from "@/components/dashboard/empty-state"; 7 8 import { Limit } from "@/components/dashboard/limit"; 8 9 import { columns } from "@/components/data-table/monitor/columns"; 9 10 import { DataTable } from "@/components/data-table/monitor/data-table"; 10 - import { getMonitorListData, getResponseTimeMetricsData } from "@/lib/tb"; 11 - import { convertTimezoneToGMT } from "@/lib/timezone"; 11 + import { env } from "@/env"; 12 12 import { api } from "@/trpc/server"; 13 + 14 + // import { RefreshWidget } from "../_components/refresh-widget"; 15 + 16 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 13 17 14 18 export default async function MonitorPage() { 15 19 const monitors = await api.monitor.getMonitorsByWorkspace.query(); ··· 29 33 /> 30 34 ); 31 35 32 - const gmt = convertTimezoneToGMT(); 33 - 34 36 const _incidents = await api.incident.getIncidentsByWorkspace.query(); 35 37 36 38 // maybe not very efficient? 37 39 // use Suspense and Client call instead? 38 40 const monitorsWithData = await Promise.all( 39 41 monitors.map(async (monitor) => { 40 - const metrics = await getResponseTimeMetricsData({ 42 + const metrics = await tb.endpointMetrics("1d")({ 41 43 monitorId: String(monitor.id), 42 44 url: monitor.url, 43 - interval: 24, 44 45 }); 45 46 46 - const data = await getMonitorListData({ 47 + const data = await tb.endpointStatusPeriod("7d")({ 47 48 monitorId: String(monitor.id), 48 - url: monitor.url, 49 - timezone: gmt, 49 + url: monitor.url, // FIXME: we should avoid adding url to the parameters 50 50 }); 51 51 52 - const [current, _] = metrics 53 - ? metrics.sort((a, b) => (a.time - b.time < 0 ? 1 : -1)) 54 - : [undefined]; 52 + const [current] = metrics?.sort((a, b) => 53 + (a.lastTimestamp || 0) - (b.lastTimestamp || 0) < 0 ? 1 : -1, 54 + ) || [undefined]; 55 55 56 56 const incidents = _incidents.filter( 57 57 (incident) => incident.monitorId === monitor.id, ··· 61 61 }), 62 62 ); 63 63 64 + // const lastCronTimestamp = monitorsWithData?.reduce((prev, acc) => { 65 + // const lastTimestamp = acc.metrics?.lastTimestamp || 0; 66 + // if (lastTimestamp > prev) return lastTimestamp; 67 + // return prev; 68 + // }, 0); 69 + 64 70 return ( 65 71 <> 66 72 <DataTable columns={columns} data={monitorsWithData} /> 67 73 {isLimitReached ? <Limit /> : null} 74 + {/* <RefreshWidget defaultValue={lastCronTimestamp} /> */} 68 75 </> 69 76 ); 70 77 }
+20 -37
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/date-picker-preset.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 - import { usePathname, useRouter } from "next/navigation"; 5 - import { CalendarIcon } from "lucide-react"; 6 4 7 - import { 8 - Select, 9 - SelectContent, 10 - SelectItem, 11 - SelectTrigger, 12 - SelectValue, 13 - } from "@openstatus/ui"; 14 - 15 - import useUpdateSearchParams from "@/hooks/use-update-search-params"; 16 - import { periodFormatter, periods } from "../utils"; 5 + import { periodFormatter } from "../utils"; 17 6 import type { Period } from "../utils"; 7 + import { SearchParamsPreset } from "./search-params-preset"; 18 8 19 - export function DatePickerPreset({ period }: { period: Period }) { 20 - const router = useRouter(); 21 - const pathname = usePathname(); 22 - const updateSearchParams = useUpdateSearchParams(); 23 - 24 - function onSelect(value: Period) { 25 - const searchParams = updateSearchParams({ period: value }); 26 - router.replace(`${pathname}?${searchParams}`, { scroll: false }); 27 - } 28 - 9 + export function DatePickerPreset({ 10 + disabled, 11 + defaultValue, 12 + values, 13 + }: { 14 + disabled?: boolean; 15 + defaultValue?: Period; 16 + values: readonly Period[]; 17 + }) { 29 18 return ( 30 - <Select defaultValue={period} onValueChange={onSelect}> 31 - <SelectTrigger className="bg-background w-[150px] text-left" id="period"> 32 - <span className="flex items-center gap-2"> 33 - <CalendarIcon className="h-4 w-4" /> 34 - <SelectValue placeholder="Pick a range" /> 35 - </span> 36 - </SelectTrigger> 37 - <SelectContent> 38 - {periods.map((period) => ( 39 - <SelectItem key={period} value={period}> 40 - {periodFormatter(period)} 41 - </SelectItem> 42 - ))} 43 - </SelectContent> 44 - </Select> 19 + <SearchParamsPreset 20 + disabled={disabled} 21 + defaultValue={defaultValue} 22 + values={values} 23 + searchParam="period" 24 + icon="calendar" 25 + placeholder="Pick a range" 26 + formatter={periodFormatter} 27 + /> 45 28 ); 46 29 }
+11 -40
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/interval-preset.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 - import { usePathname, useRouter } from "next/navigation"; 5 - import { HelpCircle, Hourglass } from "lucide-react"; 4 + import { HelpCircle } from "lucide-react"; 6 5 7 6 import { 8 7 Label, 9 8 Popover, 10 9 PopoverContent, 11 10 PopoverTrigger, 12 - Select, 13 - SelectContent, 14 - SelectItem, 15 - SelectTrigger, 16 - SelectValue, 17 11 Separator, 18 12 } from "@openstatus/ui"; 19 13 20 - import useUpdateSearchParams from "@/hooks/use-update-search-params"; 21 - import { cn } from "@/lib/utils"; 22 14 import { intervals } from "../utils"; 23 15 import type { Interval } from "../utils"; 24 - 25 - export function IntervalPreset({ 26 - interval, 27 - className, 28 - }: { 29 - interval: Interval; 30 - className?: string; 31 - }) { 32 - const router = useRouter(); 33 - const pathname = usePathname(); 34 - const updateSearchParams = useUpdateSearchParams(); 16 + import { SearchParamsPreset } from "./search-params-preset"; 35 17 36 - function onSelect(value: Interval) { 37 - const searchParams = updateSearchParams({ interval: value }); 38 - router.replace(`${pathname}?${searchParams}`, { scroll: false }); 39 - } 40 - 18 + export function IntervalPreset({ interval }: { interval: Interval }) { 41 19 return ( 42 20 <div className="grid gap-1"> 43 21 <div className="flex items-center gap-1.5"> ··· 56 34 </PopoverContent> 57 35 </Popover> 58 36 </div> 59 - <Select onValueChange={onSelect} defaultValue={interval}> 60 - <SelectTrigger className={cn("w-[150px]", className)} id="interval"> 61 - <span className="flex items-center gap-2"> 62 - <Hourglass className="h-4 w-4" /> 63 - <SelectValue /> 64 - </span> 65 - </SelectTrigger> 66 - <SelectContent> 67 - {intervals.map((interval) => ( 68 - <SelectItem key={interval} value={interval}> 69 - {interval} 70 - </SelectItem> 71 - ))} 72 - </SelectContent> 73 - </Select> 37 + <SearchParamsPreset 38 + disabled={false} 39 + defaultValue={interval} 40 + values={intervals} 41 + searchParam="interval" 42 + icon="hour-glass" 43 + placeholder="Pick an interval" 44 + /> 74 45 </div> 75 46 ); 76 47 }
+3 -6
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/metrics.tsx
··· 7 7 import { MetricsCard } from "./metrics-card"; 8 8 9 9 const metricsOrder = [ 10 - "avgLatency", 10 + "p50Latency", 11 11 "p75Latency", 12 12 "p90Latency", 13 13 "p95Latency", ··· 24 24 if (!metrics) return null; 25 25 26 26 const [current, last] = metrics.sort((a, b) => 27 - a.time - b.time < 0 ? 1 : -1, 27 + (a.lastTimestamp || 0) - (b.lastTimestamp || 0) < 0 ? 1 : -1, 28 28 ); 29 29 30 30 const isEmpty = current.count === 0; ··· 81 81 return ( 82 82 <MetricsCard 83 83 key={key} 84 - title={ 85 - // FIXME: rename in tb 86 - key === "avgLatency" ? "P50" : key.replace("Latency", "") 87 - } 84 + title={key.replace("Latency", "")} 88 85 value={value || 0} 89 86 suffix="ms" 90 87 delta={delta}
+12 -46
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/quantile-preset.tsx
··· 2 2 3 3 import * as React from "react"; 4 4 import { usePathname, useRouter } from "next/navigation"; 5 - import { CandlestickChart, HelpCircle } from "lucide-react"; 5 + import { HelpCircle } from "lucide-react"; 6 6 7 7 import { 8 8 Label, 9 9 Popover, 10 10 PopoverContent, 11 11 PopoverTrigger, 12 - Select, 13 - SelectContent, 14 - SelectItem, 15 - SelectSeparator, 16 - SelectTrigger, 17 - SelectValue, 18 12 Separator, 19 13 } from "@openstatus/ui"; 20 14 21 - import useUpdateSearchParams from "@/hooks/use-update-search-params"; 22 - import { cn } from "@/lib/utils"; 23 15 import { quantiles } from "../utils"; 24 16 import type { Quantile } from "../utils"; 17 + import { SearchParamsPreset } from "./search-params-preset"; 25 18 26 19 export function QuantilePreset({ 27 20 quantile, 28 21 disabled, 29 - className, 30 22 }: { 31 23 quantile: Quantile; 32 24 disabled?: boolean; 33 - className?: string; 34 25 }) { 35 - const router = useRouter(); 36 - const pathname = usePathname(); 37 - const updateSearchParams = useUpdateSearchParams(); 38 - 39 - function onSelect(value: Quantile) { 40 - const searchParams = updateSearchParams({ quantile: value }); 41 - router.replace(`${pathname}?${searchParams}`, { scroll: false }); 42 - } 43 - 44 26 return ( 45 27 <div className="grid gap-1"> 46 28 <div className="flex items-center gap-1.5"> ··· 62 44 </PopoverContent> 63 45 </Popover> 64 46 </div> 65 - <Select 66 - onValueChange={onSelect} 67 - defaultValue={quantile} 47 + <SearchParamsPreset 68 48 disabled={disabled} 69 - > 70 - <SelectTrigger 71 - className={cn("w-[150px] uppercase", className)} 72 - id="quantile" 73 - > 74 - <span className="flex items-center gap-2"> 75 - <CandlestickChart className="h-4 w-4" /> 76 - <SelectValue /> 77 - </span> 78 - </SelectTrigger> 79 - <SelectContent> 80 - {quantiles.map((quantile) => { 81 - return ( 82 - <React.Fragment key={quantile}> 83 - {quantile === "avg" && <SelectSeparator />} 84 - <SelectItem value={quantile} className="uppercase"> 85 - {quantile} 86 - </SelectItem> 87 - </React.Fragment> 88 - ); 89 - })} 90 - </SelectContent> 91 - </Select> 49 + defaultValue={quantile} 50 + values={quantiles} 51 + searchParam="quantile" 52 + icon="candlestick-chart" 53 + placeholder="Pick a quantile" 54 + formatter={(value: Quantile) => ( 55 + <span className="uppercase">{value}</span> 56 + )} 57 + /> 92 58 </div> 93 59 ); 94 60 }
+67
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/search-params-preset.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { usePathname, useRouter } from "next/navigation"; 5 + 6 + import { 7 + Select, 8 + SelectContent, 9 + SelectItem, 10 + SelectTrigger, 11 + SelectValue, 12 + } from "@openstatus/ui"; 13 + 14 + import { Icons } from "@/components/icons"; 15 + import type { ValidIcon } from "@/components/icons"; 16 + import useUpdateSearchParams from "@/hooks/use-update-search-params"; 17 + 18 + export function SearchParamsPreset<T extends string>({ 19 + disabled, 20 + defaultValue, 21 + values, 22 + searchParam, 23 + icon, 24 + placeholder, 25 + formatter, 26 + }: { 27 + disabled?: boolean; 28 + defaultValue?: T; 29 + values: readonly T[]; 30 + searchParam: string; 31 + icon?: ValidIcon; 32 + placeholder?: string; 33 + formatter?(value: T): React.ReactNode; 34 + }) { 35 + const router = useRouter(); 36 + const pathname = usePathname(); 37 + const updateSearchParams = useUpdateSearchParams(); 38 + 39 + function onSelect(value: T) { 40 + const searchParams = updateSearchParams({ [searchParam]: value }); 41 + router.replace(`${pathname}?${searchParams}`, { scroll: false }); 42 + } 43 + 44 + const Icon = icon ? Icons[icon] : undefined; 45 + 46 + return ( 47 + <Select 48 + defaultValue={defaultValue} 49 + onValueChange={onSelect} 50 + disabled={disabled} 51 + > 52 + <SelectTrigger className="bg-background w-[150px] text-left"> 53 + <span className="flex items-center gap-2"> 54 + {Icon ? <Icon className="h-4 w-4" /> : null} 55 + <SelectValue placeholder={placeholder} /> 56 + </span> 57 + </SelectTrigger> 58 + <SelectContent> 59 + {values.map((value) => ( 60 + <SelectItem key={value} value={value}> 61 + {formatter?.(value) || value} 62 + </SelectItem> 63 + ))} 64 + </SelectContent> 65 + </Select> 66 + ); 67 + }
+11 -7
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/page.tsx
··· 2 2 import { notFound } from "next/navigation"; 3 3 import * as z from "zod"; 4 4 5 + import { OSTinybird } from "@openstatus/tinybird"; 6 + 5 7 import { columns } from "@/components/data-table/columns"; 6 8 import { DataTable } from "@/components/data-table/data-table"; 7 - import { getResponseListData } from "@/lib/tb"; 9 + import { env } from "@/env"; 8 10 import { api } from "@/trpc/server"; 9 11 import { DatePickerPreset } from "../_components/date-picker-preset"; 10 - import { getDateByPeriod, periods } from "../utils"; 12 + import { periods } from "../utils"; 13 + 14 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 11 15 12 16 /** 13 17 * allowed URL search params ··· 34 38 return notFound(); 35 39 } 36 40 37 - const date = getDateByPeriod(search.data.period); 41 + // FIXME: the other pipes are missing and mv need to include `timestamp` in the data 42 + const allowedPeriods = ["1h", "1d"] as const; 43 + const period = allowedPeriods.find((i) => i === search.data.period) || "1d"; 38 44 39 - const data = await getResponseListData({ 45 + const data = await tb.endpointList(period)({ 40 46 monitorId: id, 41 47 url: monitor.url, 42 - fromDate: date.from.getTime(), 43 - toDate: date.to.getTime(), 44 48 }); 45 49 46 50 if (!data) return null; ··· 48 52 return ( 49 53 <div className="grid gap-4"> 50 54 <div className="flex flex-wrap items-center gap-2 sm:justify-end"> 51 - <DatePickerPreset period={search.data.period} /> 55 + <DatePickerPreset defaultValue={period} values={allowedPeriods} /> 52 56 </div> 53 57 <DataTable columns={columns} data={data} /> 54 58 </div>
+13 -9
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/_components/combined-chart-wrapper.tsx
··· 47 47 return ( 48 48 <> 49 49 <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between"> 50 - <div className="flex w-full gap-2 sm:flex-row sm:justify-between"> 51 - <Toggle 52 - pressed={combinedRegions} 53 - onPressedChange={setCombinedRegions} 54 - variant="outline" 55 - > 56 - <LineChart className="mr-2 h-4 w-4" /> 57 - {!combinedRegions ? "Combine regions" : "Split regions"} 58 - </Toggle> 50 + <div className="flex w-full items-end gap-2 sm:flex-row sm:justify-between"> 51 + <div className="flex flex-col gap-1.5"> 52 + <p className="text-muted-foreground text-xs">Change the view</p> 53 + <Toggle 54 + pressed={combinedRegions} 55 + onPressedChange={setCombinedRegions} 56 + variant="outline" 57 + className="w-max" 58 + > 59 + <LineChart className="mr-2 h-4 w-4" /> 60 + {!combinedRegions ? "Combine regions" : "Split regions"} 61 + </Toggle> 62 + </div> 59 63 <RegionsPreset 60 64 regions={monitor.regions as Region[]} 61 65 selectedRegions={regions}
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/_components/region-table.tsx
··· 60 60 <SimpleChart data={data.data} region={region} /> 61 61 </TableCell> 62 62 <TableCell className="text-right font-medium"> 63 - {formatNumber(metrics?.avgLatency)} 63 + {formatNumber(metrics?.p50Latency)} 64 64 <span className="text-muted-foreground text-xs font-normal"> 65 65 ms 66 66 </span>
+15 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/_components/simple-chart.tsx
··· 1 1 "use client"; 2 2 3 - import type { CustomTooltipProps } from "@tremor/react"; 3 + import { useEffect, useState } from "react"; 4 + import type { CustomTooltipProps, EventProps } from "@tremor/react"; 4 5 import { LineChart } from "@tremor/react"; 5 6 6 7 import { dataFormatter } from "./utils"; ··· 10 11 region: string; 11 12 } 12 13 14 + // TODO: allow click to open `./details` intercepting route 13 15 export function SimpleChart({ data, region }: SimpleChartProps) { 16 + const [value, setValue] = useState<EventProps>(null); 17 + 18 + useEffect(() => { 19 + console.log(value); 20 + // const href = `./details?monitorId=${ping.monitorId}&cronTimestamp=${ping.cronTimestamp}&region=${ping.region}`; 21 + }, [value]); 22 + 14 23 return ( 15 24 <LineChart 16 25 data={data} ··· 27 36 showGridLines={false} 28 37 showLegend={false} 29 38 customTooltip={customTooltip} 39 + // FEATURE: it would be nice, if on click, the tooltip would be open 40 + // onValueChange={(v) => setValue(v)} 30 41 showAnimation={true} 31 42 /> 32 43 ); ··· 34 45 35 46 const customTooltip = ({ payload, active, label }: CustomTooltipProps) => { 36 47 if (!active || !payload) return null; 48 + const data = payload?.[0]; // BUG: when onValueChange is set, payload is duplicated 49 + 37 50 return ( 38 51 <div className="rounded-tremor-default text-tremor-default dark:text-dark-tremor-default bg-tremor-background dark:bg-dark-tremor-background shadow-tremor-dropdown border-tremor-border dark:border-dark-tremor-border border p-2"> 39 52 <div className="flex flex-col gap-3"> 40 - {payload.map((category, idx) => { 53 + {[data].map((category, idx) => { 41 54 return ( 42 55 <div key={idx} className="flex flex-1 gap-2"> 43 56 <div
+15 -37
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 4 4 5 5 import { flyRegions } from "@openstatus/db/src/schema"; 6 6 import type { Region } from "@openstatus/tinybird"; 7 + import { OSTinybird } from "@openstatus/tinybird"; 7 8 import { Separator } from "@openstatus/ui"; 8 9 9 - import { 10 - getResponseGraphData, 11 - getResponseTimeMetricsByRegionData, 12 - getResponseTimeMetricsData, 13 - } from "@/lib/tb"; 10 + import { env } from "@/env"; 14 11 import { api } from "@/trpc/server"; 15 12 import { ButtonReset } from "../_components/button-reset"; 16 13 import { DatePickerPreset } from "../_components/date-picker-preset"; 17 14 import { Metrics } from "../_components/metrics"; 18 - import { 19 - getDateByPeriod, 20 - getHoursByPeriod, 21 - getMinutesByInterval, 22 - intervals, 23 - periods, 24 - quantiles, 25 - } from "../utils"; 15 + import { getMinutesByInterval, intervals, periods, quantiles } from "../utils"; 26 16 import { CombinedChartWrapper } from "./_components/combined-chart-wrapper"; 17 + 18 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 27 19 28 20 const DEFAULT_QUANTILE = "p95"; 29 21 const DEFAULT_INTERVAL = "30m"; ··· 48 40 ?.split(",") 49 41 .filter((i) => flyRegions.includes(i as Region)) ?? flyRegions, 50 42 ), 51 - // fromDate: z.coerce 52 - // .number() 53 - // .optional() 54 - // .default(startOfDay(new Date()).getTime()), 55 - // toDate: z.coerce.number().optional().default(endOfDay(new Date()).getTime()), 56 43 }); 57 44 58 45 export default async function Page({ ··· 73 60 return notFound(); 74 61 } 75 62 76 - const date = getDateByPeriod(search.data.period); 77 - const intervalMinutes = getMinutesByInterval(search.data.interval); 63 + const { period, quantile, interval, regions } = search.data; 64 + 65 + // TODO: work it out easier 66 + const intervalMinutes = getMinutesByInterval(interval); 78 67 const periodicityMinutes = getMinutesByInterval(monitor.periodicity); 79 - const periodicityHours = getHoursByPeriod(search.data.period); 80 68 81 69 const isQuantileDisabled = intervalMinutes <= periodicityMinutes; 82 70 const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 83 71 84 - const data = await getResponseGraphData({ 72 + const metrics = await tb.endpointMetrics(period)({ 85 73 monitorId: id, 86 74 url: monitor.url, 87 - ...search.data, 88 - /** 89 - * 90 - */ 91 - fromDate: date.from.getTime(), 92 - toDate: date.to.getTime(), 93 - interval: minutes, 94 75 }); 95 76 96 - const metrics = await getResponseTimeMetricsData({ 77 + const data = await tb.endpointChart(period)({ 97 78 monitorId: id, 98 79 url: monitor.url, 99 - interval: periodicityHours, 80 + interval: minutes, 100 81 }); 101 82 102 - const metricsByRegion = await getResponseTimeMetricsByRegionData({ 83 + const metricsByRegion = await tb.endpointMetricsByRegion(period)({ 103 84 monitorId: id, 104 85 url: monitor.url, 105 - interval: periodicityHours, 106 86 }); 107 87 108 88 if (!data || !metrics || !metricsByRegion) return null; 109 89 110 - const { period, quantile, interval, regions } = search.data; 111 - 112 90 const isDirty = 113 91 period !== DEFAULT_PERIOD || 114 92 quantile !== DEFAULT_QUANTILE || ··· 118 96 return ( 119 97 <div className="grid gap-4"> 120 98 <div className="flex justify-between gap-2"> 121 - <DatePickerPreset period={period} /> 99 + <DatePickerPreset defaultValue={period} values={periods} /> 122 100 {isDirty ? <ButtonReset /> : null} 123 101 </div> 124 102 <Metrics metrics={metrics} period={period} /> ··· 128 106 period={period} 129 107 quantile={quantile} 130 108 interval={interval} 131 - regions={regions as Region[]} 109 + regions={regions as Region[]} // FIXME: not properly reseted after filtered 132 110 monitor={monitor} 133 111 isQuantileDisabled={isQuantileDisabled} 134 112 metricsByRegion={metricsByRegion}
+1 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/utils.ts
··· 10 10 import type { MonitorPeriodicity } from "@openstatus/db/src/schema"; 11 11 12 12 export const periods = ["1h", "1d", "3d", "7d", "14d"] as const; // If neeeded (e.g. Pro plans), "7d", "30d" 13 - export const quantiles = ["p99", "p95", "p90", "p75", "avg"] as const; 13 + export const quantiles = ["p99", "p95", "p90", "p75", "p50"] as const; 14 14 export const intervals = ["1m", "10m", "30m", "1h"] as const; 15 15 16 16 export type Period = (typeof periods)[number];
+48
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/_components/refresh-widget.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 4 + import { useRouter } from "next/navigation"; 5 + 6 + import { Button } from "@openstatus/ui"; 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 + }
-44
apps/web/src/app/monitor/[id]/page.tsx
··· 1 - import * as z from "zod"; 2 - 3 - import { flyRegions } from "@openstatus/utils"; 4 - 5 - import { columns } from "@/components/data-table/columns"; 6 - import { DataTable } from "@/components/data-table/data-table"; 7 - import { getResponseListData } from "@/lib/tb"; 8 - import { api } from "@/trpc/server"; 9 - 10 - // 11 - 12 - /** 13 - * allowed URL search params 14 - */ 15 - const searchParamsSchema = z.object({ 16 - statusCode: z.coerce.number().optional(), 17 - region: z.enum(flyRegions).optional(), 18 - cronTimestamp: z.coerce.number().optional(), 19 - fromDate: z.coerce.number().optional(), 20 - toDate: z.coerce.number().optional(), 21 - }); 22 - 23 - export default async function Monitor({ 24 - params, 25 - searchParams, 26 - }: { 27 - params: { id: string }; 28 - searchParams: { [key: string]: string | string[] | undefined }; 29 - }) { 30 - const search = searchParamsSchema.safeParse(searchParams); 31 - const monitor = await api.monitor.getMonitorById.query({ 32 - id: Number(params.id), 33 - }); 34 - const data = search.success 35 - ? // TODO: lets hard-code our `monitorId` here 36 - await getResponseListData({ 37 - monitorId: params.id, 38 - url: monitor.url, 39 - ...search.data, 40 - }) 41 - : await getResponseListData({ monitorId: params.id, url: monitor.url }); 42 - if (!data || !search.success) return <div>Something went wrong</div>; 43 - return <DataTable columns={columns} data={data} />; 44 - }
-19
apps/web/src/app/monitor/layout.tsx
··· 1 - import * as React from "react"; 2 - 3 - import { Shell } from "@/components/dashboard/shell"; 4 - import { BackButton } from "@/components/layout/back-button"; 5 - import { MarketingLayout } from "@/components/layout/marketing-layout"; 6 - 7 - // same layout as /play 8 - export default function MonitorLayout({ 9 - children, 10 - }: { 11 - children: React.ReactNode; 12 - }) { 13 - return ( 14 - <MarketingLayout> 15 - <BackButton href="/play" /> 16 - <Shell>{children}</Shell> 17 - </MarketingLayout> 18 - ); 19 - }
-24
apps/web/src/app/play/@modal/(..)monitor/[id]/modal.tsx
··· 1 - "use client"; 2 - 3 - import { useRouter } from "next/navigation"; 4 - 5 - import { Dialog, DialogContent } from "@openstatus/ui"; 6 - 7 - export function Modal({ children }: { children: React.ReactNode }) { 8 - const router = useRouter(); 9 - 10 - const handleOpenChange = (open: boolean) => { 11 - if (!open) { 12 - router.back(); 13 - } 14 - }; 15 - 16 - return ( 17 - <Dialog open onOpenChange={handleOpenChange}> 18 - {/* overflow-auto should happen inside content table */} 19 - <DialogContent className="max-h-screen w-full overflow-auto sm:max-w-3xl sm:p-8"> 20 - {children} 21 - </DialogContent> 22 - </Dialog> 23 - ); 24 - }
-43
apps/web/src/app/play/@modal/(..)monitor/[id]/page.tsx
··· 1 - import * as z from "zod"; 2 - 3 - import { flyRegions } from "@openstatus/utils"; 4 - 5 - import { columns } from "@/components/data-table/columns"; 6 - import { DataTable } from "@/components/data-table/data-table"; 7 - import { getResponseListData } from "@/lib/tb"; 8 - import { Modal } from "./modal"; 9 - 10 - // 11 - 12 - /** 13 - * allowed URL search params 14 - */ 15 - const searchParamsSchema = z.object({ 16 - statusCode: z.coerce.number().optional(), 17 - region: z.enum(flyRegions).optional(), 18 - cronTimestamp: z.coerce.number().optional(), 19 - fromDate: z.coerce.number().optional(), 20 - toDate: z.coerce.number().optional(), 21 - }); 22 - 23 - export default async function Monitor({ 24 - params, 25 - searchParams, 26 - }: { 27 - params: { id: string }; 28 - searchParams: { [key: string]: string | string[] | undefined }; 29 - }) { 30 - const search = searchParamsSchema.safeParse(searchParams); 31 - const data = search.success 32 - ? // TODO: lets hard-code our `monitorId` here 33 - await getResponseListData({ monitorId: params.id, ...search.data }) 34 - : await getResponseListData({ monitorId: params.id }); 35 - 36 - if (!data) return <div>Something went wrong</div>; 37 - 38 - return ( 39 - <Modal> 40 - <DataTable columns={columns} data={data} /> 41 - </Modal> 42 - ); 43 - }
-3
apps/web/src/app/play/@modal/default.tsx
··· 1 - export default function Default() { 2 - return null; 3 - }
+1 -8
apps/web/src/app/play/layout.tsx
··· 20 20 21 21 export default function PlayLayout({ 22 22 children, 23 - modal, 24 23 }: { 25 24 children: React.ReactNode; 26 - modal: React.ReactNode; 27 25 }) { 28 - return ( 29 - <MarketingLayout> 30 - {children} 31 - {modal} 32 - </MarketingLayout> 33 - ); 26 + return <MarketingLayout>{children}</MarketingLayout>; 34 27 }
+1
apps/web/src/components/data-table/data-table-row-action.tsx
··· 20 20 export function DataTableRowActions<TData>({ 21 21 row, 22 22 }: DataTableRowActionsProps<TData>) { 23 + // FIXME: DRY - this is a duplicate of the OSTinybird endpoint 23 24 const ping = tbBuildResponseList.parse(row.original); 24 25 return ( 25 26 <DropdownMenu>
+17 -9
apps/web/src/components/data-table/monitor/columns.tsx
··· 45 45 }, 46 46 { 47 47 accessorKey: "tracker", 48 - header: "Last 7 days", 48 + header: () => ( 49 + <HeaderTooltip label="Last 7 days" content="UTC time period" /> 50 + ), 49 51 cell: ({ row }) => { 50 52 const tracker = new Tracker({ 51 53 data: row.original.data?.slice(0, 7).reverse(), ··· 80 82 }, 81 83 { 82 84 accessorKey: "uptime", 83 - header: () => <HeaderTooltip>Uptime</HeaderTooltip>, 85 + header: () => ( 86 + <HeaderTooltip label="Uptime" content="Data from the last 24h" /> 87 + ), 84 88 cell: ({ row }) => { 85 89 const { count, ok } = row.original?.metrics || {}; 86 90 if (!count || !ok) ··· 90 94 }, 91 95 }, 92 96 { 93 - accessorKey: "avgLatency", 94 - header: () => <HeaderTooltip>P50</HeaderTooltip>, 97 + accessorKey: "p50Latency", 98 + header: () => ( 99 + <HeaderTooltip label="P50" content="Data from the last 24h" /> 100 + ), 95 101 cell: ({ row }) => { 96 - const latency = row.original.metrics?.avgLatency; 102 + const latency = row.original.metrics?.p50Latency; 97 103 if (latency) return <Number value={latency} suffix="ms" />; 98 104 return <span className="text-muted-foreground">-</span>; 99 105 }, 100 106 }, 101 107 { 102 108 accessorKey: "p95Latency", 103 - header: () => <HeaderTooltip>P95</HeaderTooltip>, 109 + header: () => ( 110 + <HeaderTooltip label="P95" content="Data from the last 24h" /> 111 + ), 104 112 cell: ({ row }) => { 105 113 const latency = row.original.metrics?.p95Latency; 106 114 if (latency) return <Number value={latency} suffix="ms" />; ··· 119 127 }, 120 128 ]; 121 129 122 - function HeaderTooltip({ children }: { children: string }) { 130 + function HeaderTooltip({ label, content }: { label: string; content: string }) { 123 131 return ( 124 132 <TooltipProvider> 125 133 <Tooltip> 126 134 <TooltipTrigger className="underline decoration-dotted"> 127 - {children} 135 + {label} 128 136 </TooltipTrigger> 129 - <TooltipContent>Data from the last 24h</TooltipContent> 137 + <TooltipContent>{content}</TooltipContent> 130 138 </Tooltip> 131 139 </TooltipProvider> 132 140 );
+4
apps/web/src/components/icons.tsx
··· 5 5 Book, 6 6 Bot, 7 7 Calendar, 8 + CandlestickChart, 8 9 Check, 9 10 Clock, 10 11 Cog, ··· 12 13 CreditCard, 13 14 Fingerprint, 14 15 Globe2, 16 + Hourglass, 15 17 Image, 16 18 KeyRound, 17 19 Laptop, ··· 96 98 book: Book, 97 99 newspaper: Newspaper, 98 100 youtube: Youtube, 101 + "hour-glass": Hourglass, 102 + "candlestick-chart": CandlestickChart, 99 103 discord: ({ ...props }: LucideProps) => ( 100 104 <svg viewBox="0 0 640 512" {...props}> 101 105 <path
+4 -56
apps/web/src/lib/tb.ts
··· 2 2 HomeStatsParams, 3 3 MonitorListParams, 4 4 ResponseDetailsParams, 5 - ResponseGraphParams, 6 - ResponseListParams, 7 - ResponseTimeMetricsByRegionParams, 8 - ResponseTimeMetricsParams, 9 5 } from "@openstatus/tinybird"; 10 6 import { 11 7 getHomeMonitorList, 12 8 getHomeStats, 13 9 getMonitorList, 14 10 getResponseDetails, 15 - getResponseGraph, 16 - getResponseList, 17 - getResponseTimeMetrics, 18 - getResponseTimeMetricsByRegion, 19 11 Tinybird, 20 12 } from "@openstatus/tinybird"; 21 13 22 14 import { env } from "@/env"; 23 15 16 + // @depreciated in favor to use the OSTinybird client directly 24 17 const tb = new Tinybird({ token: env.TINY_BIRD_API_KEY }); 25 18 26 - // TODO: add security layer 27 - export async function getResponseListData(props: Partial<ResponseListParams>) { 28 - try { 29 - const res = await getResponseList(tb)(props); 30 - return res.data; 31 - } catch (e) { 32 - console.error(e); 33 - } 34 - return; 35 - } 36 - 19 + // TODO: not yet converted to new mv 37 20 export async function getResponseDetailsData( 38 21 props: Partial<ResponseDetailsParams>, 39 22 ) { ··· 46 29 return; 47 30 } 48 31 32 + // REMINDER: includes yet timezone 49 33 export async function getMonitorListData(props: MonitorListParams) { 50 34 try { 51 35 const res = await getMonitorList(tb)(props); ··· 56 40 return; 57 41 } 58 42 59 - // Includes caching of data for 10 minutes 43 + // REMINDER: includes yet timezone 60 44 export async function getHomeMonitorListData( 61 45 props: Pick<MonitorListParams, "timezone">, 62 46 ) { ··· 82 66 } 83 67 return; 84 68 } 85 - 86 - export async function getResponseGraphData( 87 - props: Partial<ResponseGraphParams>, 88 - ) { 89 - try { 90 - const res = await getResponseGraph(tb)(props); 91 - return res.data; 92 - } catch (e) { 93 - console.error(e); 94 - } 95 - return; 96 - } 97 - 98 - export async function getResponseTimeMetricsData( 99 - props: ResponseTimeMetricsParams, 100 - ) { 101 - try { 102 - const res = await getResponseTimeMetrics(tb)(props); 103 - return res.data; 104 - } catch (e) { 105 - console.error(e); 106 - } 107 - return; 108 - } 109 - 110 - export async function getResponseTimeMetricsByRegionData( 111 - props: ResponseTimeMetricsByRegionParams, 112 - ) { 113 - try { 114 - const res = await getResponseTimeMetricsByRegion(tb)(props); 115 - return res.data; 116 - } catch (e) { 117 - console.error(e); 118 - } 119 - return; 120 - }
+1
packages/api/package.json
··· 12 12 "@openstatus/db": "workspace:*", 13 13 "@openstatus/emails": "workspace:*", 14 14 "@openstatus/plans": "workspace:*", 15 + "@openstatus/tinybird": "workspace:*", 15 16 "@t3-oss/env-core": "0.7.0", 16 17 "@trpc/client": "10.38.5", 17 18 "@trpc/server": "10.38.5",
+2
packages/api/src/edge.ts
··· 7 7 import { pageRouter } from "./router/page"; 8 8 import { pageSubscriberRouter } from "./router/pageSubscriber"; 9 9 import { statusReportRouter } from "./router/statusReport"; 10 + import { tinybirdRouter } from "./router/tinybird"; 10 11 import { userRouter } from "./router/user"; 11 12 import { workspaceRouter } from "./router/workspace"; 12 13 import { createTRPCRouter } from "./trpc"; ··· 24 25 invitation: invitationRouter, 25 26 incident: incidentRouter, 26 27 pageSubscriber: pageSubscriberRouter, 28 + tinybird: tinybirdRouter, 27 29 });
+2
packages/api/src/env.ts
··· 7 7 PROJECT_ID_VERCEL: z.string(), 8 8 TEAM_ID_VERCEL: z.string(), 9 9 VERCEL_AUTH_BEARER_TOKEN: z.string(), 10 + TINY_BIRD_API_KEY: z.string(), 10 11 }, 11 12 12 13 runtimeEnv: { ··· 14 15 PROJECT_ID_VERCEL: process.env.PROJECT_ID_VERCEL, 15 16 TEAM_ID_VERCEL: process.env.TEAM_ID_VERCEL, 16 17 VERCEL_AUTH_BEARER_TOKEN: process.env.VERCEL_AUTH_BEARER_TOKEN, 18 + TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY, 17 19 }, 18 20 skipValidation: process.env.NODE_ENV === "test", 19 21 });
+23
packages/api/src/router/tinybird/index.ts
··· 1 + import { z } from "zod"; 2 + 3 + import { OSTinybird } from "@openstatus/tinybird"; 4 + 5 + import { env } from "../../env"; 6 + import { createTRPCRouter, protectedProcedure } from "../../trpc"; 7 + 8 + const tb = new OSTinybird({ token: env.TINY_BIRD_API_KEY }); 9 + 10 + // WORK IN PROGRESS - we can create a tb router to call it via TRPC server and client 11 + 12 + export const tinybirdRouter = createTRPCRouter({ 13 + lastCronTimestamp: protectedProcedure.query(async (opts) => { 14 + const workspaceId = String(opts.ctx.workspace.id); 15 + return await tb.endpointLastCronTimestamp("workspace")({ workspaceId }); 16 + }), 17 + 18 + monitorMetricsFromWorkspace: protectedProcedure 19 + .input(z.object({ period: z.string() })) 20 + .query(async (opts) => { 21 + const workspaceId = String(opts.ctx.workspace.id); 22 + }), 23 + });
+2 -1
packages/db/package.json
··· 8 8 "push": "drizzle-kit push:sqlite", 9 9 "migrate": "bun src/migrate.mts", 10 10 "studio": "drizzle-kit studio", 11 - "seed": "bun src/seed.mts" 11 + "seed": "bun src/seed.mts", 12 + "dev": "turso dev --db-file openstatus.db" 12 13 }, 13 14 "dependencies": { 14 15 "@libsql/client": "0.5.2",
+17
packages/tinybird/datasources/__ttl_14d_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_14d__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `latency` Int64, 7 + `monitorId` String, 8 + `region` LowCardinality(String), 9 + `statusCode` Nullable(Int16), 10 + `url` String, 11 + `workspaceId` String, 12 + `cronTimestamp` Int64 13 + 14 + ENGINE "MergeTree" 15 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 16 + ENGINE_SORTING_KEY "monitorId, time" 17 + ENGINE_TTL "time + toIntervalDay(14)"
+18
packages/tinybird/datasources/__ttl_1d_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_1d__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `latency` Int64, 7 + `monitorId` String, 8 + `region` LowCardinality(String), 9 + `statusCode` Nullable(Int16), 10 + `url` String, 11 + `workspaceId` String, 12 + `timestamp` Int64, 13 + `cronTimestamp` Int64 14 + 15 + ENGINE "MergeTree" 16 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 17 + ENGINE_SORTING_KEY "monitorId, time" 18 + ENGINE_TTL "time + toIntervalDay(1)"
+18
packages/tinybird/datasources/__ttl_1h_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_1h__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `latency` Int64, 7 + `monitorId` String, 8 + `region` LowCardinality(String), 9 + `statusCode` Nullable(Int16), 10 + `url` String, 11 + `workspaceId` String, 12 + `timestamp` Int64, 13 + `cronTimestamp` Int64 14 + 15 + ENGINE "MergeTree" 16 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 17 + ENGINE_SORTING_KEY "monitorId, time" 18 + ENGINE_TTL "time + toIntervalHour(1)"
+17
packages/tinybird/datasources/__ttl_3d_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_3d__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `latency` Int64, 7 + `monitorId` String, 8 + `region` LowCardinality(String), 9 + `statusCode` Nullable(Int16), 10 + `url` String, 11 + `workspaceId` String, 12 + `cronTimestamp` Int64 13 + 14 + ENGINE "MergeTree" 15 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 16 + ENGINE_SORTING_KEY "monitorId, time" 17 + ENGINE_TTL "time + toIntervalDay(3)"
+14
packages/tinybird/datasources/__ttl_45d_count_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_45d_count__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `monitorId` String, 7 + `url` String, 8 + `count` AggregateFunction(count), 9 + `ok` AggregateFunction(count, Nullable(UInt8)) 10 + 11 + ENGINE "AggregatingMergeTree" 12 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 13 + ENGINE_SORTING_KEY "time, monitorId, url" 14 + ENGINE_TTL "time + toIntervalDay(45)"
+17
packages/tinybird/datasources/__ttl_45d_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_45d__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `latency` Int64, 7 + `monitorId` String, 8 + `region` LowCardinality(String), 9 + `statusCode` Nullable(Int16), 10 + `url` String, 11 + `workspaceId` String, 12 + `cronTimestamp` Int64 13 + 14 + ENGINE "MergeTree" 15 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 16 + ENGINE_SORTING_KEY "monitorId, time" 17 + ENGINE_TTL "time + toIntervalDay(45)"
+14
packages/tinybird/datasources/__ttl_7d_count_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_7d_count__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `monitorId` String, 7 + `url` String, 8 + `count` AggregateFunction(count), 9 + `ok` AggregateFunction(count, Nullable(UInt8)) 10 + 11 + ENGINE "AggregatingMergeTree" 12 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 13 + ENGINE_SORTING_KEY "time, monitorId, url" 14 + ENGINE_TTL "time + toIntervalDay(7)"
+17
packages/tinybird/datasources/__ttl_7d_mv.datasource
··· 1 + # Data Source created from Pipe '__ttl_7d__v0' 2 + VERSION 0 3 + 4 + SCHEMA > 5 + `time` DateTime, 6 + `latency` Int64, 7 + `monitorId` String, 8 + `region` LowCardinality(String), 9 + `statusCode` Nullable(Int16), 10 + `url` String, 11 + `workspaceId` String, 12 + `cronTimestamp` Int64 13 + 14 + ENGINE "MergeTree" 15 + ENGINE_PARTITION_KEY "toYYYYMM(time)" 16 + ENGINE_SORTING_KEY "monitorId, time" 17 + ENGINE_TTL "time + toIntervalDay(7)"
+20
packages/tinybird/pipes/__ttl_14d.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_14d_0 4 + SQL > 5 + 6 + SELECT 7 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 8 + latency, 9 + monitorId, 10 + region, 11 + statusCode, 12 + url, 13 + workspaceId, 14 + cronTimestamp 15 + FROM ping_response__v7 16 + 17 + TYPE materialized 18 + DATASOURCE __ttl_14d_mv__v0 19 + 20 +
+28
packages/tinybird/pipes/__ttl_14d_chart_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_14d_chart_get_endpoint_read_2504" READ 4 + 5 + NODE __ttl_14d_chart_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.5)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.9)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM __ttl_14d_mv__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1') }} 24 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 25 + GROUP BY h, region 26 + ORDER BY h DESC 27 + 28 +
+40
packages/tinybird/pipes/__ttl_14d_metrics_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_14d_metrics_get_endpoint_read_3564" READ 4 + 5 + NODE __ttl_14d_metrics_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.5)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.9)(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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM __ttl_14d_mv__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1') }} 21 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 22 + AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3) 23 + UNION ALL 24 + SELECT 25 + round(quantile(0.5)(latency)) AS p50Latency, 26 + round(quantile(0.75)(latency)) AS p75Latency, 27 + round(quantile(0.9)(latency)) AS p90Latency, 28 + round(quantile(0.95)(latency)) AS p95Latency, 29 + round(quantile(0.99)(latency)) AS p99Latency, 30 + count() as count, 31 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 32 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 33 + FROM __ttl_45d_mv__v0 34 + WHERE 35 + monitorId = {{ String(monitorId, '1') }} 36 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 37 + AND time >= toDateTime64(now() - INTERVAL 28 DAY, 3) 38 + AND time < toDateTime64(now() - INTERVAL 14 DAY, 3) 39 + 40 +
+25
packages/tinybird/pipes/__ttl_14d_metrics_get_by_region.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_14d_metrics_get_by_region_endpoint_read_0048" READ 4 + 5 + NODE __ttl_14d_metrics_get_by_region_0 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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 18 + max(cronTimestamp) AS lastTimestamp 19 + FROM __ttl_14d_mv__v0 20 + WHERE 21 + monitorId = {{ String(monitorId, '1') }} 22 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 23 + GROUP BY region 24 + 25 +
+21
packages/tinybird/pipes/__ttl_1d.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_1d_0 4 + SQL > 5 + 6 + SELECT 7 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 8 + latency, 9 + monitorId, 10 + region, 11 + statusCode, 12 + url, 13 + workspaceId, 14 + timestamp, 15 + cronTimestamp 16 + FROM ping_response__v7 17 + 18 + TYPE materialized 19 + DATASOURCE __ttl_1d_mv__v0 20 + 21 +
+28
packages/tinybird/pipes/__ttl_1d_chart_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1d_chart_get_endpoint_read_4815" READ 4 + 5 + NODE __ttl_1d_chart_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.5)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.9)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM __ttl_1d_mv__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1') }} 24 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 25 + GROUP BY h, region 26 + ORDER BY h DESC 27 + 28 +
+16
packages/tinybird/pipes/__ttl_1d_list_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1d_list_get__v0_endpoint_read_1695" READ 4 + 5 + NODE __ttl_1d_list_get__v0_0 6 + SQL > 7 + 8 + % 9 + SELECT latency, monitorId, region, statusCode, timestamp, url, workspaceId, cronTimestamp 10 + FROM __ttl_1d_mv__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1') }} 13 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 14 + ORDER BY cronTimestamp DESC 15 + 16 +
+40
packages/tinybird/pipes/__ttl_1d_metrics_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1d_metrics_get_endpoint_read_1167" READ 4 + 5 + NODE __ttl_1d_metrics_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.5)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.9)(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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM __ttl_1d_mv__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1') }} 21 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 22 + AND time >= toDateTime64(now() - INTERVAL 1 DAY, 3) 23 + UNION ALL 24 + SELECT 25 + round(quantile(0.5)(latency)) AS p50Latency, 26 + round(quantile(0.75)(latency)) AS p75Latency, 27 + round(quantile(0.9)(latency)) AS p90Latency, 28 + round(quantile(0.95)(latency)) AS p95Latency, 29 + round(quantile(0.99)(latency)) AS p99Latency, 30 + count() as count, 31 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 32 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 33 + FROM __ttl_3d_mv__v0 34 + WHERE 35 + monitorId = {{ String(monitorId, '1') }} 36 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 37 + AND time >= toDateTime64(now() - INTERVAL 2 DAY, 3) 38 + AND time < toDateTime64(now() - INTERVAL 1 DAY, 3) 39 + 40 +
+24
packages/tinybird/pipes/__ttl_1d_metrics_get_by_region.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1d_metrics_get_by_region_endpoint_read_3463" READ 4 + 5 + NODE __ttl_1d_metrics_get_by_region_0 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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 18 + FROM __ttl_1d_mv__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1') }} 21 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 22 + GROUP BY region 23 + 24 +
+21
packages/tinybird/pipes/__ttl_1h.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_1h_0 4 + SQL > 5 + 6 + SELECT 7 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 8 + latency, 9 + monitorId, 10 + region, 11 + statusCode, 12 + url, 13 + workspaceId, 14 + timestamp, 15 + cronTimestamp 16 + FROM ping_response__v7 17 + 18 + TYPE materialized 19 + DATASOURCE __ttl_1h_mv__v0 20 + 21 +
+28
packages/tinybird/pipes/__ttl_1h_chart_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1h_chart_get__v0_endpoint_read_3603" READ 4 + 5 + NODE __ttl_1h_chart_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.5)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.9)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM __ttl_1h_mv__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1') }} 24 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 25 + GROUP BY h, region 26 + ORDER BY h DESC 27 + 28 +
+15
packages/tinybird/pipes/__ttl_1h_last_timestamp_monitor_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1h_last_timestamp_monitor_get__v0_endpoint_read_5766" READ 4 + 5 + NODE __ttl_1h_last_timestamp_monitor_get__v0_0 6 + SQL > 7 + 8 + % 9 + SELECT max(cronTimestamp) as cronTimestamp 10 + FROM __ttl_1h_mv__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '1') }} 13 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 14 + 15 +
+13
packages/tinybird/pipes/__ttl_1h_last_timestamp_workspace_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1h_last_timestamp_workspace_get__v0_endpoint_read_5322" READ 4 + 5 + NODE __ttl_1h_last_timestamp_workspace_get__v0_0 6 + SQL > 7 + 8 + % 9 + SELECT max(cronTimestamp) as cronTimestamp 10 + FROM __ttl_1h_mv__v0 11 + WHERE workspaceId = {{ String(workspaceId, '1') }} 12 + 13 +
+17
packages/tinybird/pipes/__ttl_1h_list_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1h_list_get__v0_endpoint_read_0371" READ 4 + 5 + NODE __ttl_1h_list_get__v0_0 6 + SQL > 7 + 8 + % 9 + -- FIXME: `timestamp` is missing on 1h, 3d, 7d, 45d mv! 10 + SELECT latency, monitorId, region, statusCode, url, workspaceId, cronTimestamp, timestamp 11 + FROM __ttl_1h_mv__v0 12 + WHERE 13 + monitorId = {{ String(monitorId, '1') }} 14 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 15 + ORDER BY cronTimestamp DESC 16 + 17 +
+40
packages/tinybird/pipes/__ttl_1h_metrics_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1h_metrics_get__v0_endpoint_read_0457" READ 4 + 5 + NODE __ttl_1h_metrics_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.5)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.9)(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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM __ttl_1h_mv__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1') }} 21 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 22 + AND time >= toDateTime64(now() - INTERVAL 1 HOUR, 3) 23 + UNION ALL 24 + SELECT 25 + round(quantile(0.5)(latency)) AS p50Latency, 26 + round(quantile(0.75)(latency)) AS p75Latency, 27 + round(quantile(0.9)(latency)) AS p90Latency, 28 + round(quantile(0.95)(latency)) AS p95Latency, 29 + round(quantile(0.99)(latency)) AS p99Latency, 30 + count() as count, 31 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 32 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 33 + FROM __ttl_1d_mv__v0 34 + WHERE 35 + monitorId = {{ String(monitorId, '1') }} 36 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 37 + AND time >= toDateTime64(now() - INTERVAL 2 HOUR, 3) 38 + AND time < toDateTime64(now() - INTERVAL 1 HOUR, 3) 39 + 40 +
+24
packages/tinybird/pipes/__ttl_1h_metrics_get_by_region.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_1h_metrics_get_by_region__v0_endpoint_read_0839" READ 4 + 5 + NODE __ttl_1h_metrics_get_by_region_0 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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 18 + FROM __ttl_1h_mv__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1') }} 21 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 22 + GROUP BY region 23 + 24 +
+20
packages/tinybird/pipes/__ttl_3d.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_3d_0 4 + SQL > 5 + 6 + SELECT 7 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 8 + latency, 9 + monitorId, 10 + region, 11 + statusCode, 12 + url, 13 + workspaceId, 14 + cronTimestamp 15 + FROM ping_response__v7 16 + 17 + TYPE materialized 18 + DATASOURCE __ttl_3d_mv__v0 19 + 20 +
+28
packages/tinybird/pipes/__ttl_3d_chart_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_3d_chart_get_endpoint_read_5551" READ 4 + 5 + NODE __ttl_3d_chart_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.5)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.9)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM __ttl_3d_mv__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1') }} 24 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 25 + GROUP BY h, region 26 + ORDER BY h DESC 27 + 28 +
+40
packages/tinybird/pipes/__ttl_3d_metrics_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_3d_metrics_get_endpoint_read_5801" READ 4 + 5 + NODE __ttl_3d_metrics_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + round(quantile(0.5)(latency)) as p50Latency, 11 + round(quantile(0.75)(latency)) as p75Latency, 12 + round(quantile(0.9)(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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 17 + max(cronTimestamp) AS lastTimestamp 18 + FROM __ttl_3d_mv__v0 19 + WHERE 20 + monitorId = {{ String(monitorId, '1') }} 21 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 22 + AND time >= toDateTime64(now() - INTERVAL 3 DAY, 3) 23 + UNION ALL 24 + SELECT 25 + round(quantile(0.5)(latency)) AS p50Latency, 26 + round(quantile(0.75)(latency)) AS p75Latency, 27 + round(quantile(0.9)(latency)) AS p90Latency, 28 + round(quantile(0.95)(latency)) AS p95Latency, 29 + round(quantile(0.99)(latency)) AS p99Latency, 30 + count() as count, 31 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 32 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 33 + FROM __ttl_7d_mv__v0 34 + WHERE 35 + monitorId = {{ String(monitorId, '1') }} 36 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 37 + AND time >= toDateTime64(now() - INTERVAL 6 DAY, 3) 38 + AND time < toDateTime64(now() - INTERVAL 3 DAY, 3) 39 + 40 +
+25
packages/tinybird/pipes/__ttl_3d_metrics_get_by_region.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_3d_metrics_get_by_region__v0_endpoint_read_3349" READ 4 + 5 + NODE __ttl_3d_metrics_get_by_region_0 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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 18 + max(cronTimestamp) AS lastTimestamp 19 + FROM __ttl_3d_mv__v0 20 + WHERE 21 + monitorId = {{ String(monitorId, '1') }} 22 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 23 + GROUP BY region 24 + 25 +
+20
packages/tinybird/pipes/__ttl_45d.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_45d_0 4 + SQL > 5 + 6 + SELECT 7 + toStartOfDay(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 8 + latency, 9 + monitorId, 10 + region, 11 + statusCode, 12 + url, 13 + workspaceId, 14 + cronTimestamp 15 + FROM ping_response__v7 16 + 17 + TYPE materialized 18 + DATASOURCE __ttl_45d_mv__v0 19 + 20 +
+25
packages/tinybird/pipes/__ttl_45d_count.pipe
··· 1 + VERSION 0 2 + 3 + DESCRIPTION > 4 + TODO: descripe what it is for! 5 + 6 + 7 + NODE __ttl_45d_count_0 8 + SQL > 9 + 10 + SELECT 11 + time, 12 + monitorId, 13 + url, 14 + countState() AS count, 15 + countState(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 16 + FROM __ttl_45d_mv__v0 17 + GROUP BY 18 + time, 19 + monitorId, 20 + url 21 + 22 + TYPE materialized 23 + DATASOURCE __ttl_45d_count_mv__v0 24 + 25 +
+23
packages/tinybird/pipes/__ttl_45d_count_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_45d_count_get__v0_endpoint_read_2389" READ 4 + 5 + NODE __ttl_45d_count_get_0 6 + SQL > 7 + 8 + % 9 + SELECT time as day, countMerge(count) as count, countMerge(ok) as ok 10 + FROM __ttl_45d_count_mv__v0 11 + WHERE 12 + monitorId = {{ String(monitorId, '4') }} 13 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 14 + GROUP BY day 15 + ORDER BY day DESC 16 + WITH FILL 17 + FROM 18 + toStartOfDay(now()) 19 + TO toStartOfDay( 20 + date_sub(DAY, 45, now()) 21 + ) STEP INTERVAL -1 DAY 22 + 23 +
+20
packages/tinybird/pipes/__ttl_7d.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_7d_0 4 + SQL > 5 + 6 + SELECT 7 + toDateTime(fromUnixTimestamp64Milli(cronTimestamp)) AS time, 8 + latency, 9 + monitorId, 10 + region, 11 + statusCode, 12 + url, 13 + workspaceId, 14 + cronTimestamp 15 + FROM ping_response__v7 16 + 17 + TYPE materialized 18 + DATASOURCE __ttl_7d_mv__v0 19 + 20 +
+28
packages/tinybird/pipes/__ttl_7d_chart_get.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_7d_chart_get_endpoint_read_8119" READ 4 + 5 + NODE __ttl_7d_chart_get_0 6 + SQL > 7 + 8 + % 9 + SELECT 10 + region, 11 + toStartOfInterval( 12 + toDateTime(cronTimestamp / 1000), 13 + INTERVAL {{ Int64(interval, 30) }} MINUTE 14 + ) as h, 15 + toUnixTimestamp64Milli(toDateTime64(h, 3)) as timestamp, 16 + round(quantile(0.5)(latency)) as p50Latency, 17 + round(quantile(0.75)(latency)) as p75Latency, 18 + round(quantile(0.9)(latency)) as p90Latency, 19 + round(quantile(0.95)(latency)) as p95Latency, 20 + round(quantile(0.99)(latency)) as p99Latency 21 + FROM __ttl_7d_mv__v0 22 + WHERE 23 + monitorId = {{ String(monitorId, '1') }} 24 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 25 + GROUP BY h, region 26 + ORDER BY h DESC 27 + 28 +
+25
packages/tinybird/pipes/__ttl_7d_count.pipe
··· 1 + VERSION 0 2 + 3 + DESCRIPTION > 4 + TODO: descripe what it is for! 5 + 6 + 7 + NODE __ttl_7d_count_0 8 + SQL > 9 + 10 + SELECT 11 + time, 12 + monitorId, 13 + url, 14 + countState() AS count, 15 + countState(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok 16 + FROM __ttl_45d_mv__v0 17 + GROUP BY 18 + time, 19 + monitorId, 20 + url 21 + 22 + TYPE materialized 23 + DATASOURCE __ttl_7d_count_mv__v0 24 + 25 +
+21
packages/tinybird/pipes/__ttl_7d_count_get.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_7d_count_get_0 4 + SQL > 5 + 6 + % 7 + SELECT time as day, countMerge(count) as count, countMerge(ok) as ok 8 + FROM __ttl_7d_count_mv__v0 9 + WHERE 10 + monitorId = {{ String(monitorId, '4') }} 11 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 12 + GROUP BY day 13 + ORDER BY day DESC 14 + WITH FILL 15 + FROM 16 + toStartOfDay(now()) 17 + TO toStartOfDay( 18 + date_sub(DAY, 7, now()) 19 + ) STEP INTERVAL -1 DAY 20 + 21 +
+38
packages/tinybird/pipes/__ttl_7d_metrics_get.pipe
··· 1 + VERSION 0 2 + 3 + NODE __ttl_7d_metrics_get_0 4 + SQL > 5 + 6 + % 7 + SELECT 8 + round(quantile(0.5)(latency)) as p50Latency, 9 + round(quantile(0.75)(latency)) as p75Latency, 10 + round(quantile(0.9)(latency)) as p90Latency, 11 + round(quantile(0.95)(latency)) as p95Latency, 12 + round(quantile(0.99)(latency)) as p99Latency, 13 + count() as count, 14 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 15 + max(cronTimestamp) AS lastTimestamp 16 + FROM __ttl_7d_mv__v0 17 + WHERE 18 + monitorId = {{ String(monitorId, '1') }} 19 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 20 + AND time >= toDateTime64(now() - INTERVAL 7 DAY, 3) 21 + UNION ALL 22 + SELECT 23 + round(quantile(0.5)(latency)) AS p50Latency, 24 + round(quantile(0.75)(latency)) AS p75Latency, 25 + round(quantile(0.9)(latency)) AS p90Latency, 26 + round(quantile(0.95)(latency)) AS p95Latency, 27 + round(quantile(0.99)(latency)) AS p99Latency, 28 + count() as count, 29 + count(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 30 + NULL as lastTimestamp -- no need to query the `lastTimestamp` as not relevant 31 + FROM __ttl_14d_mv__v0 32 + WHERE 33 + monitorId = {{ String(monitorId, '1') }} 34 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 35 + AND time >= toDateTime64(now() - INTERVAL 14 DAY, 3) 36 + AND time < toDateTime64(now() - INTERVAL 7 DAY, 3) 37 + 38 +
+25
packages/tinybird/pipes/__ttl_7d_metrics_get_by_region.pipe
··· 1 + VERSION 0 2 + 3 + TOKEN "__ttl_7d_metrics_get_by_region__v0_endpoint_read_4172" READ 4 + 5 + NODE __ttl_7d_metrics_get_by_region_0 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(multiIf((statusCode >= 200) AND (statusCode <= 299), 1, NULL)) AS ok, 18 + max(cronTimestamp) AS lastTimestamp 19 + FROM __ttl_7d_mv__v0 20 + WHERE 21 + monitorId = {{ String(monitorId, '1') }} 22 + {% if defined(url) %} AND url = {{ String(url) }} {% end %} 23 + GROUP BY region 24 + 25 +
-55
packages/tinybird/src/client.ts
··· 5 5 tbBuildMonitorList, 6 6 tbBuildPublicStatus, 7 7 tbBuildResponseDetails, 8 - tbBuildResponseGraph, 9 - tbBuildResponseList, 10 - tbBuildResponseTimeMetrics, 11 - tbBuildResponseTimeMetricsByRegion, 12 8 tbIngestPingResponse, 13 9 tbParameterHomeStats, 14 10 tbParameterMonitorList, 15 11 tbParameterPublicStatus, 16 12 tbParameterResponseDetails, 17 - tbParameterResponseGraph, 18 - tbParameterResponseList, 19 - tbParameterResponseTimeMetrics, 20 - tbParameterResponseTimeMetricsByRegion, 21 13 } from "./validation"; 22 14 23 15 // REMINDER: ··· 28 20 event: tbIngestPingResponse, 29 21 }); 30 22 31 - // TODO: add longer cache for NODE_ENV === "development" 32 - 33 - export function getResponseList(tb: Tinybird) { 34 - return tb.buildPipe({ 35 - pipe: "response_list__v2", 36 - parameters: tbParameterResponseList, 37 - data: tbBuildResponseList, 38 - opts: { 39 - // cache: "default", 40 - revalidate: 600, // 10 min cache 41 - }, 42 - }); 43 - } 44 - 45 23 export function getResponseDetails(tb: Tinybird) { 46 24 return tb.buildPipe({ 47 25 pipe: "response_details__v0", ··· 53 31 }); 54 32 } 55 33 56 - export function getResponseGraph(tb: Tinybird) { 57 - return tb.buildPipe({ 58 - pipe: "response_graph__v0", 59 - parameters: tbParameterResponseGraph, 60 - data: tbBuildResponseGraph, 61 - opts: { 62 - revalidate: 60, // 1 min cache 63 - }, 64 - }); 65 - } 66 - 67 34 export function getMonitorList(tb: Tinybird) { 68 35 return tb.buildPipe({ 69 36 pipe: "status_timezone__v1", ··· 113 80 data: tbBuildPublicStatus, 114 81 }); 115 82 } 116 - 117 - export function getResponseTimeMetrics(tb: Tinybird) { 118 - return tb.buildPipe({ 119 - pipe: "response_time_metrics__v0", 120 - parameters: tbParameterResponseTimeMetrics, 121 - data: tbBuildResponseTimeMetrics, 122 - opts: { 123 - revalidate: 30, // 30 sec cache - mostly for timestamp metric 124 - }, 125 - }); 126 - } 127 - 128 - export function getResponseTimeMetricsByRegion(tb: Tinybird) { 129 - return tb.buildPipe({ 130 - pipe: "response_time_metrics_by_region__v0", 131 - parameters: tbParameterResponseTimeMetricsByRegion, 132 - data: tbBuildResponseTimeMetricsByRegion, 133 - opts: { 134 - revalidate: 30, // 30 sec cache - mostly for timestamp metric 135 - }, 136 - }); 137 - }
+3
packages/tinybird/src/index.ts
··· 2 2 export * from "./validation"; 3 3 export * from "./audit-log"; 4 4 export * from "@chronark/zod-bird"; 5 + 6 + // OSTinybird: for specific pipes 7 + export * from "./os-client";
+260
packages/tinybird/src/os-client.ts
··· 1 + import { Tinybird } from "@chronark/zod-bird"; 2 + import { z } from "zod"; 3 + 4 + import { flyRegions } from "@openstatus/utils"; 5 + 6 + const MIN_CACHE = 30; // 30s 7 + const DEFAULT_CACHE = 120; // 2min 8 + 9 + export const latencySchema = z.object({ 10 + p50Latency: z.number().int().nullable(), 11 + p75Latency: z.number().int().nullable(), 12 + p90Latency: z.number().int().nullable(), 13 + p95Latency: z.number().int().nullable(), 14 + p99Latency: z.number().int().nullable(), 15 + }); 16 + 17 + export const timingSchema = z.object({ 18 + dnsStart: z.number(), 19 + dnsDone: z.number(), 20 + connectStart: z.number(), 21 + connectDone: z.number(), 22 + tlsHandshakeStart: z.number(), 23 + tlsHandshakeDone: z.number(), 24 + firstByteStart: z.number(), 25 + firstByteDone: z.number(), 26 + transferStart: z.number(), 27 + transferDone: z.number(), 28 + }); 29 + 30 + export class OSTinybird { 31 + private tb: Tinybird; 32 + 33 + // FIXME: use Tinybird instead with super(args) maybe 34 + constructor(private args: { token: string; baseUrl?: string | undefined }) { 35 + this.tb = new Tinybird(args); 36 + } 37 + 38 + endpointChart(period: "1h" | "1d" | "3d" | "7d" | "14d") { 39 + const parameters = z.object({ 40 + interval: z.number().int().optional(), 41 + monitorId: z.string(), 42 + url: z.string().optional(), 43 + }); 44 + 45 + return async (props: z.infer<typeof parameters>) => { 46 + try { 47 + const res = await this.tb.buildPipe({ 48 + pipe: `__ttl_${period}_chart_get__v0`, 49 + parameters, 50 + data: z 51 + .object({ 52 + region: z.enum(flyRegions), 53 + timestamp: z.number().int(), 54 + }) 55 + .merge(latencySchema), 56 + opts: { 57 + revalidate: DEFAULT_CACHE, 58 + }, 59 + })(props); 60 + return res.data; 61 + } catch (e) { 62 + console.error(e); 63 + } 64 + }; 65 + } 66 + 67 + endpointMetrics(period: "1h" | "1d" | "3d" | "7d" | "14d") { 68 + const parameters = z.object({ 69 + monitorId: z.string(), 70 + url: z.string().optional(), 71 + }); 72 + 73 + return async (props: z.infer<typeof parameters>) => { 74 + try { 75 + const res = await this.tb.buildPipe({ 76 + pipe: `__ttl_${period}_metrics_get__v0`, 77 + parameters, 78 + data: z 79 + .object({ 80 + region: z.enum(flyRegions).default("ams"), // FIXME: default 81 + count: z.number().default(0), 82 + ok: z.number().default(0), 83 + lastTimestamp: z.number().int().nullable(), 84 + }) 85 + .merge(latencySchema), 86 + opts: { 87 + revalidate: DEFAULT_CACHE, 88 + }, 89 + })(props); 90 + return res.data; 91 + } catch (e) { 92 + console.error(e); 93 + } 94 + }; 95 + } 96 + 97 + endpointMetricsByRegion(period: "1h" | "1d" | "3d" | "7d" | "14d") { 98 + const parameters = z.object({ 99 + monitorId: z.string(), 100 + url: z.string().optional(), 101 + }); 102 + 103 + return async (props: z.infer<typeof parameters>) => { 104 + try { 105 + const res = await this.tb.buildPipe({ 106 + pipe: `__ttl_${period}_metrics_get_by_region__v0`, 107 + parameters, 108 + data: z 109 + .object({ 110 + region: z.enum(flyRegions), 111 + count: z.number().default(0), 112 + ok: z.number().default(0), 113 + lastTimestamp: z.number().int().optional(), // FIXME: optional 114 + }) 115 + .merge(latencySchema), 116 + opts: { 117 + revalidate: DEFAULT_CACHE, 118 + }, 119 + })(props); 120 + return res.data; 121 + } catch (e) { 122 + console.error(e); 123 + } 124 + }; 125 + } 126 + 127 + endpointStatusPeriod(period: "7d" | "45d") { 128 + const parameters = z.object({ 129 + monitorId: z.string(), 130 + url: z.string().optional(), 131 + }); 132 + 133 + return async (props: z.infer<typeof parameters>) => { 134 + try { 135 + const res = await this.tb.buildPipe({ 136 + pipe: `__ttl_${period}_count_get__v0`, 137 + parameters, 138 + data: z.object({ 139 + day: z.string().transform((val) => { 140 + // That's a hack because clickhouse return the date in UTC but in shitty format (2021-09-01 00:00:00) 141 + return new Date(`${val} GMT`).toISOString(); 142 + }), 143 + count: z.number().default(0), 144 + ok: z.number().default(0), 145 + }), 146 + opts: { 147 + revalidate: DEFAULT_CACHE, 148 + }, 149 + })(props); 150 + return res.data; 151 + } catch (e) { 152 + console.error(e); 153 + } 154 + }; 155 + } 156 + 157 + // TBH: not sure if we need more than 1d for that, better allow the user 158 + // to click on a specific region and time 159 + endpointList(period: "1h" | "1d") { 160 + const parameters = z.object({ 161 + monitorId: z.string(), 162 + url: z.string().optional(), 163 + }); 164 + 165 + return async (props: z.infer<typeof parameters>) => { 166 + try { 167 + const res = await this.tb.buildPipe({ 168 + pipe: `__ttl_${period}_list_get__v0`, 169 + parameters, 170 + data: z.object({ 171 + latency: z.number().int(), // in ms 172 + monitorId: z.string(), 173 + region: z.enum(flyRegions), 174 + statusCode: z.number().int().nullable().default(null), 175 + timestamp: z.number().int(), 176 + url: z.string().url(), 177 + workspaceId: z.string(), 178 + cronTimestamp: z.number().int().nullable().default(Date.now()), 179 + }), 180 + opts: { 181 + revalidate: DEFAULT_CACHE, 182 + }, 183 + })(props); 184 + return res.data; 185 + } catch (e) { 186 + console.error(e); 187 + } 188 + }; 189 + } 190 + 191 + // FEATURE: include a simple Widget for the user to refresh the page or display on the top of the page 192 + // type: "workspace" | "monitor" 193 + endpointLastCronTimestamp(type: "workspace") { 194 + const parameters = z.object({ workspaceId: z.string() }); 195 + 196 + return async (props: z.infer<typeof parameters>) => { 197 + try { 198 + const res = await this.tb.buildPipe({ 199 + pipe: `__ttl_1h_last_timestamp_${type}_get__v0`, 200 + parameters, 201 + data: z.object({ cronTimestamp: z.number().int() }), 202 + opts: { 203 + revalidate: MIN_CACHE, 204 + }, 205 + })(props); 206 + return res.data; 207 + } catch (e) { 208 + console.error(e); 209 + } 210 + }; 211 + } 212 + 213 + endpointResponseDetails() { 214 + return this.tb.buildPipe({ 215 + pipe: "response_details__v0", // TODO: make it also a bit dynamic to avoid query through too much data 216 + parameters: z.object({ 217 + monitorId: z.string().default(""), 218 + url: z.string().url().optional(), 219 + region: z.enum(flyRegions).optional(), 220 + cronTimestamp: z.number().int().optional(), 221 + }), 222 + data: z.object({ 223 + monitorId: z.string().default(""), 224 + url: z.string().url().optional(), 225 + region: z.enum(flyRegions).optional(), 226 + cronTimestamp: z.number().int().optional(), 227 + message: z.string().nullable().optional(), 228 + headers: z 229 + .string() 230 + .nullable() 231 + .optional() 232 + .transform((val) => { 233 + if (!val) return null; 234 + const value = z 235 + .record(z.string(), z.string()) 236 + .safeParse(JSON.parse(val)); 237 + if (value.success) return value.data; 238 + return null; 239 + }), 240 + timing: z 241 + .string() 242 + .nullable() 243 + .optional() 244 + .transform((val) => { 245 + if (!val) return null; 246 + const value = timingSchema.safeParse(JSON.parse(val)); 247 + if (value.success) return value.data; 248 + return null; 249 + }), 250 + }), 251 + opts: { 252 + revalidate: DEFAULT_CACHE, 253 + }, 254 + }); 255 + } 256 + } 257 + 258 + /** 259 + * TODO: if it would be possible to... 260 + */
+1 -2
packages/tinybird/src/validation.ts
··· 113 113 }); 114 114 115 115 export const latencyMetrics = z.object({ 116 - avgLatency: z.number().int().nullable(), 116 + p50Latency: z.number().int().nullable(), 117 117 p75Latency: z.number().int().nullable(), 118 118 p90Latency: z.number().int().nullable(), 119 119 p95Latency: z.number().int().nullable(), ··· 213 213 count: z.number().int(), 214 214 ok: z.number().int(), 215 215 lastTimestamp: z.number().int().nullable().optional(), 216 - time: z.number().int(), // only to sort the data - cannot be done on server because of UNION ALL 217 216 }) 218 217 .merge(latencyMetrics); 219 218
+7
pnpm-lock.yaml
··· 391 391 '@openstatus/plans': 392 392 specifier: workspace:* 393 393 version: link:../plans 394 + '@openstatus/tinybird': 395 + specifier: workspace:* 396 + version: link:../tinybird 394 397 '@t3-oss/env-core': 395 398 specifier: 0.7.0 396 399 version: 0.7.0(typescript@5.2.2)(zod@3.22.2) ··· 1389 1392 peerDependencies: 1390 1393 '@effect-ts/otel-node': '*' 1391 1394 peerDependenciesMeta: 1395 + '@effect-ts/core': 1396 + optional: true 1397 + '@effect-ts/otel': 1398 + optional: true 1392 1399 '@effect-ts/otel-node': 1393 1400 optional: true 1394 1401 dependencies: