what we measure
Every dashboard panel, the data source behind it, and what writes the rows.
Every panel maps to one of three storage layers: live span ingest into D1, 1-minute pre-aggregates in Workers Analytics Engine (WAE), or the daily roll-up tables the cron worker produces every 15 minutes.
| Panel | Source table | Updated by |
|---|---|---|
| Cost per user · 7d | rollups_metrics_daily | cron */15 |
| Cost per repo · 7d | rollups_metrics_daily | cron */15 |
| Cost trend · 30d (sparkline + run-rate) | rollups_metrics_daily | cron */15 |
| Cache hit % per user · 7d | rollups_metrics_daily.cache_* | cron */15 |
| Tool approval rate · 7d | rollups_decisions_daily | cron */15 |
| Active developers (DAU/WAU/MAU) · 30d | rollups_metrics_daily distinct users | cron */15 |
| Top cost prompts · 7d | spans (group by prompt_id, cost computed live from tokens × pricing) | live ingest |
| Subagent fanout / depth per repo · 7d | spans (BFS at query time, grouped by repo) | live ingest |
Subagent tree (by prompt_id) | spans (parent / child walk) | live ingest |
Every panel honors the source filter chip at the top of the page (Claude Code · Agent SDK · Cowork · All). The source is detected at ingest from service.name resource attribute and persisted on every row.
Cowork events
Cowork emits org-level OTLP from its admin portal alongside Claude Code’s per-developer telemetry. Both streams hit the same /v1/{traces,metrics,logs} endpoint with the same shared org bearer, share prompt.id across the user turn, and land in the dashboards below.
| Cowork event category | Where it lands | Used by |
|---|---|---|
api_request | D1.events + per-prompt aggregate from spans | Cost trend · Top cost prompts |
tool_result | D1.events, joined with claude_code.tool spans | Subagent tree viewer |
| File access | D1.events | Investigate (per-prompt drill-down) |
| Skills / plugins invocation | D1.events | Investigate (per-prompt drill-down) |
| Human approval (approve / deny) | D1.tool_decisions → rollups_decisions_daily | Tool approval rate |
Approval events are the differentiator: generic backends ingest them as opaque log lines. We split approve vs deny per user, correlate them with the prompt.id they gated, and surface the rate in the Quality tab.
what we don’t store
- Raw prompt text. The
claude_code.interactionspan carries the prompt id, not the body. - Raw response bodies. Forensics tier (Scale) renders these via signed URLs from your own R2/S3 bucket — we never custody them.
- Per-token detail beyond aggregate input / output / cache_read / cache_creation counts.
- Persisted dollar amounts. Cost is computed at read time from tokens × the pricing table in
worker/src/pricing.ts. When Anthropic adjusts pricing, no historical row silently rewrites itself with stale dollars. - Trace data older than the tier retention window. 7d on Free, 30d on Pro, 90d on Scale.
why pre-aggregate
A 12-hour Claude Code session emits ~50,000 spans. Indexing every span at full fidelity is the cost driver behind generic OTel backends — they store everything, you pay for it. We collapse to ~200 WAE rows keyed by (org, user, repo, model, tool, status, metric_name, source) per 1-minute bucket. The dashboards you actually look at weekly only need that aggregate.
The 13 D1 tables are deliberately small: thin span metadata for the subagent-tree viewer, daily rollups (per source) for fast dashboard reads, sessions / OTP challenges for auth, members / org-level subscription state for billing. Everything else stays in WAE or is computed at read time.