Openstatus
www.openstatus.dev
1"use client";
2
3import type { MetricCard } from "@/components/metric/metric-card";
4import { formatDateTime, formatMilliseconds } from "@/lib/formatter";
5import type { RouterOutputs } from "@openstatus/api";
6import { monitorRegions } from "@openstatus/db/src/schema/constants";
7import { startOfDay, subDays } from "date-fns";
8import type { RegionMetric } from "./region-metrics";
9
10export const STATUS = ["success", "error", "degraded"] as const;
11export const PERIODS = ["1d", "7d", "14d"] as const;
12export const REGIONS =
13 monitorRegions as unknown as (typeof monitorRegions)[number][];
14export const PERCENTILES = ["p50", "p75", "p90", "p95", "p99"] as const;
15export const INTERVALS = [5, 15, 30, 60, 120, 240, 480, 1440] as const;
16export const TRIGGER = ["api", "cron"] as const;
17
18const PERCENTILE_MAP = {
19 p50: "p50Latency",
20 p75: "p75Latency",
21 p90: "p90Latency",
22 p95: "p95Latency",
23 p99: "p99Latency",
24} as const;
25
26// FIXME: rename pipe return values
27
28export function mapMetrics(metrics: RouterOutputs["tinybird"]["metrics"]) {
29 return metrics.data?.map((metric) => {
30 return {
31 p50: metric.p50Latency,
32 p75: metric.p75Latency,
33 p90: metric.p90Latency,
34 p95: metric.p95Latency,
35 p99: metric.p99Latency,
36 total: metric.count,
37 uptime: (metric.success + metric.degraded) / metric.count,
38 degraded: metric.degraded,
39 error: metric.error,
40 lastTimestamp: metric.lastTimestamp,
41 };
42 });
43}
44
45export const metricsCards = {
46 uptime: {
47 label: "UPTIME",
48 variant: "success",
49 },
50 degraded: {
51 label: "DEGRADED",
52 variant: "warning",
53 },
54 error: {
55 label: "FAILING",
56 variant: "destructive",
57 },
58 total: {
59 label: "REQUESTS",
60 variant: "default",
61 },
62 lastTimestamp: {
63 label: "LAST CHECKED",
64 variant: "ghost",
65 },
66 p50: {
67 label: "P50",
68 variant: "default",
69 },
70 p75: {
71 label: "P75",
72 variant: "default",
73 },
74 p90: {
75 label: "P90",
76 variant: "default",
77 },
78 p95: {
79 label: "P95",
80 variant: "default",
81 },
82 p99: {
83 label: "P99",
84 variant: "default",
85 },
86} as const satisfies Record<
87 keyof ReturnType<typeof mapMetrics>[number],
88 {
89 label: string;
90 variant: React.ComponentProps<typeof MetricCard>["variant"];
91 }
92>;
93
94export function mapUptime(status: RouterOutputs["tinybird"]["uptime"]) {
95 return status.data
96 .map((status) => {
97 return {
98 ...status,
99 ok: status.success,
100 interval: formatDateTime(status.interval),
101 total: status.success + status.error + status.degraded,
102 };
103 })
104 .reverse();
105}
106
107/**
108 * Transform Tinybird `metricsRegions` response into RegionMetric[] for UI.
109 */
110export function mapRegionMetrics(
111 timeline: RouterOutputs["tinybird"]["metricsRegions"] | undefined,
112 regions: string[],
113 percentile: (typeof PERCENTILES)[number],
114): RegionMetric[] {
115 if (!timeline)
116 return (regions
117 .sort((a, b) => a.localeCompare(b))
118 .map((region) => ({
119 region,
120 p50: 0,
121 p90: 0,
122 p99: 0,
123 trend: [] as {
124 latency: number;
125 timestamp: number;
126 [key: string]: number;
127 }[],
128 })) ?? []) satisfies RegionMetric[];
129
130 type TimelineRow = (typeof timeline.data)[number];
131
132 const map = new Map<
133 string,
134 {
135 region: string;
136 p50: number;
137 p90: number;
138 p99: number;
139 trend: {
140 latency: number;
141 timestamp: number;
142 [key: string]: number;
143 }[];
144 }
145 >();
146
147 (timeline.data as TimelineRow[])
148 .filter((row) => regions.includes(row.region))
149 .sort((a, b) => a.region.localeCompare(b.region))
150 .forEach((row) => {
151 const region = row.region;
152 const entry = map.get(region) ?? {
153 region,
154 p50: 0,
155 p90: 0,
156 p99: 0,
157 trend: [],
158 };
159
160 entry.trend.push({
161 latency: row[PERCENTILE_MAP[percentile]] ?? 0,
162 timestamp: row.timestamp,
163 [region]: row[PERCENTILE_MAP[percentile]] ?? 0,
164 });
165
166 entry.p50 += row.p50Latency ?? 0;
167 entry.p90 += row.p90Latency ?? 0;
168 entry.p99 += row.p99Latency ?? 0;
169
170 map.set(region, entry);
171 });
172
173 map.forEach((entry) => {
174 const count = entry.trend.length || 1;
175 entry.trend.reverse();
176 entry.p50 = Math.round(entry.p50 / count);
177 entry.p90 = Math.round(entry.p90 / count);
178 entry.p99 = Math.round(entry.p99 / count);
179 });
180
181 return Array.from(map.values()) as RegionMetric[];
182}
183
184export function mapGlobalMetrics(
185 metrics: RouterOutputs["tinybird"]["globalMetrics"],
186) {
187 return metrics.data?.map((metric) => {
188 return {
189 p50: metric.p50Latency,
190 p75: metric.p75Latency,
191 p90: metric.p90Latency,
192 p95: metric.p95Latency,
193 p99: metric.p99Latency,
194 total: metric.count,
195 monitorId: metric.monitorId,
196 };
197 });
198}
199
200export type MonitorListMetric = {
201 title: string;
202 key: "degraded" | "error" | "active" | "inactive" | "p95";
203 value: number | string | undefined;
204 variant: React.ComponentProps<typeof MetricCard>["variant"];
205};
206
207export const globalCards = [
208 "active",
209 "degraded",
210 "error",
211 "inactive",
212 "p95",
213] as const;
214
215export const metricsGlobalCards: Record<
216 (typeof globalCards)[number],
217 {
218 title: string;
219 key: (typeof globalCards)[number];
220 }
221> = {
222 active: {
223 title: "Normal",
224 key: "active" as const,
225 },
226 degraded: {
227 title: "Degraded",
228 key: "degraded" as const,
229 },
230 error: {
231 title: "Failing",
232 key: "error" as const,
233 },
234 inactive: {
235 title: "Inactive",
236 key: "inactive" as const,
237 },
238 p95: {
239 title: "Slowest P95",
240 key: "p95" as const,
241 },
242};
243
244/**
245 * Build the metric cards data that is shown on the monitors list page.
246 */
247export function getMonitorListMetrics(
248 monitors: RouterOutputs["monitor"]["list"] = [],
249 data: {
250 p95Latency: number;
251 monitorId: string;
252 }[] = [],
253): readonly MonitorListMetric[] {
254 const variantMap: Record<
255 (typeof globalCards)[number],
256 React.ComponentProps<typeof MetricCard>["variant"]
257 > = {
258 active: "success",
259 degraded: "warning",
260 error: "destructive",
261 inactive: "default",
262 p95: "ghost",
263 } as const;
264
265 return globalCards.map((key) => {
266 let value: number | string | undefined;
267 switch (key) {
268 case "active":
269 value = monitors.filter(
270 (m) => m.status === "active" && m.active,
271 ).length;
272 break;
273 case "degraded":
274 value = monitors.filter(
275 (m) => m.status === "degraded" && m.active,
276 ).length;
277 break;
278 case "error":
279 value = monitors.filter((m) => m.status === "error" && m.active).length;
280 break;
281 case "inactive":
282 value = monitors.filter((m) => m.active === false).length;
283 break;
284 case "p95":
285 const p95 = data.sort((a, b) => b.p95Latency - a.p95Latency)[0]
286 ?.p95Latency;
287 value = p95 ? formatMilliseconds(p95) : "N/A";
288 break;
289 }
290
291 return {
292 title: metricsGlobalCards[key].title,
293 key,
294 value,
295 variant: variantMap[key],
296 } as const;
297 }) as readonly MonitorListMetric[];
298}
299
300export function mapLatency(
301 latency: RouterOutputs["tinybird"]["metricsLatency"],
302 percentile: (typeof PERCENTILES)[number],
303) {
304 return latency.data?.map((metric) => {
305 return {
306 timestamp: formatDateTime(new Date(metric.timestamp)),
307 latency: metric[PERCENTILE_MAP[percentile]],
308 };
309 });
310}
311
312export function mapTimingPhases(
313 timingPhases: RouterOutputs["tinybird"]["metricsTimingPhases"],
314 percentile: (typeof PERCENTILES)[number],
315) {
316 return timingPhases.data?.map((metric) => {
317 return {
318 timestamp: formatDateTime(new Date(metric.timestamp)),
319 dns: metric[`${percentile}Dns`],
320 ttfb: metric[`${percentile}Ttfb`],
321 transfer: metric[`${percentile}Transfer`],
322 connect: metric[`${percentile}Connect`],
323 tls: metric[`${percentile}Tls`],
324 };
325 });
326}
327
328export const periodToInterval = {
329 "1d": 60,
330 "7d": 240,
331 "14d": 480,
332} satisfies Record<(typeof PERIODS)[number], number>;
333
334export const periodToFromDate = {
335 "1d": startOfDay(subDays(new Date(), 1)),
336 "7d": startOfDay(subDays(new Date(), 7)),
337 "14d": startOfDay(subDays(new Date(), 14)),
338} satisfies Record<(typeof PERIODS)[number], Date>;