Cloudflare Worker that receives anonymous install-heartbeat pings from self-hosted subarr instances and serves aggregated stats.
Backs telemetry.subarr.com (the receiver) and subarr.com/stats (the public dashboard).
The privacy posture is enforced at the schema level. The worker has a hard allow-list of columns; anything else in the payload is silently dropped, and a payload containing forbidden fields (paths, titles, IPs, hostnames, API keys, tokens, languages, usernames, emails) is rejected entirely with HTTP 400.
Mirrors the regression test in subarr/tests/test_telemetry.py::test_payload_never_includes_forbidden_fields exactly. If you add a new payload field on the subarr side, add it here too in ALLOWED_FIELDS and the test will catch any drift.
| Column | Example | What for |
|---|---|---|
install_id |
fd19004ba4e14885910c09d06ff8cc71 |
Random UUID generated by subarr on first run. NOT a user identity. |
subarr_version |
1.0.0 |
Adoption + upgrade pace |
python_version |
3.12.13 |
Compat matrix |
os_arch |
Linux/x86_64 |
Build target priority |
docker_tier |
tier3 |
Which discovery mode users actually pick |
subgen_kind |
subarr-subgen / vanilla / unreachable |
Compat-mode adoption split |
subgen_version |
2026.05.3 |
Patch-rev adoption |
integrations_json |
{"bazarr":true,"sonarr":true,"tautulli":false,...} |
Per-integration usage % |
library_bucket |
1k_10k |
Distribution of library size |
scheduler_mode |
manual_confirm / auto_queue / disabled |
Workflow split |
walks_per_day |
4.2 |
Cadence distribution |
error_counts_json |
{"SubgenUnavailable":3,"BazarrTimeout":1} |
Top exception classes globally |
File paths, episode/movie titles, IPs, hostnames, API keys, OAuth tokens, language identifiers, usernames, email addresses, anything user-fingerprintable. The forbidden-pattern check in validatePayload() rejects 400 if any of these fields appear in the inbound JSON.
Per install_id:
MIN_INTERVAL_S(default 3600s): pings closer than this are 429'd with{ok:false, reason:"rate_limited"}. Normal daily-cadence client never hits this.FLOOD_THRESHOLD_S(default 60s): pings closer than this are 429'd with{ok:false, flood_detected:true, user_message:"..."}. Subarr's Settings panel surfacesuser_messageverbatim so the user sees "Your install is pinging too often, this is likely a bug, please file an issue."- After 3 consecutive floods, the install_id is
flagged=1ininstall_statefor permanent attention.
This is the in-product alert path you wanted — no Discord/email webhook, just a structured response field the subarr UI knows how to render.
| Method + path | What it does |
|---|---|
POST /v1/ping |
Receive a heartbeat. Returns {ok:true} or rate-limit / validation error |
GET /v1/health |
Liveness check (no DB read) |
GET /v1/stats/installs |
Active installs in last 7d / 30d + total |
GET /v1/stats/subgen-mix |
% running subarr-subgen vs vanilla vs unreachable (last 30d, latest per install) |
GET /v1/stats/integrations |
Per-integration usage % (last 30d, latest per install) |
GET /v1/stats/library-size |
Distribution across library buckets |
GET /v1/stats/walks-per-day |
Distribution across cadence buckets |
GET /v1/stats/scheduler-modes |
manual_confirm vs auto_queue vs disabled distribution |
The stats endpoints are public read-only, CORS-allowed for subarr.com and www.subarr.com. They power the planned subarr.com/stats dashboard.
npm install
npm run db:init:local # create local D1 with the baseline schema
npm run dev # wrangler dev — http://localhost:8787
npm test # unit tests# One time setup
wrangler login
wrangler d1 create subarr-telemetry
# Copy the returned UUID into wrangler.toml's database_id field
npm run db:init # apply schema/001_baseline.sql to remote D1
wrangler deploy
# Then in the Cloudflare dashboard:
# Workers & Pages -> subarr-telemetry -> Settings -> Triggers -> Custom Domains
# add telemetry.subarr.comGitHub Actions runs vitest on every PR. On push to main, it auto-deploys via wrangler deploy. Requires two repo secrets:
CLOUDFLARE_API_TOKEN— scoped to Workers + D1 for this accountCLOUDFLARE_ACCOUNT_ID— visible in the Cloudflare dashboard URL
MIT.