Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
9.4 kB ยท 368 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369"use client";
import type { RouterOutputs } from "@openstatus/api";import { monitorRegions } from "@openstatus/db/src/schema/constants";import { startOfDay, subDays } from "date-fns";
import type { MetricCard } from "@/components/metric/metric-card";import { formatDateTime, formatMilliseconds } from "@/lib/formatter";
import type { RegionMetric } from "./region-metrics";
export const STATUS = ["success", "error", "degraded"] as const;export const PERIODS = ["1d", "7d", "14d", "30d", "90d"] as const;// 30d/90d require a paid plan; the dropdown gates them for free workspaces.export const PAID_PERIODS = ["30d", "90d"] as const;
// longest window a free workspace may view; paid periods snap back to this.export const FREE_MAX_PERIOD = "14d" as const;
export type Period = (typeof PERIODS)[number];export type PaidPeriod = (typeof PAID_PERIODS)[number];
// widen to string[] so any Period can be tested, not just the paid literalsexport function isPaidPeriod(period: string): period is PaidPeriod { return (PAID_PERIODS as readonly string[]).includes(period);}export const REGIONS = monitorRegions as unknown as (typeof monitorRegions)[number][];export const PERCENTILES = ["p50", "p75", "p90", "p95", "p99"] as const;export const INTERVALS = [5, 15, 30, 60, 120, 240, 480, 1440] as const;export const TRIGGER = ["api", "cron"] as const;
const PERCENTILE_MAP = { p50: "p50Latency", p75: "p75Latency", p90: "p90Latency", p95: "p95Latency", p99: "p99Latency",} as const;
// FIXME: rename pipe return values
export function mapMetrics(metrics: RouterOutputs["tinybird"]["metrics"]) { return metrics.data?.map((metric) => { return { p50: metric.p50Latency, p75: metric.p75Latency, p90: metric.p90Latency, p95: metric.p95Latency, p99: metric.p99Latency, total: metric.count, uptime: (metric.success + metric.degraded) / metric.count, degraded: metric.degraded, error: metric.error, lastTimestamp: metric.lastTimestamp, }; });}
export const metricsCards = { uptime: { label: "UPTIME", variant: "success", }, degraded: { label: "DEGRADED", variant: "warning", }, error: { label: "FAILING", variant: "destructive", }, total: { label: "REQUESTS", variant: "default", }, lastTimestamp: { label: "LAST CHECKED", variant: "ghost", }, p50: { label: "P50", variant: "default", }, p75: { label: "P75", variant: "default", }, p90: { label: "P90", variant: "default", }, p95: { label: "P95", variant: "default", }, p99: { label: "P99", variant: "default", },} as const satisfies Record< keyof ReturnType<typeof mapMetrics>[number], { label: string; variant: React.ComponentProps<typeof MetricCard>["variant"]; }>;
export function mapUptime(status: RouterOutputs["tinybird"]["uptime"]) { return status.data .map((status) => { return { ...status, ok: status.success, interval: formatDateTime(status.interval), total: status.success + status.error + status.degraded, }; }) .reverse();}
/** * Transform Tinybird `metricsRegions` response into RegionMetric[] for UI. */export function mapRegionMetrics( timeline: RouterOutputs["tinybird"]["metricsRegions"] | undefined, regions: string[], percentile: (typeof PERCENTILES)[number],): RegionMetric[] { if (!timeline) return (regions .sort((a, b) => a.localeCompare(b)) .map((region) => ({ region, p50: 0, p90: 0, p99: 0, trend: [] as { latency: number; timestamp: number; [key: string]: number; }[], })) ?? []) satisfies RegionMetric[];
type TimelineRow = (typeof timeline.data)[number];
const map = new Map< string, { region: string; p50: number; p90: number; p99: number; trend: { latency: number; timestamp: number; [key: string]: number; }[]; } >();
(timeline.data as TimelineRow[]) .filter((row) => regions.includes(row.region)) .sort((a, b) => a.region.localeCompare(b.region)) .forEach((row) => { const region = row.region; const entry = map.get(region) ?? { region, p50: 0, p90: 0, p99: 0, trend: [], };
entry.trend.push({ latency: row[PERCENTILE_MAP[percentile]] ?? 0, timestamp: row.timestamp, [region]: row[PERCENTILE_MAP[percentile]] ?? 0, });
entry.p50 += row.p50Latency ?? 0; entry.p90 += row.p90Latency ?? 0; entry.p99 += row.p99Latency ?? 0;
map.set(region, entry); });
map.forEach((entry) => { const count = entry.trend.length || 1; entry.trend.reverse(); entry.p50 = Math.round(entry.p50 / count); entry.p90 = Math.round(entry.p90 / count); entry.p99 = Math.round(entry.p99 / count); });
return Array.from(map.values()) as RegionMetric[];}
export function mapGlobalMetrics( metrics: RouterOutputs["tinybird"]["globalMetrics"],) { return metrics.data?.map((metric) => { return { p50: metric.p50Latency, p75: metric.p75Latency, p90: metric.p90Latency, p95: metric.p95Latency, p99: metric.p99Latency, total: metric.count, monitorId: metric.monitorId, }; });}
export type MonitorListMetric = { title: string; key: "degraded" | "error" | "active" | "inactive" | "p95"; value: number | string | undefined; variant: React.ComponentProps<typeof MetricCard>["variant"];};
export const globalCards = [ "active", "degraded", "error", "inactive", "p95",] as const;
export const metricsGlobalCards: Record< (typeof globalCards)[number], { title: string; key: (typeof globalCards)[number]; }> = { active: { title: "Normal", key: "active" as const, }, degraded: { title: "Degraded", key: "degraded" as const, }, error: { title: "Failing", key: "error" as const, }, inactive: { title: "Inactive", key: "inactive" as const, }, p95: { title: "Slowest P95", key: "p95" as const, },};
/** * Build the metric cards data that is shown on the monitors list page. */export function getMonitorListMetrics( monitors: RouterOutputs["monitor"]["list"] = [], data: { p95Latency: number; monitorId: string; }[] = [],): readonly MonitorListMetric[] { const variantMap: Record< (typeof globalCards)[number], React.ComponentProps<typeof MetricCard>["variant"] > = { active: "success", degraded: "warning", error: "destructive", inactive: "default", p95: "ghost", } as const;
return globalCards.map((key) => { let value: number | string | undefined; switch (key) { case "active": value = monitors.filter( (m) => m.status === "active" && m.active, ).length; break; case "degraded": value = monitors.filter( (m) => m.status === "degraded" && m.active, ).length; break; case "error": value = monitors.filter((m) => m.status === "error" && m.active).length; break; case "inactive": value = monitors.filter((m) => m.active === false).length; break; case "p95": const p95 = data.sort((a, b) => b.p95Latency - a.p95Latency)[0] ?.p95Latency; value = p95 ? formatMilliseconds(p95) : "N/A"; break; }
return { title: metricsGlobalCards[key].title, key, value, variant: variantMap[key], } as const; }) as readonly MonitorListMetric[];}
export function mapLatency( latency: RouterOutputs["tinybird"]["metricsLatency"], percentile: (typeof PERCENTILES)[number],) { return latency.data?.map((metric) => { return { timestamp: formatDateTime(new Date(metric.timestamp)), latency: metric[PERCENTILE_MAP[percentile]], }; });}
export function mapTimingPhases( timingPhases: RouterOutputs["tinybird"]["metricsTimingPhases"], percentile: (typeof PERCENTILES)[number],) { return timingPhases.data?.map((metric) => { return { timestamp: formatDateTime(new Date(metric.timestamp)), dns: metric[`${percentile}Dns`], ttfb: metric[`${percentile}Ttfb`], transfer: metric[`${percentile}Transfer`], connect: metric[`${percentile}Connect`], tls: metric[`${percentile}Tls`], }; });}
export const periodToInterval = { "1d": 60, "7d": 240, "14d": 480, "30d": 1440, "90d": 1440,} satisfies Record<(typeof PERIODS)[number], number>;
export const periodToFromDate = { "1d": startOfDay(subDays(new Date(), 1)), "7d": startOfDay(subDays(new Date(), 7)), "14d": startOfDay(subDays(new Date(), 14)), "30d": startOfDay(subDays(new Date(), 30)), "90d": startOfDay(subDays(new Date(), 90)),} satisfies Record<(typeof PERIODS)[number], Date>;
// Minimum selectable resolution (minutes) per period. Long windows floor the// interval so charts stay readable and queries bounded. Mirrors the server-side// clamp in `@openstatus/api` tinybird router โ keep the two in sync.export const periodToMinInterval = { "1d": 5, "7d": 5, "14d": 5, "30d": 240, "90d": 1440,} satisfies Record<(typeof PERIODS)[number], (typeof INTERVALS)[number]>;