Appearance
API
The dashboard is a plain SPA talking to these endpoints. They are documented because you may want to script against them — a deploy check, a status page — not because they are a stable public contract. Treat them as internal to the minor version.
All paths are relative to route (/_monitor by default). Everything except POST /api/ingest and POST /api/login requires a valid session cookie, and answers 404 while no credentials are configured at all.
POST /api/login
json
{ "username": "admin", "password": "…" }Sets the session cookie. Answers 401 for a wrong username or a wrong password, with the same message either way — telling the two apart would confirm a valid username to somebody guessing.
POST /api/logout
Clears the session. Requires a same-origin Origin header.
POST /api/session
Whether the current cookie is valid.
json
{ "authenticated": true }GET /api/dashboard
Everything the overview screen draws, in one call: totals, a trend carrying requests and errors on the same buckets, the breakdowns with each slice's share of traffic beside its errors, the busiest endpoints, the biggest contributor and the most recent issues.
| Parameter | Meaning |
|---|---|
window | Milliseconds |
facets | Comma-separated dimensions to break down by |
| Facet names | Repeatable — narrows every figure at once |
One endpoint rather than one per widget: the numbers on a screen have to describe one instant, and six requests are six chances to disagree about which.
GET /api/overview
The older, narrower version of the same thing: error counts by side, issue counts, a trend series, the worst routes and the most recent issues.
Query: hours (default 24).
GET /api/issues
The issue list.
| Parameter | Meaning |
|---|---|
side | client or server |
resolved | true or false |
search | Message, file or route |
type | Error type |
manual | true for exception() reports only, false for caught only |
group | A priority group, e.g. payments |
level | info, warning, error or critical |
kind | api, page or asset |
limit, offset | Paging |
| Facet names | Repeatable — ?browser=Safari&browser=Firefox |
json
{ "issues": [ … ], "total": 128 }GET /api/issues/:fingerprint
One issue with its recent occurrences, each with its stack resolved to source. Also returns the facet counts for the breakdown, the number of distinct sessions, and how many occurrences match the filter.
Accepts the same facet parameters, so the breakdown and the occurrences below it always describe the same slice.
PATCH /api/issues/:fingerprint
json
{ "resolved": true }Requires a same-origin Origin header. A resolved issue reopens by itself if it happens again.
GET /api/facets
Facet counts over a window, for the filter panel.
Query: window (milliseconds), plus any facet filters to count within.
GET /api/interactions
What was pressed, ranked, optionally within one page. Counts per (route, label) — never a sequence, never tied to a session.
| Parameter | Meaning |
|---|---|
window | Milliseconds |
route | Narrows to one page; normalised, so /posts/1 finds /posts/:id |
limit | Rows returned, 1–200 (default 20) |
share is measured against every press in the window, not only the rows returned — a truncated list would otherwise report its top row as dominant.
GET /api/stats
Releases, routes, sessions and environments — the four section screens.
Query: window, and section to fetch just one (releases, routes, sessions, environments, traffic, heatmap).
environments also returns traffic_facets: the same dimensions counted over page views, so a share can be judged against the audience rather than against other errors.
They share one endpoint because they are read together and describe the same window; splitting them would mean four round trips and four chances for the numbers on one screen to disagree about which instant they describe.
GET /api/health
The collector's own state. See Storage.
GET /api/notifications
What is configured and what has been sent. See Notifications.
Query: limit (default 100, max 200).
json
{
"enabled": true,
"channels": [{ "name": "ops-chat", "type": "telegram", "enabled": true }],
"triggers": { … },
"cooldownMinutes": 60,
"deliveries": [
{ "id": 41, "at": 1737000000000, "channel": "ops-chat", "reason": "regression",
"fingerprint": "9f2c…", "alerts": 1, "status": "sent" }
]
}Channel names and types only — a token in a response is a token in a browser's memory and in anything that later reads it.
status is sent, failed with the reason the channel gave, or suppressed with the rule that silenced it. The failures are the point: the question asked of an alerting system is "why did nobody tell me?", and the answer is never among the successes.
POST /api/notifications
Sends a test alert to every configured channel and returns what happened. Takes no body — what a test sends is not the caller's choice, or the endpoint becomes a way to post arbitrary text through somebody's bot token.
json
{ "sent": true, "deliveries": [ … ] }Answers { "sent": false, "reason": … } when no channel is configured.
GET /api/uptime
The calm-days bar: one entry per day, each with a verdict and the counts behind it, plus how many of the recorded days were calm.
Query: days (default 90, max 365).
Not bounded by the dashboard's window — see the overview.
GET /api/export
Downloads the stored data. Streamed, so an export of a large table does not build the whole body in memory.
| Parameter | Meaning |
|---|---|
table | issues (default) or events |
format | json (default) or csv |
since | Epoch ms; only rows at or after it |
limit | Row ceiling. Defaults to 50,000; limit=0 means everything |
CSV cells beginning =, +, - or @ are prefixed with an apostrophe: those are formulas to Excel and Sheets, and error messages are attacker-influenced text.
POST /api/ingest
The one route without a session — the browser posts here and has no credentials to offer. It is therefore treated as hostile input:
- Same-origin only. A cross-origin post cannot be a genuine report from your app, and letting one through would let any site fill your database.
- Rate limited to 100 events per address per minute.
- Bounded body at 512 KB, checked before anything parses it.
- Bounded fields: 20 events per batch, 10 KB per stack, 1 KB per message, 30 breadcrumbs.
Answers 202 when events were accepted, 413 for an oversized body, 429 when rate limited, and 204 when it declined for any other reason — a cross-origin post, an unreadable body, an empty batch. A client cannot act on a more precise answer, and a precise one would help somebody probing the limits.
