feat: stats API v1 — cached, observable, contract-first #5

Merged
synapse merged 3 commits from feat/stats-api into develop 2026-08-21 13:26:25 +00:00
Owner

Implements the stats-api OpenSpec change — the gateway's reason to exist. All 16 tasks complete, full nix flake check green including the in-VM hurl contract suite.

Public surface (v1, versioned, minimal): GET /api/v1/summary?period=7d|30d (total + zero-filled per-day series), GET /api/v1/pages (top 20, events excluded), GET /api/v1/openapi.json (the contract, generated from the serving code). Everything else 404/405; stable {"error": code} shape; CORS * + Cache-Control with remaining TTL.

Why it holds up: upstream recon from GoatCounter 2.7.0 source confirmed hard rate limits (4/s, 500/h per token). The cache (4 enum keys, single-flight, TTL 300s, stale-to-24h) caps worst-case upstream load at 48 refreshes/hour independent of public traffic; upstream failures degrade to stale-serving with the original generated_at, then 502 only when nothing was ever cached. No retries against the rate-limited upstream, ever.

Observability (rich, as demanded): tracing spans per request; Prometheus on an internal-only listener — http_requests_total/duration, cache_events_total{hit,miss,stale_hit}, upstream_requests_total{ok,timeout,connect_error,http_4xx,http_5xx,invalid_body} + durations. Token provably never logged (redacted Debug + test).

Contract workflow (first full run): 6 hurl files written before any code (verified red), now green locally and inside the NixOS VM e2e (seeded GoatCounter + sandboxed gateway with LoadCredential token). openapi.json committed; new openapi-snapshot flake check fails the build on contract drift. 18 unit/wiremock tests cover every failure mode.

⚠️ Review flags (decided overnight, veto welcome — design.md D2/D3/D8): no public auth (public data + cache-bounded load), fixed period enum only, metrics listener defaults on at 127.0.0.1:9090 when deployed via the module.

Docs: new book chapter (Public API), dev/architecture chapters, README, CLAUDE.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T5j3dAJVcUjZv9k76sRTt9

Implements the `stats-api` OpenSpec change — the gateway's reason to exist. All 16 tasks complete, full `nix flake check` green including the in-VM hurl contract suite. **Public surface (v1, versioned, minimal):** `GET /api/v1/summary?period=7d|30d` (total + zero-filled per-day series), `GET /api/v1/pages` (top 20, events excluded), `GET /api/v1/openapi.json` (the contract, generated from the serving code). Everything else 404/405; stable `{"error": code}` shape; CORS `*` + `Cache-Control` with remaining TTL. **Why it holds up:** upstream recon from GoatCounter 2.7.0 source confirmed hard rate limits (4/s, 500/h per token). The cache (4 enum keys, single-flight, TTL 300s, stale-to-24h) caps worst-case upstream load at 48 refreshes/hour independent of public traffic; upstream failures degrade to stale-serving with the original `generated_at`, then 502 only when nothing was ever cached. No retries against the rate-limited upstream, ever. **Observability (rich, as demanded):** tracing spans per request; Prometheus on an internal-only listener — `http_requests_total/duration`, `cache_events_total{hit,miss,stale_hit}`, `upstream_requests_total{ok,timeout,connect_error,http_4xx,http_5xx,invalid_body}` + durations. Token provably never logged (redacted Debug + test). **Contract workflow (first full run):** 6 hurl files written before any code (verified red), now green locally *and* inside the NixOS VM e2e (seeded GoatCounter + sandboxed gateway with LoadCredential token). `openapi.json` committed; new `openapi-snapshot` flake check fails the build on contract drift. 18 unit/wiremock tests cover every failure mode. **⚠️ Review flags (decided overnight, veto welcome — design.md D2/D3/D8):** no public auth (public data + cache-bounded load), fixed period enum only, metrics listener defaults on at 127.0.0.1:9090 when deployed via the module. Docs: new book chapter (Public API), dev/architecture chapters, README, CLAUDE.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01T5j3dAJVcUjZv9k76sRTt9
hurl e2e suite written first (red), then: config (fail-fast, token
redacted), observability (tracing + prometheus on internal listener),
upstream client (outcome classification, no retries against the 4/s +
500/h rate-limited GoatCounter), cache (4-key TTL map, single-flight,
stale-to-24h, worst case 48 upstream refreshes/hour independent of
public traffic), v1 handlers (summary/pages/openapi.json, zero-filled
days, stable error shape, CORS + Cache-Control), utoipa contract with
committed openapi.json snapshot. 18 unit/wiremock tests green, clippy
clean, hurl 6/6 against live seeded GoatCounter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5j3dAJVcUjZv9k76sRTt9
Module: upstream url/host, tokenFile via LoadCredential, cacheTtl,
metricsListen (loopback default), allowedOrigin. Flake: openapi-snapshot
check, dev-e2e runner. VM test rewritten as full e2e: seeded GoatCounter
(pre-start root seed absorbed by StateDirectory chown), gateway with
credential, hurl suite executed in-VM, metrics asserted internal-only.
Book: Public API chapter; dev/arch chapters and README/CLAUDE.md updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5j3dAJVcUjZv9k76sRTt9
Both units crash-loop into systemd's start limit when booted without a
DB/credential; the test script starts them in seeded order instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5j3dAJVcUjZv9k76sRTt9
synapse merged commit 09856c05ee into develop 2026-08-21 13:26:25 +00:00
synapse deleted branch feat/stats-api 2026-08-21 13:26:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
synapse/goatcounter-stats-gateway!5
No description provided.