Files
Vijay Janapa Reddi 7700726de2 chore(staffml): release polish — drop hash pin, skeletons, error reporting
Three small polish items flagged in the pre-release audit:

1. DROP release_hash pin
   The regression guard in staffml-validate-vault.yml compared vault.db's
   computed release_hash against a pinned value in
   interviews/vault/corpus-equivalence-hash.txt. That pin was load-bearing
   when corpus.json was the source of truth (guarded drift between
   committed-JSON and computed-from-YAMLs hash), but post-v1.0 the YAMLs
   ARE the source of truth and the hash is deterministic from them.
   The pin became a circular check that would bounce every YAML-touching
   PR unless the contributor remembered to manually bump the hash.
   Removed the pin comparison; the step now just runs vault build as a
   reproducibility smoke test. Real integrity still comes from vault
   check --strict + codegen drift earlier in the same workflow.
   Deleted interviews/vault/corpus-equivalence-hash.txt.

2. Hydration SKELETON for scenario
   Summary bundle ships scenario: "" and details with empty strings;
   useFullQuestion fetches the real content from the worker (~100-300ms
   warm, <5s cold). Before this commit the practice + plans pages showed
   a visibly empty region for that hydration window, then popped the
   scenario in — a text-FOUC.
   Added ScenarioSkeleton component (three pulsing bars of approximate
   paragraph height, aria-busy) and rendered it when current.scenario is
   empty on both practice and plans. Layout no longer jumps when real
   text arrives.

3. CLIENT-SIDE ERROR REPORTER
   Silent production regressions (like the getQuestionFullDetail shape
   mismatch in PR #1440) were only discoverable when a user said
   'getting an error'. Added a lightweight error reporter that hooks
   window.error + unhandledrejection, scrubs email patterns, rate-limits
   to 20 unique reports per tab, and pipes into the existing analytics
   worker as 'client_error' events. No new vendor dependency — reuses
   analytics-worker KV storage.
   Worker allowlist extended: adds 'client_error' event type + larger
   8 KiB per-event cap to fit stack traces + 'message/stack/url/
   userAgent' to the allowed-fields list.
   Installed from Providers.tsx at app mount.

Build verified green.
2026-04-22 12:17:12 -04:00
..

StaffML Analytics Worker

Lightweight Cloudflare Worker for collecting anonymous usage analytics from StaffML.

What It Collects

Anonymous events with no PII, no cookies, no persistent user IDs:

  • Question scores (topic, zone, level, track, score 0-3)
  • Gauntlet starts/completions
  • Issue reports and improvement suggestions
  • Daily challenge completions

Session IDs are ephemeral UUIDs that reset when the browser tab closes.

Setup

1. Install Wrangler

npm install -g wrangler
wrangler login

2. Create KV Namespace

cd interviews/staffml/analytics-worker
wrangler kv:namespace create STAFFML_ANALYTICS

Copy the returned namespace ID and update wrangler.toml:

[[kv_namespaces]]
binding = "STAFFML_ANALYTICS"
id = "<YOUR_KV_NAMESPACE_ID>"

3. Deploy

wrangler deploy

Note the URL (e.g., https://staffml-analytics.<your-subdomain>.workers.dev).

4. Configure StaffML

Set the analytics endpoint in your build environment:

# In the GitHub Actions workflow or .env.local:
NEXT_PUBLIC_ANALYTICS_URL=https://staffml-analytics.<your-subdomain>.workers.dev

Without this variable, analytics works in local-only mode (dashboard shows local data).

Endpoints

POST /

Accepts a batch of events:

{
  "events": [
    { "type": "question_scored", "topic": "roofline-analysis", "zone": "recall", "level": "L3", "track": "cloud", "score": 2, "_ts": 1712000000000, "_sid": "abc-123" }
  ]
}

Response: { "accepted": 1 }

GET /

Returns aggregate summary:

{
  "totalEvents": 1234,
  "last7Days": {
    "uniqueSessions": 42,
    "questionsScored": 380,
    "gauntletsCompleted": 15,
    "eventsByDay": { "2026-04-01": 50, ... },
    "scoresByLevel": { "L3": { "total": 120, "count": 50, "avg": "2.40" } }
  }
}

Security

  • CORS restricted to mlsysbook.ai, harvard-edge.github.io, and localhost
  • Max 100 events per request
  • Max 1KB per event
  • Email-pattern detection (rejects events containing PII)
  • Field allowlist (strips unknown fields)
  • 90-day TTL on stored data
  • No authentication required (anonymous by design)

Data Retention

Events are stored with a 90-day TTL in Cloudflare KV. After 90 days, they are automatically deleted. The running event counter (meta:total_events) persists indefinitely.