SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
4.4 KB · 70 lines typescript
Raw Blame History
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