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.
10 kB · 322 lines
TypeScript
at incident-api
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323import { Effect } from "effect";
import type { FetchError } from "./fetch";import { fetchJson, fetchTextWithUrl } from "./fetch";import { atlassianResponseSchema } from "./fetchers/atlassian";import { betterStackResponseSchema } from "./fetchers/betterstack";import { instatusResponseSchema } from "./fetchers/instatus";import type { ApiConfigType, StatusPageProvider } from "./types";import { urlHostnameEndsWith } from "./utils";
export type DetectableApiType = Extract< ApiConfigType, "atlassian" | "incidentio" | "instatus" | "betterstack">;
export type ProviderMatch = { type: DetectableApiType; provider: StatusPageProvider; endpoint: string;};
export type DetectionResult = { currentProviderValidated: boolean; matches: ProviderMatch[]; hostnameSuggestions: StatusPageProvider[]; // Set when the page redirects to another origin whose probes validate: the // stored status_page_url is stale, not (necessarily) the provider. movedTo?: { base: string; matches: ProviderMatch[] }; evidence: string[];};
type Candidate = { type: DetectableApiType; provider: StatusPageProvider };
type Probe = { path: string; candidates: Candidate[]; validate: (url: string, entryId?: string) => Effect.Effect<void, FetchError>;};
// atlassian and incident.io expose the identical summary API, so one probe// carries both candidates and the pair is disambiguated afterwards.const PROBES: Probe[] = [ { path: "/api/v2/summary.json", candidates: [ { type: "atlassian", provider: "atlassian-statuspage" }, { type: "incidentio", provider: "incidentio" }, ], validate: (url, entryId) => fetchJson({ url, schema: atlassianResponseSchema, fetcherName: "detect", entryId, }).pipe(Effect.asVoid), }, { path: "/summary.json", candidates: [{ type: "instatus", provider: "instatus" }], validate: (url, entryId) => fetchJson({ url, schema: instatusResponseSchema, fetcherName: "detect", entryId, }).pipe(Effect.asVoid), }, { path: "/index.json", candidates: [{ type: "betterstack", provider: "better-uptime" }], validate: (url, entryId) => fetchJson({ url, schema: betterStackResponseSchema, fetcherName: "detect", entryId, }).pipe(Effect.asVoid), },];
type HostnameHit = { domain: string; provider: StatusPageProvider };
const HOSTNAME_EVIDENCE: HostnameHit[] = [ { domain: "statuspage.io", provider: "atlassian-statuspage" }, { domain: "status.atlassian.com", provider: "atlassian-statuspage" }, { domain: "incident.io", provider: "incidentio" }, { domain: "incidentio.com", provider: "incidentio" }, { domain: "instatus.com", provider: "instatus" }, { domain: "betteruptime.com", provider: "better-uptime" }, { domain: "betterstack.com", provider: "better-uptime" }, { domain: "stats.uptimerobot.com", provider: "uptime-robot" },];
const HTML_MARKERS: { provider: StatusPageProvider; needle: string }[] = [ { provider: "incidentio", needle: "incident.io" }, { provider: "atlassian-statuspage", needle: "statuspage.io" }, { provider: "instatus", needle: "instatus.com" }, { provider: "better-uptime", needle: "betteruptime.com" },];
const describeError = (err: FetchError): string => `${err.kind ?? "error"}${err.httpStatus ? ` ${err.httpStatus}` : ""}`;
const hostnameHits = (url: string): HostnameHit[] => HOSTNAME_EVIDENCE.filter((h) => urlHostnameEndsWith(url, h.domain));
const htmlMarkerHits = (html: string) => { const lower = html.toLowerCase(); return HTML_MARKERS.filter((m) => lower.includes(m.needle));};
const trimTrailingSlash = (value: string): string => { let end = value.length; while (end > 0 && value[end - 1] === "/") end--; return value.slice(0, end);};
// URL parsing drops query/hash so probe paths append to the page path; the// fallbacks keep the error channel `never` for unparseable input.const probeBase = (statusPageUrl: string): string => { try { const url = new URL(statusPageUrl); return trimTrailingSlash(`${url.origin}${url.pathname}`); } catch { return trimTrailingSlash(statusPageUrl); }};
const canonicalHref = (value: string): string => { try { return new URL(value).href; } catch { return value; }};
const originOf = (value: string): string | null => { try { return new URL(value).origin; } catch { return null; }};
const isPair = (candidates: ProviderMatch[]): boolean => candidates.length === 2 && candidates.every((c) => c.type === "atlassian" || c.type === "incidentio");
const runProbes = ( base: string, entryId: string | undefined, evidence: string[],): Effect.Effect<ProviderMatch[]> => Effect.forEach( PROBES, (probe) => { const endpoint = `${base}${probe.path}`; return probe.validate(endpoint, entryId).pipe( Effect.match({ onSuccess: () => ({ probe, endpoint, ok: true }), onFailure: (err: FetchError) => { evidence.push(`${endpoint}: ${describeError(err)}`); return { probe, endpoint, ok: false }; }, }), ); }, { concurrency: 3 }, ).pipe( Effect.map((probed) => probed .filter((r) => r.ok) .flatMap((r) => { evidence.push(`validated ${r.endpoint}`); return r.probe.candidates.map((c) => ({ ...c, endpoint: r.endpoint, })); }), ), );
// Hostname evidence outranks html markers: a page hosted on a provider domain// is unambiguous, while markers can name a competitor in prose.const tiebreakPair = ( candidates: ProviderMatch[], hits: HostnameHit[], html: string | undefined, evidence: string[],): ProviderMatch[] => { const hitProviders = new Set(hits.map((h) => h.provider)); const pairHits = candidates.filter((c) => hitProviders.has(c.provider)); if (pairHits.length === 1) { evidence.push(`hostname tiebreak: ${pairHits[0].provider}`); return pairHits; } if (html === undefined) return candidates; const markerHits = htmlMarkerHits(html).filter((m) => candidates.some((c) => c.provider === m.provider), ); const markerProviders = new Set(markerHits.map((m) => m.provider)); if (markerProviders.size !== 1) return candidates; const pick = markerHits[0]; evidence.push(`html marker: ${pick.needle}`); return candidates.filter((c) => c.provider === pick.provider);};
export const detectProvider = (args: { statusPageUrl: string; currentProvider: StatusPageProvider; entryId?: string;}): Effect.Effect<DetectionResult> => Effect.gen(function* () { const base = probeBase(args.statusPageUrl); const evidence: string[] = [];
const candidates = yield* runProbes(base, args.entryId, evidence);
// Deliberate trade-off: a validating current provider ends detection, so // atlassian↔incidentio label drift goes unflagged — the APIs are identical // and fetching works, only the public label may lag. if (candidates.some((c) => c.provider === args.currentProvider)) { return { currentProviderValidated: true, matches: [], hostnameSuggestions: [], evidence, }; }
const staticHits = hostnameHits(args.statusPageUrl); for (const h of staticHits) evidence.push(`hostname matches ${h.domain}`);
const ambiguous = isPair(candidates); let finalUrlHits: HostnameHit[] = []; let matches = candidates; let movedTo: DetectionResult["movedTo"]; let page: { text: string; finalUrl: string } | null = null;
if (ambiguous || candidates.length === 0) { page = yield* fetchTextWithUrl({ url: args.statusPageUrl, fetcherName: "detect", entryId: args.entryId, }).pipe( Effect.match({ onSuccess: (p): { text: string; finalUrl: string } | null => p, onFailure: (err: FetchError) => { evidence.push(`html fetch failed: ${describeError(err)}`); return null; }, }), );
const redirected = page?.finalUrl && canonicalHref(page.finalUrl) !== canonicalHref(args.statusPageUrl); if (page && redirected) { evidence.push(`final url ${page.finalUrl}`); finalUrlHits = hostnameHits(page.finalUrl); for (const h of finalUrlHits) { evidence.push(`redirect hostname matches ${h.domain}`); } }
if (ambiguous) { matches = tiebreakPair( candidates, [...staticHits, ...finalUrlHits], page?.text, evidence, ); }
// Nothing validates here but the page lives elsewhere now: probe the // new origin so a stale status_page_url surfaces as a concrete move. const originMoved = page && redirected && originOf(page.finalUrl) !== null && originOf(page.finalUrl) !== originOf(args.statusPageUrl); if (page && candidates.length === 0 && originMoved) { // Providers serve their API at the root, so a redirect landing on a // subpath must not carry that path into the probes. const movedBase = originOf(page.finalUrl) ?? probeBase(page.finalUrl); const movedCandidates = yield* runProbes( movedBase, args.entryId, evidence, ); if (movedCandidates.length > 0) { movedTo = { base: movedBase, matches: isPair(movedCandidates) ? tiebreakPair(movedCandidates, finalUrlHits, page.text, evidence) : movedCandidates, }; } } }
let hostnameSuggestions: StatusPageProvider[] = []; if (candidates.length === 0) { const markerHits = page && !movedTo ? htmlMarkerHits(page.text) : []; for (const m of markerHits) evidence.push(`html marker: ${m.needle}`); hostnameSuggestions = [ ...new Set( [...staticHits, ...finalUrlHits, ...markerHits] .map((h) => h.provider) .filter((p) => p !== args.currentProvider), ), ]; }
return { currentProviderValidated: false, matches, hostnameSuggestions, ...(movedTo ? { movedTo } : {}), evidence, }; });