-
Notifications
You must be signed in to change notification settings - Fork 0
API Integration
The bot supports multiple status page providers. Each provider lives in its own adapter under src/providers/ and normalizes its API responses into the same canonical shapes (Incident, IncidentUpdate, Summary, PageStatus) so the polling loop, rendering, and state management are completely provider-agnostic.
| Provider | ID | Example URL | API Type |
|---|---|---|---|
| Statuspage.io (Atlassian) | statuspage |
https://status.atlassian.com |
Public v2 API, no key required |
| incident.io | incidentio |
https://status.openai.com |
Public widget proxy, no key required |
| Instatus | instatus |
https://status.perplexity.com |
Public v3 JSON API + Atom history feed, no key required |
No API key is required for any supported provider — all endpoints are public.
When a user runs /monitor add <url>, the bot probes each provider in order (see PROBE_ORDER in src/providers/index.ts). The first provider whose probe() returns a non-null result wins and is saved to the monitor's provider field in data/monitors.json.
Current probe order:
-
incident.io — probed first because many incident.io pages also expose a Statuspage-compatible
/api/v2/shim, but the shim returns empty update bodies and a truncated history. Probing incident.io first ensures we use the richer native widget API when available. - Statuspage.io — fallback for pages that are not on incident.io.
-
Instatus — probed last. Its
/v3/summary.jsonpath does not collide with the earlier providers, and its probe rejects Statuspage-shaped summaries (see below).
Monitors loaded from data/monitors.json or MONITORS_JSON that pre-date multi-provider support default to statuspage for backwards compatibility.
File: src/providers/statuspage.ts
| Endpoint | Used By | Purpose |
|---|---|---|
<baseUrl>/api/v2/summary.json |
probe(), fetchSummary()
|
Overall page status + active incidents |
<baseUrl>/api/v2/incidents.json |
fetchIncidents() |
Full incident list with all updates (for polling, /replay) |
Both endpoints return responses that already match the canonical shapes — the adapter is a thin pass-through.
type Summary = {
page: { id: string; name: string; url: string; updated_at?: string };
status: { indicator: string; description: string };
incidents: Incident[]; // active only in /summary.json
};
type Incident = {
id: string;
name: string;
status: string; // "investigating" | "identified" | "monitoring" | "resolved"
impact: string; // "none" | "minor" | "major" | "critical"
shortlink?: string;
created_at: string;
updated_at?: string;
resolved_at?: string | null;
incident_updates: IncidentUpdate[];
};
type IncidentUpdate = {
id: string;
status: string;
body: string;
created_at: string;
updated_at?: string;
};File: src/providers/incidentio.ts
incident.io exposes its public status page data through a proxy at <baseUrl>/proxy/<host>, where <host> is the hostname of the base URL. For example:
https://status.openai.com/proxy/status.openai.com # summary
https://status.openai.com/proxy/status.openai.com/incidents # full history
| Endpoint | Used By | Purpose |
|---|---|---|
<baseUrl>/proxy/<host> |
probe(), fetchSummary()
|
summary object with ongoing incidents, affected components, page metadata |
<baseUrl>/proxy/<host>/incidents |
fetchIncidents() |
{ incidents: [...] } with resolved incidents and full update messages |
-
Page status (
PageStatus.indicator) is derived from the highest severity acrossongoing_incidents+affected_components. An empty list with no in-progress maintenance maps tonone/ "All Systems Operational". -
Incident status is lowercased and mapped onto the canonical set:
investigating,identified(incident.io'sfixingalso maps here),monitoring,resolved. -
Incident impact comes from
incident.impactwhen present, otherwise derived from the max severity acrosscomponent_impacts[]andstatus_summaries[]. incident.io's raw impact strings (degraded,partial_outage,full_outage, etc.) are collapsed onto the canonicalnone/minor/major/criticalset. -
Update message bodies use a nested rich-doc structure (
{ type: "doc", content: [{ type: "paragraph", content: [...] }] }). The adapter'sflattenMessage()walks this recursively and returns plain text with paragraph breaks. -
Shortlinks come from
incident.urlwhen present, otherwise constructed as<public_url>/incident/<id>.
File: src/providers/instatus.ts
Instatus exposes a documented keyless JSON API plus a standard Atom history feed. The adapter joins them on the incident id: the JSON API gives live state and the impact enum, while the Atom feed gives the full update history including the operator's written prose.
| Endpoint | Used By | Purpose |
|---|---|---|
<baseUrl>/v3/summary.json |
probe(), fetchSummary()
|
Page status + activeIncidents[] + activeMaintenances[] (current state, impact enum) |
<baseUrl>/history.atom |
fetchIncidents() |
Atom feed of recent incidents and maintenances with full update prose (for polling, /replay) |
-
Page status comes from
page.status:UP→ operational,UNDERMAINTENANCE→ maintenance, anything else (HASISSUES, otherHAS*) → derived from the worst active impact. -
Incident status maps
INVESTIGATING/IDENTIFIED/MONITORING/RESOLVED(and the title-case feed equivalents) onto the canonical set. Unknown words default toinvestigating. -
Maintenance status maps
NOTSTARTEDYET/Scheduled→scheduled,INPROGRESS/VERIFYING/Identified→in_progress,COMPLETED/Resolved→resolved. Maintenances carryimpact: "maintenance"(rendered grey). -
Impact maps
OPERATIONAL→none,MINOROUTAGE/DEGRADEDPERFORMANCE→minor,PARTIALOUTAGE→major,MAJOROUTAGE→critical. The Atom feed carries no impact enum, so resolved/historical incidents default tominor; incidents still active insummary.jsonare stamped with their real impact by joining onid. -
Update bodies come from the Atom
<content>HTML. Each<p>update block (<small>timestamp</small><br><strong>Status</strong> - body) is parsed into anIncidentUpdate; header<p>blocks (<strong>Type:</strong> …) are skipped. Update ids are<incidentId>:<updateTimestampIso>for dedup. Update blocks are sorted chronologically (the feed does not emit them in order). -
Update timestamps in the feed carry no year. The year is taken from the entry
<published>, with a rollover guard: if the resulting date lands before<published>(beyond a ~24h grace window), it is rolled to the following year (incident spanning Dec→Jan). -
page.idis synthesized from the base URL host (Instatus summaries omit it).
Instatus is probed last (PROBE_ORDER is [incidentio, statuspage, instatus]). Its /v3/summary.json path does not collide with incident.io's /proxy/<host> or Statuspage's /api/v2/summary.json, and the probe additionally rejects Statuspage-shaped summaries (which carry page.id and a top-level status object).
Provider-agnostic. On startup and when adding a runtime monitor, the bot resolves an icon for embed author fields:
- If
iconUrlis set on the monitor config, use it directly (skips all fetching). - Otherwise,
GET <baseUrl>(HTML page). - Scan every
<link>tag whoserelcontainsicon(matchesrel="icon",rel="shortcut icon", andrel="apple-touch-icon"in any attribute order). - Rank candidates: non-SVG first (Discord embed author icons don't render SVG), then largest
sizes="WxH"wins. - Decode common HTML entities (
&,&,") and resolve relative/protocol-relative hrefs against the base URL. - Cached in memory (
monitorIconsMap) for embed author icons.
Use iconUrl to override auto-detection when a page's icon is injected by JavaScript, hosted on a CDN that rejects hotlinking, or otherwise unreachable for Discord's image fetcher.
- Non-200 responses: Adapters throw with status code and response body for debugging.
- Network errors: Caught at the poll level; logged and retried on next cycle.
-
Invalid status page URL:
/monitor addruns every provider'sprobe()and only accepts URLs where at least one returns success. - API rate limits: Not explicitly handled; public APIs are generous and the 60s default poll interval keeps request volume low.
-
Probe failures: A provider's
probe()should returnnullrather than throwing when the URL is not its own.detectProvider()swallows thrown probe errors and moves on to the next provider.
The bot maps status indicators to Discord embed colors. The mapping handles the union of statuses across all providers (after canonicalization).
| Status/Impact | Color | Hex |
|---|---|---|
| Operational / Resolved / None | Green | #2fb344 |
| Identified | Yellow | #f2c94c |
| Monitoring | Blue | #6aa9ff |
| Investigating / Minor / Degraded | Orange | #f2994a |
| Major / Critical / Major Outage | Red | #eb5757 |
| Under Maintenance | Grey | #8e8e93 |
| Maintenance | Dark Grey | #7f8c8d |
| Removed (ghost) | Light Grey | #95a5a6 |
| Unknown/Default | Discord Blurple | #5865f2 |