Status pages
Status page analytics
Every status page counts who looks at it, when, and whether agents read it. The counting happens on our server when the page is served. Nothing runs in the visitor's browser, and nothing that identifies a visitor is stored.
What you get
Open a status page in the dashboard and choose Analytics. Pick the last 24 hours, 7 days, 30 days or your plan's whole history window.
- Page views: people opening the page, its history or an incident page.
- Unique visitors: distinct people per day. Over several days it is the sum of the days, because a visitor cannot be recognised from one day to the next (see below).
- Agent reads: AI assistants, MCP clients, scripts and SDKs reading the page, its JSON, its verdict, its MCP server or its feeds.
- Subscribers: how many are active, how many joined and left, by type, and the email confirmation rate.
- A chart of people, agents and bots over time with incidents marked on it, the surfaces that were read, where visitors came from, countries, devices and languages.
- For every incident: views while it was open and afterwards, the busiest five minutes, how long until the first person looked, notifications sent and failed, and unsubscribes that followed.
The status pages list shows each page's views for the last seven days, and the weekly digest carries one line: “Your status page had 1,240 views (310 by agents) and 12 new subscribers.”
What is collected, exactly
One row per page and hour, and one per page and day. A row holds counters and nothing else:
| Stored | How it is derived |
|---|---|
| Reads per surface, split into people, agents and bots | The path that was served and the User-Agent, matched against a list of known agents and bots. Only the resulting class and a family name such as “ClaudeBot” or “curl” are kept. |
| An estimate of distinct visitors per day | A 1,024-register HyperLogLog sketch (see below). It cannot list visitors or tell whether a given one was there. |
| Referrer host | The Referer header cut down to its host name: https://news.example.com/item?id=4&token=… becomes news.example.com. Paths and query strings are never kept. At most 100 different hosts per page and day; the rest count as “other”. |
| Country (two letters) | Cloudflare's CF-IPCountry header, only when the request really came through Cloudflare. Custom domains are not proxied, so they have no country. |
| Device class | Desktop, phone or tablet, from the User-Agent. |
| Language | The first language of Accept-Language, two or three letters (de, not de-AT). |
| Page section and MCP tool | Home, history, incident page or subscribe page; the name of the MCP tool that was called. |
| Per incident: views in five-minute steps, the time of the first view | A read that happens while a public incident is open on the page is added to that incident. |
| Per incident: notifications sent and failed, by type | Counted when the outbox finishes a subscriber notification. No recipient is recorded. |
What is not collected
- No cookies, no local storage, no fingerprinting script, no pixel. The status page loads nothing from a third party for this, and its Content-Security-Policy is unchanged.
- No IP addresses. The address is used in memory for a few microseconds and never written to a database or a log by analytics.
- No User-Agent strings, no full referrer URLs, no query strings.
- No visitor ids and no per-visit rows. There is no table of visits: only counters per hour and day, so there is nothing to look up about a person, and nothing to export or erase per person.
- No cross-site or cross-page tracking. The visitor hash includes the page, so the same person on two status pages produces unrelated values.
How unique visitors are counted without identifying anyone
When a person's page view is counted, the server computes HMAC-SHA256(salt, page id | IP address | User-Agent). The salt is 40 random characters, created at the start of each UTC day and deleted when the day ends. The hash is not stored either: ten of its bits pick one of 1,024 registers and the following bits give a small number (how many zero bits it starts with), and the register keeps the largest number it has seen. The hash is then discarded.
What remains for a day is up to 1,024 integers between 1 and 33. From them the number of different visitors can be estimated (within one or two for small numbers, about ±3% for large ones), but no visitor can be recovered from them. Once the salt is gone, nobody, including us, can recompute yesterday's hashes from an IP address, and today's values cannot be linked to yesterday's. That is why a person who visits on Monday and on Tuesday counts as one visitor on each day.
People, agents, bots
The User-Agent decides, in this order:
- UpButler itself (our checks, regional probes, upstream poller and webhooks) is never counted.
- Agents: Claude, ClaudeBot, Claude Code, ChatGPT, GPTBot, OpenAI, Perplexity, Gemini, Mistral, Apple Intelligence, Meta AI, DuckAssist, Amazonbot, Bytespider, Common Crawl, Cohere and 30 more, including MCP clients,
curland the common HTTP libraries. A program that reads a machine endpoint without naming itself is an agent too, and so is every tool call on the page's MCP server. - Bots: search engines, link previews (Slack, Discord, X), other uptime monitors, feed readers and SEO crawlers: Googlebot, Bingbot, DuckDuckBot, Yandex, Baidu, Applebot, PetalBot, Seznam, Slack link preview, X link preview and others.
- People: everything else that is a browser.
A user agent is a claim, not proof: a script that pretends to be Chrome is counted as a person. The list is data in our code and grows as new agents appear.
Surfaces
| Surface | What counts |
|---|---|
| Status page (HTML) | the page, /history, /incidents/<id>, /subscribe |
| status.json and the public API | /status.json, GET /api/v1/public/pages/<slug>, …/incidents |
| status.agent.json (verdict) | /status.agent.json, /.well-known/status |
| Page MCP server (tool calls) | tools/call on /mcp, counted per tool |
| RSS and Atom feeds | /feed.rss, /feed.atom |
| llms.txt | /llms.txt |
| Badges | /badge/<slug>.svg, /status.svg, /uptime.svg |
| Embeds (status line, notice and iframe) | /embed/<slug>, /embed/<slug>.js |
| Report widget (form opened) | the report form opened from another site |
Badges, embeds and the report widget are shown on other people's sites: they are listed as surfaces but are not part of page views or agent reads.
What is deliberately not counted
- Members of your workspace looking at the page while signed in on upbutler.com, and anything the dashboard itself loads: the preview frames of the page editor and the badge previews.
- The page's own background refresh (an open tab reloads its content every minute; that is one view, not sixty) and browser prefetches.
- Private pages, error responses, and anything that is not a
GET. - RSS and Atom subscribers: feed readers are anonymous, so feed fetches are counted as reads, but there is no subscriber count for them.
Turning it off
Analytics are on for every page. In the Analytics tab, switch off Count views of this page and counting stops at once for that page; what was counted before stays visible until you choose Delete data. Both are available through the API:
curl -X PATCH https://upbutler.com/api/v1/pages/acme \
-H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
-d '{"settings": {"analytics": {"enabled": false}}}'
# and, if you want what was counted so far gone as well:
curl -X DELETE https://upbutler.com/api/v1/pages/acme/analytics -H "Authorization: Bearer $UPBUTLER_API_KEY"GDPR and ePrivacy
Nothing is stored on or read from the visitor's device, so the consent rule for cookies and similar technologies (ePrivacy Directive, Art. 5(3)) is not triggered, and no cookie banner is needed for this.
The IP address and User-Agent are personal data while a request is being answered, as they are for any web server. Analytics processes them only in memory, for the purpose of producing aggregate counts, and keeps aggregates that cannot be traced back to a person. We rely on legitimate interest (Art. 6(1)(f) GDPR): knowing whether a status page is reached during an incident, at no cost to the visitor's privacy. You are the controller for your status page's visitors and UpButler processes on your behalf, as described in the privacy policy. If you would rather not have it at all, switch it off.
This page describes what the software does; it is not legal advice. If your privacy notice lists the tools you use, a sentence like this one is accurate: “Our status page counts visits in aggregate, without cookies and without storing IP addresses.”
Retention
- Hourly rows: 9 days (the 24-hour and 7-day charts).
- Daily and per-incident rows: your plan's history window (Free 30 days, Starter 365 days, Pro 365 days, Business 730 days), with the same 30-day grace after a downgrade as the rest of your history.
- The daily salt: until the end of its UTC day.
- Deleting a status page deletes its analytics.
Plans
| Free | Starter | Pro | Business | |
|---|---|---|---|---|
| Page views, unique visitors, agent reads, subscriber growth, views chart | Yes | Yes | Yes | Yes |
| History you can look back over | 30 days | 365 days | 365 days | 730 days |
| People vs agents vs bots over time, surfaces, agent names, MCP tools, referrers, countries, devices, languages | – | Yes | Yes | Yes |
| Per-incident analytics | – | Yes | Yes | Yes |
Everything is counted on every plan, so the breakdowns of the past are there as soon as a workspace upgrades.
API and MCP
curl "https://upbutler.com/api/v1/pages/acme/analytics?range=7d" \
-H "Authorization: Bearer $UPBUTLER_API_KEY"{
"page": { "id": "pg_…", "slug": "acme", "name": "Acme Status" },
"enabled": true,
"range": { "key": "7d", "from": "2026-10-04T00:00:00.000Z", "to": "2026-10-11T00:00:00.000Z", "bucket": "6h", "timezone": "UTC" },
"plan": { "historyDays": 365, "breakdowns": true, "upgrade": null },
"totals": {
"views": 930, "uniqueVisitors": 412, "agentReads": 310,
"previous": { "views": 702, "uniqueVisitors": 330, "agentReads": 198 }
},
"series": { "bucket": "6h", "buckets": ["2026-10-04T00:00:00.000Z", "…"], "views": [12, 31, "…"],
"readers": { "human": [14, 33, "…"], "agent": [9, 7, "…"], "bot": [2, 0, "…"] } },
"incidents": [{ "id": "inc_…", "title": "Checkout errors", "impact": "major", "startedAt": "…", "resolvedAt": "…" }],
"subscribers": { "active": 184, "net": 11, "subscribed": 16, "confirmed": 12, "unsubscribed": 1, "confirmationRate": 0.75,
"byType": { "confirmed": { "email": 9, "webhook": 2, "slack": 1, "discord": 0 } }, "rss": { "countable": false, "feedReads": 96 } },
"breakdowns": {
"surfaces": [{ "surface": "html", "label": "Status page (HTML)", "human": 930, "agent": 41, "bot": 77, "total": 1048 }],
"referrers": [{ "host": "acme.com", "views": 388 }], "direct": 301,
"countries": [{ "country": "DE", "views": 210 }], "devices": [{ "device": "desktop", "views": 640 }],
"agents": [{ "name": "Claude", "reads": 120 }, { "name": "curl", "reads": 64 }],
"mcpTools": [{ "tool": "get_verdict", "calls": 58 }]
},
"week": { "views": 930, "agentReads": 310, "newSubscribers": 12,
"line": "Your status page had 1,240 views (310 by agents) and 12 new subscribers." }
}range is 24h (rolling, hourly buckets), 7d (default; six-hour buckets over seven UTC days), 30d (daily) or max (the plan's history window; weekly buckets beyond 92 days). totals.previous is the window before, or null when the plan's history does not reach that far. Without the breakdowns limit, breakdowns and series.readers are null and plan.upgrade names the plan that includes them. Breakdowns are kept per UTC day: for the rolling 24 hours they cover the one or two days it touches.
curl "https://upbutler.com/api/v1/pages/acme/analytics/incidents?range=30d" \
-H "Authorization: Bearer $UPBUTLER_API_KEY"
# one incident with its five-minute series: …/analytics/incidents?incidentId=inc_…{
"data": [{
"id": "inc_…", "title": "Checkout errors", "impact": "major", "status": "resolved",
"startedAt": "2026-10-08T09:12:00.000Z", "resolvedAt": "2026-10-08T10:02:00.000Z",
"views": { "during": { "human": 412, "agent": 88, "bot": 6 }, "after": { "human": 37, "agent": 4, "bot": 1 } },
"peak": { "at": "2026-10-08T09:20:00.000Z", "views": 96, "windowMinutes": 5 },
"secondsToFirstView": 41, "secondsToFirstAgentRead": 12,
"notifications": { "sent": 368, "failed": 2, "sentByType": { "email": 350, "webhook": 14, "slack": 4 },
"bounced": null, "secondsToFirstDelivery": 9 },
"unsubscribesAfter": 1
}]
}views.during are reads of the page (any surface that returns its content) while the incident was open; views.after are reads of the incident's own page once it was resolved. notifications.failed means the outbox gave up after its retries. bounced is always null: email bounces are not ingested. unsubscribesAfter counts subscribers who left within 48 hours of the incident's notifications. On a plan without breakdowns this endpoint answers 402 plan_limit.
The MCP tools are pages_analytics and pages_analytics_incidents (in the full toolset, not in core). An agent can answer “did anyone see the incident page?” with one call.