TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import { sha256, simhash, type ConnectorMetadata, type NormalizedContent, type Observation, type SensorEndpoint } from "@websensor/core";2import { httpFetchWithRetry } from "./fetcher";3import { NormalizeError, type WebSensorConnector } from "./types";45/**6 * Statuspage connector (Atlassian Statuspage `/api/v2/summary.json` and compatible clones:7 * instatus, status.io exports, Google/AWS style JSON where configured).8 * Emits list items for incidents + scheduled maintenances and component states so we9 * detect: incident created / updated / resolved, maintenance scheduled, component degraded.10 */11export class StatuspageConnector implements WebSensorConnector {12 mode = "list" as const;13 metadata(): ConnectorMetadata {14 return { key: "statuspage", name: "Statuspage", sensorTypes: ["STATUSPAGE"], description: "Atlassian Statuspage API v2 summary → incidents, maintenances, component status", version: "1.0.0" };15 }16 async fetch(endpoint: SensorEndpoint): Promise<Observation> {17 return httpFetchWithRetry(endpoint.id, endpoint.url, { etag: endpoint.etag, lastModified: endpoint.lastModified, accept: "application/json" });18 }19 async normalize(endpoint: SensorEndpoint, obs: Observation): Promise<NormalizedContent> {20 if (!obs.body) throw new NormalizeError("no_body", "Observation has no body");21 const text = obs.body.toString("utf8");22 let j: Record<string, unknown>;23 try {24 j = JSON.parse(text) as Record<string, unknown>;25 } catch {26 throw new NormalizeError("bad_json", "Statuspage response is not JSON");27 }28 const page = (j.page ?? {}) as Record<string, unknown>;29 const status = (j.status ?? {}) as Record<string, unknown>;30 const incidents = ((j.incidents ?? []) as Record<string, unknown>[]).map((i) => incidentItem(i, "incident"));31 const maints = ((j.scheduled_maintenances ?? []) as Record<string, unknown>[]).map((i) => incidentItem(i, "maintenance"));32 const components = ((j.components ?? []) as Record<string, unknown>[])33 .filter((c) => !c.group && c.status !== "operational")34 .map((c) => ({ key: `component:${String(c.id)}`, kind: "component", title: `${String(c.name)} — ${String(c.status).replace(/_/g, " ")}`, status: String(c.status), url: String(page.url ?? ""), summary: `Component ${String(c.name)} is ${String(c.status).replace(/_/g, " ")}.`, publishedAt: c.updated_at ? new Date(String(c.updated_at)).toISOString() : null }));35 const items: { key: string; [k: string]: unknown }[] = [...incidents, ...maints, ...components];36 const overall = { key: "overall", kind: "overall", title: `Overall: ${String(status.description ?? status.indicator ?? "unknown")}`, status: String(status.indicator ?? ""), summary: String(status.description ?? ""), url: String(page.url ?? ""), publishedAt: null, updatedAt: null };37 items.push(overall);38 const canonical = items.map((i) => `${i.key}\t${String(i.status)}\t${String(i.title)}\t${String(i.updatedAt ?? "")}`).join("\n");39 const newest = items.map((i) => i.publishedAt as string | null).filter((x): x is string => Boolean(x)).sort().at(-1);40 return {41 mode: "list",42 items,43 compareFields: ["status", "title", "updatedAt"],44 title: String(page.name ?? ""),45 rawHash: sha256(text),46 canonicalHash: sha256(canonical),47 semanticHash: simhash(items.map((i) => String(i.title)).join("\n")),48 publishedAt: newest ? new Date(newest) : null,49 extractionConfidence: 1,50 extra: { indicator: status.indicator, activeIncidents: incidents.length, maintenances: maints.length, degradedComponents: components.length },51 };52 }53}5455function incidentItem(i: Record<string, unknown>, kind: "incident" | "maintenance"): { key: string; kind: string; title: string; status: string; impact: string; url: string; summary: string; publishedAt: string | null; updatedAt: string | null } {56 const updates = (i.incident_updates ?? []) as Record<string, unknown>[];57 const latest = updates[0];58 return {59 key: `${kind}:${String(i.id)}`,60 kind,61 title: `${String(i.name)} — ${String(i.status).replace(/_/g, " ")}`,62 status: String(i.status),63 impact: String(i.impact ?? ""),64 url: String(i.shortlink ?? ""),65 summary: latest ? String(latest.body ?? "").slice(0, 800) : "",66 publishedAt: i.created_at ? new Date(String(i.created_at)).toISOString() : null,67 updatedAt: i.updated_at ? new Date(String(i.updated_at)).toISOString() : null,68 };69}70