Monitor websites uptime using Cloudflare Workers
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339/** * Public report snapshot contract served from `VITE_REPORTS_BASE_URL`. * * The gateway resolves monitors independently and status pages/index from the * committed cohort. Responses preserve the fields the frontend consumes and add * freshness metadata: * * schemaVersion Snapshot schema identifier, currently "1". * generatedAt When the publisher generated the object (ISO-8601 UTC). * latestObservationAt Latest included observation timestamp, or null. * staleAfterSeconds Publisher cadence plus probe/cache allowance; beyond * generatedAt + staleAfterSeconds the UI shows stale. * * Gateway paths relative to the configured base URL: * public/monitors/{slug}.json `MonitorReportSnapshot` * public/status-pages/{slug}.json `StatusPageReportSnapshot` * public/status-pages.json `StatusPageIndexSnapshot` */
import type { MonitorReportSnapshot as PublishedMonitorReportSnapshot, SnapshotFreshness as PublishedSnapshotFreshness, StatusPageIndexSnapshot as PublishedStatusPageIndexSnapshot, StatusPageReportSnapshot as PublishedStatusPageReportSnapshot, StatusPageReportSnapshotMonitor,} from '@uptime/contracts';import type { MonitorLatencyData, PublicMonitorDetailResponse, PublicStatusPage } from './api.js';import * as apiModule from './api.js';
function workspaceSearch(): string { try { const helper = apiModule.publicWorkspaceSearch; return typeof helper === 'function' ? helper() : ''; } catch { return ''; }}import { frontendConfig } from './config.js';
export type SnapshotFreshness = Omit< PublishedSnapshotFreshness, 'generatedAt' | 'staleAfterSeconds'> & { generatedAt: string | null; staleAfterSeconds: number | null; generation?: string; sourceWatermark?: string;};
type FrontendLatencyPayload = MonitorLatencyData['latency'];
export type MonitorReportSnapshot = Omit< PublishedMonitorReportSnapshot, keyof PublishedSnapshotFreshness | 'latency' | 'latencyByRange'> & { latency: FrontendLatencyPayload; latencyByRange?: Partial<Record<'1h' | '24h' | '7d' | '30d', FrontendLatencyPayload>>;} & SnapshotFreshness;
export type { StatusPageReportSnapshotMonitor };export type StatusPageReportSnapshot = Omit< PublishedStatusPageReportSnapshot, keyof PublishedSnapshotFreshness> & SnapshotFreshness;export type StatusPageIndexSnapshot = Omit< PublishedStatusPageIndexSnapshot, keyof PublishedSnapshotFreshness> & SnapshotFreshness;
/** Public reference: prefer the slug, fall back to the id for legacy URLs. */export function reportReference(slug: string | null, id: string): string { return slug ?? id;}
export function monitorReportUrl( reference: string, baseUrl: string | null = frontendConfig().reportsBaseUrl,): string | null { if (!baseUrl) return null; return `${baseUrl}/public/monitors/${encodeURIComponent(reference)}.json${workspaceSearch()}`;}
export function statusPageReportUrl( reference: string, baseUrl: string | null = frontendConfig().reportsBaseUrl,): string | null { if (!baseUrl) return null; return `${baseUrl}/public/status-pages/${encodeURIComponent(reference)}.json${workspaceSearch()}`;}
export function statusPageIndexUrl( baseUrl: string | null = frontendConfig().reportsBaseUrl,): string | null { if (!baseUrl) return null; return `${baseUrl}/public/status-pages.json${workspaceSearch()}`;}
export class SnapshotError extends Error { constructor( message: string, readonly status: number, ) { super(message); this.name = 'SnapshotError'; }}
export class SnapshotPendingError extends SnapshotError { constructor(readonly retryAfterMs: number) { super('Report refresh in progress', 503); this.name = 'SnapshotPendingError'; }}
function pendingRetryAfterMs(value: string | null): number { const seconds = value?.trim() ? Number(value) : Number.NaN; const delay = Number.isFinite(seconds) ? seconds * 1_000 : value ? Date.parse(value) - Date.now() : Number.NaN; return Number.isFinite(delay) ? Math.min(5_000, Math.max(1_000, delay)) : 1_000;}
async function fetchSnapshot<T>(url: string, signal?: AbortSignal): Promise<T> { let response: Response; try { response = await fetch(url, { credentials: 'omit', cache: 'no-cache', ...(signal ? { signal } : {}), }); } catch { throw new SnapshotError('Could not reach the public report host', 0); } if (!response.ok) { if (response.status === 503) { const body: unknown = await response.json().catch(() => null); if ( body && typeof body === 'object' && 'error' in body && body.error === 'Report refresh in progress' ) { throw new SnapshotPendingError(pendingRetryAfterMs(response.headers.get('Retry-After'))); } } // 404 means the page/monitor was removed or unpublished; surface it as a // distinct state so the UI can say so instead of a generic failure. throw new SnapshotError( response.status === 404 ? 'This public report is no longer published' : 'Report unavailable', response.status, ); } return (await response.json()) as T;}
export function loadMonitorReport( reference: string, baseUrl?: string | null, signal?: AbortSignal,) { const url = monitorReportUrl(reference, baseUrl ?? frontendConfig().reportsBaseUrl); if (!url) throw new SnapshotError('Public reports are not configured', 0); return fetchSnapshot<MonitorReportSnapshot>(url, signal);}
export function loadStatusPageReport(reference: string, baseUrl?: string | null) { const url = statusPageReportUrl(reference, baseUrl ?? frontendConfig().reportsBaseUrl); if (!url) throw new SnapshotError('Public reports are not configured', 0); return fetchSnapshot<StatusPageReportSnapshot>(url);}
export function loadStatusPageIndex(baseUrl?: string | null) { const url = statusPageIndexUrl(baseUrl ?? frontendConfig().reportsBaseUrl); if (!url) throw new SnapshotError('Public reports are not configured', 0); return fetchSnapshot<StatusPageIndexSnapshot>(url);}
export type FreshnessState = 'fresh' | 'stale' | 'unknown';
export interface FreshnessAssessment { state: FreshnessState; generatedAt: string | null; latestObservationAt: string | null; ageSeconds: number | null; staleAfterSeconds: number | null; /** Human-readable reason, safe to render. */ detail: string;}
/** * Decide whether a snapshot is fresh. A snapshot is stale when the wall clock * is beyond `generatedAt + staleAfterSeconds`, or when the publisher never * recorded a generation time. `unknown` means the snapshot carries no usable * freshness metadata (older publisher), so the UI must not claim liveness. */export function assessFreshness( snapshot: Pick<SnapshotFreshness, 'generatedAt' | 'latestObservationAt' | 'staleAfterSeconds'>, now: Date = new Date(),): FreshnessAssessment { const staleAfterSeconds = typeof snapshot.staleAfterSeconds === 'number' && snapshot.staleAfterSeconds > 0 ? snapshot.staleAfterSeconds : null; const generatedAt = snapshot.generatedAt; if (!generatedAt) { return { state: 'unknown', generatedAt: null, latestObservationAt: snapshot.latestObservationAt ?? null, ageSeconds: null, staleAfterSeconds, detail: 'This report does not include freshness metadata.', }; } const generatedMs = Date.parse(generatedAt); if (!Number.isFinite(generatedMs)) { return { state: 'unknown', generatedAt, latestObservationAt: snapshot.latestObservationAt ?? null, ageSeconds: null, staleAfterSeconds, detail: 'This report has an unreadable generation time.', }; } const ageSeconds = Math.max(0, Math.round((now.getTime() - generatedMs) / 1_000)); if (staleAfterSeconds === null) { return { state: 'unknown', generatedAt, latestObservationAt: snapshot.latestObservationAt ?? null, ageSeconds, staleAfterSeconds: null, detail: 'This report does not publish a freshness window.', }; } if (ageSeconds > staleAfterSeconds) { return { state: 'stale', generatedAt, latestObservationAt: snapshot.latestObservationAt ?? null, ageSeconds, staleAfterSeconds, detail: `Last generated ${formatAge(ageSeconds)} ago; stale after ${formatAge(staleAfterSeconds)}.`, }; } return { state: 'fresh', generatedAt, latestObservationAt: snapshot.latestObservationAt ?? null, ageSeconds, staleAfterSeconds, detail: `Generated ${formatAge(ageSeconds)} ago.`, };}
export function formatAge(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) return 'unknown time'; if (seconds < 90) return `${Math.round(seconds)}s`; const minutes = Math.round(seconds / 60); if (minutes < 90) return `${minutes}m`; const hours = Math.round(minutes / 60); if (hours < 48) return `${hours}h`; return `${Math.round(hours / 24)}d`;}
/** * Latency sampling/freshness metadata attached by the coordinator. These fields * are optional so snapshots from older publishers still load; `sampled` must be * treated as true only when explicitly set. */export interface LatencySampling { /** True when points/percentiles cover only a bounded recent sample. */ sampled: boolean; /** Raw observation row budget applied, when bounded. */ sampleLimit: number | null; /** When the range payload was computed, when published. */ computedAt: string | null;}
export interface LatencySamplingAssessment extends LatencySampling { /** Age of `computedAt` at `now`, in seconds; null when unavailable. */ computedAgeSeconds: number | null;}
/** * Normalize the coordinator's `sampled`/`sampleLimit`/`computedAt` metadata so * the UI never presents a bounded sample as an exact whole-range statistic. */export function assessLatencySampling( latency: Pick<MonitorLatencyData['latency'], 'sampled' | 'sampleLimit' | 'computedAt'>, now: Date = new Date(),): LatencySamplingAssessment { const sampled = latency.sampled === true; const sampleLimit = typeof latency.sampleLimit === 'number' && Number.isFinite(latency.sampleLimit) ? latency.sampleLimit : null; const computedAt = typeof latency.computedAt === 'string' ? latency.computedAt : null; const computedMs = computedAt ? Date.parse(computedAt) : Number.NaN; const computedAgeSeconds = Number.isFinite(computedMs) ? Math.max(0, Math.round((now.getTime() - computedMs) / 1_000)) : null; return { sampled, sampleLimit, computedAt, computedAgeSeconds };}/** Map a monitor snapshot onto the existing detail response shape. */export function monitorSnapshotToDetail( snapshot: MonitorReportSnapshot, range?: string,): PublicMonitorDetailResponse { if (!snapshot.summary || !snapshot.uptime || !snapshot.latency) { throw new SnapshotError( 'Snapshot is missing the required summary, uptime or latency fields', 0, ); } const ranged = range && snapshot.latencyByRange ? (snapshot.latencyByRange[range as keyof typeof snapshot.latencyByRange] ?? snapshot.latency) : snapshot.latency; return { summary: snapshot.summary, uptime: snapshot.uptime, latency: ranged, };}
/** Map a status-page snapshot onto the existing public status page shape. */export function statusPageSnapshotToPage(snapshot: StatusPageReportSnapshot): PublicStatusPage { if (!snapshot.statusPage) { throw new SnapshotError('Snapshot is missing the required statusPage field', 0); } return snapshot.statusPage;}