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.
23 kB ยท 721 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722import type { PageComponentImpact } from "@openstatus/db/src/schema";import { impactToStatusType, impactUptimeWeight, LEGACY_IMPACT_WEIGHT, worstImpact,} from "@openstatus/db/src/schema";import { clipToCoverage, type CoverageSegment, downtimeIntervals, type Event, MS_PER_DAY, probeDowntimeIntervals, type StatusData, type UptimeWindow, type WeightedInterval, dayCoverage, durationDowntimeMs, floorPct, getHighestPriorityStatus, getWorstVariant, isDateWithinEvent, mergedDowntimeMs, reportEventDayImpact, reportEventDayStatus, reportsOnlyDowntimeMs, requestsTally,} from "@openstatus/services/status-timeline";
export * from "@openstatus/services/status-timeline";
// Status pages must render even when Tinybird is degraded. Read latency above// this budget is treated as an outage and the page falls back to manual mode.export const TINYBIRD_FALLBACK_TIMEOUT_MS = 5_000;
// Discriminated result of a guarded Tinybird read: `ok: true` carries the data,// `ok: false` (timeout or error) carries `null` and signals manual-mode fallback.export type TinybirdResult<T> = | { ok: true; data: T } | { ok: false; data: null };
// Races a Tinybird read against the fallback budget. `ok: false` (timeout or// thrown error) signals the caller to serve manual mode โ DB-authored events// only โ instead of hanging or 500ing on Tinybird.export async function withTinybirdFallback<T>( fetch: () => Promise<T>, timeoutMs = TINYBIRD_FALLBACK_TIMEOUT_MS,): Promise<TinybirdResult<T>> { let timer: ReturnType<typeof setTimeout> | undefined; try { const data = await Promise.race([ fetch(), new Promise<never>((_, reject) => { timer = setTimeout( () => reject(new Error("tinybird timeout")), timeoutMs, ); }), ]); return { ok: true, data }; } catch (err) { console.error("[status-page] tinybird unhealthy, using manual mode:", err); return { ok: false, data: null }; } finally { clearTimeout(timer); }}
type UptimeData = { day: string; events: Event[]; bar: { status: "success" | "degraded" | "error" | "info" | "empty"; height: number; // percentage }[]; card: { status: "success" | "degraded" | "error" | "info" | "empty"; value: string; /** Worst report impact of the day โ refines the generic status label. */ impact?: PageComponentImpact; }[];};
const MILLISECONDS_PER_MINUTE = 1000 * 60;
// Helper to format numbersfunction formatNumber(num: number): string { if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; if (num >= 1000) return `${(num / 1000).toFixed(1)}k`; return num.toString();}
// Helper to check if date is todayfunction isToday(date: Date): boolean { const today = new Date(); return ( date.getUTCDate() === today.getUTCDate() && date.getUTCMonth() === today.getUTCMonth() && date.getUTCFullYear() === today.getUTCFullYear() );}
// Helper to format duration from minutesfunction formatDuration(minutes: number): string { if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; if (remainingMinutes === 0) return `${hours}h`; return `${hours}h ${remainingMinutes}m`;}
// Helper to calculate total minutes in a day (handles today vs past days)function getTotalMinutesInDay(date: Date): number { const now = new Date(); const startOfDay = new Date(date); startOfDay.setUTCHours(0, 0, 0, 0);
if (isToday(date)) { const minutesElapsed = Math.floor( (now.getTime() - startOfDay.getTime()) / MILLISECONDS_PER_MINUTE, ); return minutesElapsed; } return 24 * 60;}
// Helper to calculate duration in minutes for a specific event typefunction calculateEventDurationMinutes(events: Event[], date: Date): number { const totalDuration = getTotalEventsDurationMs(events, date); return Math.round(totalDuration / MILLISECONDS_PER_MINUTE);}
// Helper to calculate maintenance duration in minutes for a specific dayfunction getMaintenanceDurationMinutes( maintenances: Event[], date: Date,): number { return calculateEventDurationMinutes(maintenances, date);}
// Helper to get adjusted total minutes accounting for maintenancefunction getAdjustedTotalMinutesInDay( date: Date, maintenances: Event[],): number { const totalMinutes = getTotalMinutesInDay(date); const maintenanceMinutes = getMaintenanceDurationMinutes(maintenances, date); return Math.max(0, totalMinutes - maintenanceMinutes);}
function getTotalEventsDurationMs(events: Event[], date: Date): number { if (events.length === 0) return 0;
const startOfDay = new Date(date); startOfDay.setUTCHours(0, 0, 0, 0);
const endOfDay = new Date(date); endOfDay.setUTCHours(23, 59, 59, 999);
const total = events.reduce((acc, curr) => { if (!curr.from) return acc;
const eventStart = new Date(curr.from); const eventEnd = curr.to ? new Date(curr.to) : new Date();
// Only count events that overlap with this date if ( eventEnd.getTime() < startOfDay.getTime() || eventStart.getTime() > endOfDay.getTime() ) { return acc; }
// Calculate the overlapping duration within the date boundaries const overlapStart = Math.max(eventStart.getTime(), startOfDay.getTime()); const overlapEnd = Math.min(eventEnd.getTime(), endOfDay.getTime());
const duration = overlapEnd - overlapStart; return acc + Math.max(0, duration); }, 0);
// Cap at 24 hours per day return Math.min(total, MS_PER_DAY);}
export function setDataByType({ events, data, cardType, barType,}: { events: Event[]; data: StatusData[]; cardType: "requests" | "duration" | "dominant" | "manual"; barType: "absolute" | "dominant" | "manual";}): UptimeData[] { // Helper functions moved inside to share inputs and avoid parameter passing function createEventSegments( incidents: Event[], reports: Event[], maintenances: Event[], date: Date, ): Array<{ status: "info" | "degraded" | "error"; count: number }> { // impact reports contribute per-interval slices to each color bucket, so a // 1h major_outage inside a 24h report only paints 1h red; legacy reports // keep their full duration in the degraded bucket; operational slices drop const degradedSlices: Event[] = []; const errorSlices: Event[] = []; for (const report of reports) { if (!report.impactIntervals) { degradedSlices.push(report); continue; } for (const iv of report.impactIntervals) { const color = impactToStatusType(iv.impact); if (color === "success") continue; const slice = { ...report, from: iv.from, to: iv.to }; if (!isDateWithinEvent(date, slice)) continue; (color === "error" ? errorSlices : degradedSlices).push(slice); } }
const eventTypes = [ { status: "info" as const, events: maintenances }, { status: "degraded" as const, events: degradedSlices }, { status: "error" as const, events: [...incidents, ...errorSlices], }, ];
return eventTypes .filter(({ events }) => events.length > 0) .map(({ status, events }) => ({ status, count: getTotalEventsDurationMs(events, date), })); }
function createErrorOnlyBarData( errorSegmentCount: number, ): UptimeData["bar"] { return [ { status: "success" as const, height: ((MS_PER_DAY - errorSegmentCount) / MS_PER_DAY) * 100, }, { status: "error" as const, height: (errorSegmentCount / MS_PER_DAY) * 100, }, ]; }
function createProportionalBarData( segments: Array<{ status: "info" | "degraded" | "error"; count: number }>, ): UptimeData["bar"] { // Downtime keeps its true proportion of the day; maintenance/reports are // highlight events that fill the remaining space (no uptime shown). const errorMs = segments .filter((segment) => segment.status === "error") .reduce((sum, segment) => sum + segment.count, 0); const errorHeight = (Math.min(errorMs, MS_PER_DAY) / MS_PER_DAY) * 100; const remainingHeight = Math.max(0, 100 - errorHeight);
const highlightSegments = segments.filter( (segment) => segment.status !== "error", ); const highlightTotal = highlightSegments.reduce( (sum, segment) => sum + segment.count, 0, );
return segments.map((segment) => { if (segment.status === "error") { return { status: segment.status, height: errorHeight }; } // instant highlight events (no duration) split the remaining space evenly return { status: segment.status, height: highlightTotal > 0 ? (segment.count / highlightTotal) * remainingHeight : remainingHeight / highlightSegments.length, }; }); }
function createStatusSegments( dayData: StatusData, ): Array<{ status: "success" | "degraded" | "error"; count: number }> { return [ { status: "success" as const, count: dayData.ok }, { status: "degraded" as const, count: dayData.degraded }, { status: "error" as const, count: dayData.error }, ]; }
function segmentsToBarData( segments: Array<{ status: "success" | "degraded" | "error"; count: number; }>, total: number, ): UptimeData["bar"] { return segments .filter((segment) => segment.count > 0) .map((segment) => ({ status: segment.status, height: (segment.count / total) * 100, })); }
function createOperationalBarData(): UptimeData["bar"] { return [ { status: "success", height: 100, }, ]; }
function createEmptyBarData(): UptimeData["bar"] { return [ { status: "empty", height: 100, }, ]; }
function createEmptyCardData( eventStatus?: "error" | "degraded" | "info" | "success" | "empty", ): UptimeData["card"] { return [{ status: eventStatus ?? "empty", value: "" }]; }
function createRequestEntries(dayData: StatusData): Array<{ status: "success" | "degraded" | "error" | "info"; count: number; }> { return [ { status: "success" as const, count: dayData.ok }, { status: "degraded" as const, count: dayData.degraded }, { status: "error" as const, count: dayData.error }, { status: "info" as const, count: 0 }, ]; }
function createDurationEntries(dayData: StatusData): Array<{ status: "success" | "degraded" | "error" | "info"; count: number; }> { return [ { status: "error" as const, count: dayData.error }, { status: "degraded" as const, count: dayData.degraded }, { status: "success" as const, count: dayData.ok }, { status: "info" as const, count: 0 }, ]; }
function entriesToRequestCardData( entries: Array<{ status: "success" | "degraded" | "error" | "info"; count: number; }>, ): UptimeData["card"] { return entries .filter((entry) => entry.count > 0) .map((entry) => ({ status: entry.status, value: `${formatNumber(entry.count)} reqs`, })); }
// Helper to calculate duration in minutes for a specific event type function calculateEventDurationMinutes(events: Event[], date: Date): number { const totalDuration = getTotalEventsDurationMs(events, date); return Math.round(totalDuration / MILLISECONDS_PER_MINUTE); }
// Helper to create duration card data for a specific status function createDurationCardEntry( status: "error" | "degraded" | "info" | "success", events: Event[], date: Date, durationMap: Map<string, number>, maintenances: Event[] = [], ): { status: "error" | "degraded" | "info" | "success"; value: string; } | null { if (status === "success") { // Calculate success duration as remaining time let totalEventMinutes = 0; durationMap.forEach((minutes) => (totalEventMinutes += minutes));
// Use adjusted total minutes accounting for maintenance const totalMinutesInDay = getAdjustedTotalMinutesInDay( date, maintenances, ); const successMinutes = Math.max(totalMinutesInDay - totalEventMinutes, 0);
if (successMinutes === 0) return null; return { status, value: formatDuration(successMinutes), }; }
// For error, degraded, info - calculate from events const minutes = calculateEventDurationMinutes(events, date); durationMap.set(status, minutes);
if (minutes === 0) return null; return { status, value: formatDuration(minutes), }; }
return data.map((dayData) => { const date = new Date(dayData.day);
// Find events for this day const dayEvents = events.filter((event) => isDateWithinEvent(date, event));
// Determine status override based on events const incidents = dayEvents.filter((e) => e.type === "incident"); const reports = dayEvents.filter((e) => e.type === "report"); const maintenances = dayEvents.filter((e) => e.type === "maintenance");
const hasIncidents = incidents.length > 0; const hasMaintenances = maintenances.length > 0;
// worst impact color across the day's reports; "success" (operational all // day) means reports don't color the day const reportsDayStatus = reports.length ? getWorstVariant(reports.map((e) => reportEventDayStatus(e, date))) : undefined; const activeReportsDayStatus = reportsDayStatus === "success" ? undefined : reportsDayStatus;
const eventStatus = hasIncidents ? "error" : (activeReportsDayStatus ?? (hasMaintenances ? ("info" as const) : undefined));
// Calculate bar data based on barType // TODO: transform into a new Map<type, number>(); let barData: UptimeData["bar"];
const total = dayData.ok + dayData.degraded + dayData.error; const dataStatus = getHighestPriorityStatus(dayData);
switch (barType) { case "absolute": if (eventStatus) { // Create segments based on event durations for the day const eventSegments = createEventSegments( incidents, reports, maintenances, date, );
// Special case: if only errors exist, show uptime vs downtime if ( eventSegments.length === 1 && eventSegments[0].status === "error" ) { barData = createErrorOnlyBarData(eventSegments[0].count); } else { // Multiple segments: show proportional distribution barData = createProportionalBarData(eventSegments); } } else if (total === 0) { // Empty day - no data available barData = createEmptyBarData(); } else { if (cardType === "duration") { // If no eventStatus and cardType is duration, show operational bar barData = createOperationalBarData(); } else { // Multiple segments for absolute view - show proportional distribution of status data const statusSegments = createStatusSegments(dayData); barData = segmentsToBarData(statusSegments, total); } } break; case "dominant": barData = [ { status: eventStatus ?? dataStatus, height: 100, }, ]; break; case "manual": const manualEventStatus = activeReportsDayStatus ?? (hasMaintenances ? "info" : undefined); barData = [ { status: manualEventStatus || "success", height: 100, }, ]; break; default: // Default to dominant behavior barData = [ { status: eventStatus ?? dataStatus, height: 100, }, ]; break; }
// Calculate card data based on cardType // TODO: transform into a new Map<type, number>(); let cardData: UptimeData["card"] = [];
switch (cardType) { case "requests": if (total === 0) { cardData = createEmptyCardData(eventStatus); } else { const requestEntries = createRequestEntries(dayData); cardData = entriesToRequestCardData(requestEntries); } break;
case "duration": if (total === 0) { cardData = createEmptyCardData(eventStatus); } else { const entries = createDurationEntries(dayData); const durationMap = new Map<string, number>();
cardData = entries .map((entry) => { // Map each entry status to its corresponding events const eventByStatus: Record<string, Event[]> = { error: incidents, degraded: reports, info: maintenances, success: [], // Success is calculated differently }; const events = eventByStatus[entry.status];
// When there are no events for this status but the data shows a // non-zero count, calculate a proportional duration from the data // so the card reflects actual probe-based status, not just events. if ( events.length === 0 && (entry.status === "error" || entry.status === "degraded") && entry.count > 0 ) { const totalMinutes = getAdjustedTotalMinutesInDay( date, maintenances, ); const statusMinutes = Math.round( (entry.count / total) * totalMinutes, ); if (statusMinutes === 0) return null; durationMap.set(entry.status, statusMinutes); return { status: entry.status, value: formatDuration(statusMinutes), }; }
return createDurationCardEntry( entry.status, events, date, durationMap, maintenances, ); }) .filter((item): item is NonNullable<typeof item> => item !== null); } break;
case "dominant": cardData = [ { status: eventStatus ?? dataStatus, value: "", }, ]; break;
case "manual": { const manualCardStatus = activeReportsDayStatus ?? (hasMaintenances ? "info" : undefined); const dayImpacts = reports .map((e) => reportEventDayImpact(e, date)) .filter((i): i is PageComponentImpact => i !== null); const worstDayImpact = dayImpacts.length > 0 ? worstImpact(dayImpacts) : null; cardData = [ { status: manualCardStatus || "success", value: "", // only when the impact agrees with the day color โ a mixed // legacy+impact day where legacy dominates keeps the generic label impact: worstDayImpact !== null && impactToStatusType(worstDayImpact) === manualCardStatus ? worstDayImpact : undefined, }, ]; break; } default: // Default to requests behavior if (total === 0) { cardData = createEmptyCardData(eventStatus); } else { const defaultEntries = createRequestEntries(dayData); cardData = entriesToRequestCardData(defaultEntries); } break; }
// Bundle incidents that occur on the same day if there are more than 4 const bundledIncidents = incidents.length > 4 ? [ { id: -1, // Use -1 to indicate bundled incidents name: `Downtime (${incidents.length} incidents)`, from: new Date( Math.min(...incidents.map((i) => i.from.getTime())), ), to: new Date( Math.max( ...incidents.map((i) => (i.to || new Date()).getTime()), ), ), type: "incident" as const, status: "error" as const, isAggregated: true, }, ] : incidents;
return { day: dayData.day, events: [ // row dot follows the day's worst impact; floors at degraded so an // operational-only slice never renders a green row (mirrors calendar) ...reports.map((e) => ({ ...e, status: reportEventDayStatus(e, date) === "error" ? ("error" as const) : ("degraded" as const), })), ...maintenances, ...(barType === "absolute" ? bundledIncidents : []), ], bar: barData, card: cardData, }; });}
export function getUptime({ data, events, barType, cardType,}: { data: StatusData[]; events: Event[]; barType: "absolute" | "dominant" | "manual"; cardType: "requests" | "duration" | "dominant" | "manual";}): string { if (barType === "manual" || cardType === "duration") { // Clamp event durations to the data lookback window to avoid // events outside the window producing negative uptime values. const timestamps = data.map((d) => new Date(d.day).getTime()); const { segments: coverage, totalMs: total } = dayCoverage(timestamps); if (total === 0) return "100%"; const windowEndDate = new Date(Math.max(...timestamps)); windowEndDate.setUTCHours(23, 59, 59, 999); const window: UptimeWindow = { start: Math.min(...timestamps), end: windowEndDate.getTime(), now: Date.now(), };
let duration: number; if (barType === "manual") { // Manual mode: only count manually created reports duration = reportsOnlyDowntimeMs(events, window, coverage); } else { // Duration mode: merge both event-based and probe-based downtime // to capture both manual incidents/reports AND automated probe failures const eventIntervals = downtimeIntervals(events, window, false); const probeIntervals = probeDowntimeIntervals(data, window); const allIntervals = [...eventIntervals, ...probeIntervals]; duration = mergedDowntimeMs( coverage ? clipToCoverage(allIntervals, coverage) : allIntervals, ); }
return `${floorPct((total - duration) / total)}%`; }
const { up, total } = requestsTally(data); if (total === 0) return "100%"; return `${floorPct(up / total)}%`;}