-
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 |
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.
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>.
Instatus (e.g. https://status.kagi.com) is not currently supported. Its only public endpoint is /summary.json, which returns flat active-incident metadata without message bodies or history. Adding it would require either synthesizing updates from polled state diffs (degraded fidelity) or scraping the HTML incident pages (fragile). The provider interface is designed to make dropping Instatus in later a one-file addition if a richer public API appears.
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 |