pod-telemetry #
The Cloudflare Worker + D1 ingest backend for pod's anonymous, opt-out telemetry
(spec mmmzstpq §6 / §9). It lives in-repo under web/telemetry/ and is deployed
manually with wrangler; CI only bakes the URL + write-key into the released
binary.
The Worker claims exactly the route pod.aaronmallen.dev/telemetry/* (only
/telemetry/v1/ingest is live). The marketing site at the apex stays untouched.
What it does #
POST /telemetry/v1/ingest validates a static write-key and the frozen JSON
contract, INSERTs the envelope into D1, and returns 204 No Content. It
never reads or stores the client IP — there is no reference to
CF-Connecting-IP or request.cf anywhere (grep-verifiable):
grep -rIn -e CF-Connecting-IP -e 'request\.cf' src/ # → no matches
Request pipeline (fail-closed, first failure wins, empty bodies) #
| step | condition | status |
|---|---|---|
| path | not /telemetry/v1/ingest |
404 |
| method | non-POST |
405 (Allow: POST) |
| key | X-Pod-Telemetry-Key missing / not in the valid set (constant-time compare) |
401 |
| size | Content-Length > 256 KiB, or the read exceeds 256 KiB |
413 |
| type | Content-Type not application/json |
415 |
| parse | invalid JSON | 400 |
| contract | closed-world validation fails (§9.4) | 400 |
| crash text | a crash free-text field trips the §5.6 reject | 400 |
| insert | D1 error | 500 |
| success | all checks pass | 204 |
The closed-world validator (src/contract.ts) accepts exactly the frozen
golden contract pinned by the shared fixtures
../../tests/fixtures/telemetry/session_all_streams.json and crash_batch.json
(the same files the Rust contract crate src/telemetry_contract.rs asserts).
Any unknown key anywhere is a rejection.
Retention #
A daily cron (0 4 * * *) runs the scheduled handler, which
DELETEs rows older than 90 days from both events and crashes, keyed on
received_at. The DELETE and the column DEFAULT use the identical
strftime('%Y-%m-%dT%H:%M:%SZ', …) form so the string comparison is correct.
Cron currently disabled. The Cloudflare account rejects the
/schedulesdeploy, so the cron is commented out inwrangler.tomland the worker runs without auto-prune (thescheduledhandler is still in the code). Re-enable the cron once Triggers work on the account. Until then, prune manually:npx wrangler d1 execute pod-telemetry --remote --command \ "DELETE FROM events WHERE received_at < strftime('%Y-%m-%dT%H:%M:%SZ','now','-90 days'); \ DELETE FROM crashes WHERE received_at < strftime('%Y-%m-%dT%H:%M:%SZ','now','-90 days');"
Dashboard #
GET /telemetry/admin serves a read-only maintainer dashboard: a single,
self-contained HTML page (inline <style>, inline <svg>, no external JS / CSS
/ CDN, no client-side JavaScript) rendering aggregate-only views over the live
D1 data via the existing DB binding. It shows six panels:
- Active installs —
COUNT(DISTINCT anon_id)over areceived_atwindow (default 30 days, override with?days=N, clamped to 1..365), drawn as an inline-SVG sparkline. Labeled honestly as distinct installs that opted in (anon_id=sha256(machine_id), one per install, not "users"). - Platform breakdown — distinct installs grouped by
os,os_version,arch,displayoverWHERE stream='environment'. The literal"unknown"bucket is shown, never filtered out. - Top features — usage events grouped by
event_kind+nameoverWHERE stream='usage', with the on/off split forfeature_togglerows. - Version adoption — distinct installs per
app_version. - Performance — average
load_ms,frame_p95_ms,heap_mbper view overWHERE stream='performance'. - Crash groups — grouped by
app_version+message(idx_crashes_group), each expandable (<details>) to its most recent backtrace + context log.
If more than one contract schema is present across the data, a schema-mix
panel is shown so different wire-contract versions are not silently aggregated.
The query layer (src/stats.ts) is pure: each function takes a D1Database
and returns typed, aggregate-only objects (SELECT-only, parameter-bound). It
is unit-tested in src/stats.test.ts against seeded fixtures, independent of
the HTTP route. The renderer (src/render.ts) HTML-escapes every DB-derived
string. Only aggregate counts leave the Worker; no raw events, anon_ids, or
PII are rendered.
Access control #
The route is gated by Cloudflare Access (configured in the CF dashboard,
NOT in code). It does not reuse the ingest write-key
(POD_TELEMETRY_KEY / POD_TELEMETRY_KEYS): that key ships inside released
binaries and is anti-abuse, not auth. As cheap defense-in-depth the Worker also
requires the Cf-Access-Jwt-Assertion header that Access injects (a request
that did not traverse the Access policy gets 403), but Access is the primary
gate; the Worker does not verify the JWT signature itself.
Set up the Access policy once in the CF dashboard:
- Cloudflare dashboard → Zero Trust → Access → Applications → Add an application → Self-hosted.
- Application domain:
pod.aaronmallen.dev, path/telemetry/admin. - Policy: Allow, single-user SSO — include rule Emails = the maintainer's address (or your IdP group). Everything else is blocked.
- Save. Now
/telemetry/adminrequires SSO;/telemetry/v1/ingestis unaffected (Access only covers the admin path).
Post-deploy verification #
After wrangler deploy:
# Admin route is reachable behind Access (interactive SSO in a browser):
open https://pod.aaronmallen.dev/telemetry/admin # → SSO, then the page
# A direct hit without an Access session is challenged/blocked by Access (302/403):
curl -sI https://pod.aaronmallen.dev/telemetry/admin | head -1
# Ingest auth is unchanged: a missing/bad key is still 401, NOT 404:
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
https://pod.aaronmallen.dev/telemetry/v1/ingest \
-H 'Content-Type: application/json' --data '{}' # → 401
Layout #
web/telemetry/
wrangler.toml route + D1 binding + daily cron
package.json tsconfig.json
src/index.ts fetch + scheduled handlers; admin route wiring
src/contract.ts closed-world validators (§6.1 / §9.4) + §5.6 reject
src/db.ts envelope → D1 rows (§6.2), parameter-bound
src/stats.ts read-only D1 aggregation queries (dashboard data)
src/render.ts dashboard data → self-contained HTML (presentation)
src/contract.test.ts loads the golden fixtures; asserts accept + tamper-reject
src/db.test.ts asserts the row mapping
src/stats.test.ts asserts the aggregations over seeded fixtures
src/render.test.ts asserts HTML escaping + self-contained output
migrations/0001_init.sql events + crashes tables (NO IP column)
migrations/0002_rename_display_add_screen_size.sql display -> window_size, add screen_size
migrations/0003_add_app_language.sql add app_language (chosen UI language)
README.md this file
web/telemetry/ is committed; .wrangler/ is gitignored.
Toolchain (mise + aube) #
node and aube come from .config/mise.toml. The TypeScript dev dependencies
— wrangler (v4), typescript, vitest, @cloudflare/workers-types — are
declared in package.json and installed by aube (locked in aube-lock.yaml),
so deploys use the same wrangler everywhere. No global installs.
mise install # node + aube
aube install # wrangler, typescript, vitest, workers-types → aube-lock.yaml
aube run typecheck # tsc --noEmit
aube test # vitest run
Deploy runbook (maintainer, once) #
-
mise use "npm:wrangler@latest"— one-time; writes the pin into.config/mise.toml; commit it. Thereaftermise installsuffices. -
wrangler login— the account owning zoneaaronmallen.dev. -
wrangler whoami— confirm the zone is visible. -
wrangler d1 create pod-telemetry→ paste the returneddatabase_idintowrangler.toml, commit. -
wrangler d1 migrations apply pod-telemetry --remote. -
wrangler secret put POD_TELEMETRY_KEY(e.g.openssl rand -hex 32); store the same value as the GitHub Actions secretPOD_TELEMETRY_KEY. -
wrangler deploy. -
Smoke-test against the §6.3 fixture body:
curl -i -X POST https://pod.aaronmallen.dev/telemetry/v1/ingest \ -H 'Content-Type: application/json' \ -H "X-Pod-Telemetry-Key: $KEY" \ --data @../../tests/fixtures/telemetry/session_all_streams.json # → 204 curl -i -X POST https://pod.aaronmallen.dev/telemetry/v1/ingest \ -H 'Content-Type: application/json' \ --data @../../tests/fixtures/telemetry/session_all_streams.json # → 401 (no key) curl -i https://pod.aaronmallen.dev/telemetry/v1/ingest # → 405 (GET) -
curl -I https://pod.aaronmallen.dev/— still the marketing site (route isolation). -
wrangler d1 execute pod-telemetry --remotewithSELECT stream, os, arch FROM events ORDER BY id DESC LIMIT 1;.
Write-key rotation (§9.6) #
The write-key is baked into long-lived released binaries; it is anti-abuse, not
auth. To rotate without 401-orphaning already-released binaries, the Worker
accepts a set of valid keys via POD_TELEMETRY_KEYS (comma-separated),
each constant-time-compared. POD_TELEMETRY_KEY is the degenerate
one-element case.
Deprecation window:
wrangler secret put POD_TELEMETRY_KEYSset toold_key,new_key.- Ship the next release baking
new_key. - After the prior release fleet ages out, set
POD_TELEMETRY_KEYSto justnew_key(or move it back toPOD_TELEMETRY_KEY).