CareerVector Analytics Event Catalog

Status: CANONICAL. This document is the contract for all telemetry emitted by CareerVector. Every event type must appear here before shipping. Adding a new event without updating this catalog is a merge-blocking defect.

Audience: Contributors adding observability, engineers writing queries, and anyone auditing privacy posture.

Implementation references:

  • Client yellow tier: ui/src/lib/telemetry/yellow.ts
  • Client red tier: ui/src/lib/telemetry/red.ts
  • Server ingest: api/src/routers/telemetry.ts (POST /telemetry/event)
  • P2P-specific ingest: api/src/routers/telemetry.ts (POST /telemetry/p2p)
  • Architecture: REALTIME-ARCHITECTURE-V3.md §5.2

1. Schema Overview

Egress: off by default. Browser telemetry no-ops unless the bundle is built with VITE_AXIOM_DATASET and VITE_AXIOM_INGEST_TOKEN. Controlled diagnostic cohorts push directly from the browser to Axiom's EU Frankfurt regional ingest endpoint — no server proxy, no CF Worker relay.

Known datasets for explicit diagnostic rollouts:

Environment Dataset Region
Production cv_telemetry EU Frankfurt (eu-central-1.aws)
Staging / dev cv_telemetry_dev EU Frankfurt (eu-central-1.aws)

Ingest URL gotcha: The correct path shape is /v1/ingest/{dataset} (on the edge URL). The control-plane path /v1/datasets/{dataset}/ingest is NOT used for the regional edge — it returns 404. Always use https://eu-central-1.aws.edge.axiom.co/v1/ingest/{dataset}.

Axiom event format

Events are JSON objects posted as a JSON array (one HTTP call per flush batch). Every event includes a tier field ('yellow'/'red') identifying which bundle emitted it. There is no separate env field — the dataset boundary (production vs dev) carries that.

Our event field convention

Each event object follows this structure:

Field Type Content
type string Event type string (matches the catalog name, e.g. op_applied)
tier string Emitting tier: 'yellow' / 'red'
ts number Client-side unix timestamp (ms)
workspaceId string Workspace ID (yellow/red only)
peerId string Peer ID (yellow/red only)
dailyToken string Non-reversible daily hash (diagnostic heartbeat only)
tierBucket string Configured tier bucket (heartbeat only: 'yellow'/'red')
(event-specific fields) various Documented per event type below

All field names are literal JSON keys — Axiom stores the raw JSON object. Query via Axiom APL (see §4).

Fields beyond the convention are documented per event type where they deviate.


2. Per-Tier Event Types

Green is silent and loads no telemetry module. Yellow and red are explicit diagnostic tiers; red is a superset of yellow. Server-side enforcement rejects tier-mismatched events as a second defense layer.

2.1 Green Tier — Silent

Green is the default target for public/free users. It emits no events, creates no telemetry localStorage keys, and makes no telemetry network requests.

Event count: 0


aggregate_heartbeat

Attribute Value
Tier required Yellow or red diagnostic cohorts only
Emitter yellow.ts / red.tsinitAggregate()
Frequency At most once per workspace per calendar day (localStorage dedup key cv_tele_agg_<id>)
When fired On workspace init, after the 30-char daily token is derived

Data point layout:

Slot Content Example
blobs[0] 'aggregate_heartbeat' aggregate_heartbeat
blobs[1] Daily token — SHA-256(workspaceId:YYYY-MM-DD), first 32 hex chars. Non-reversible. a3f1c2...
blobs[2] Tier bucket string green / yellow / red
doubles[0] Client-side unix timestamp (ms) 1714000000000
indexes[0] Tier bucket green

What a bug looks like here: A sudden drop in daily unique tokens for a given tier bucket indicates users are not reaching workspace init (JS crash before init, broken network path to ingest endpoint, or dedup key pollution). A spike in a specific tier bucket indicates unexpected tier assignment — e.g. all new workspaces landing in red when they should default to green at GA.


2.2 Yellow Tier — Support Event Stream

Yellow is for testers and early adopters. workspace_id IS transmitted. Events give full op-level visibility to answer "what did the user do and what broke."

Event count: 18

Rate limits: 10 events/second per type, ring buffer of 200 events max, flushed every 30 s or on visibilitychange:hidden.


tier_transition

Attribute Value
Tier required Yellow
Emitter emitTierTransition(from, to)
When fired When the workspace diagnostic tier changes at runtime
Slot Content
blobs[0] 'tier_transition'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] New tier string (to argument)
doubles[0] 0
doubles[2] Timestamp (ms)
Payload field fromTier — previous tier (not mapped to a blob slot; visible in raw JSON before server normalization)

Bug pattern: Unexpected tier transitions in production indicate the tier-gate UI was triggered accidentally, or the settings migration logic is assigning wrong tier values on workspace creation.


webrtc_failure

Attribute Value
Tier required Yellow
Emitter emitWebRTCFailure(reason)
When fired When WebRTC ICE negotiation fails (STUN timeout, TURN unavailable, etc.)
Slot Content
blobs[0] 'webrtc_failure'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Failure reason string (truncated to 200 chars)
doubles[2] Timestamp (ms)

Bug pattern: A sudden spike or sustained elevation in webrtc_failure rate, especially correlated across multiple workspaces or geos, indicates a STUN/TURN regression. Cross-reference with p2p_tier events: if T1 rate drops and webrtc_failure rises simultaneously, the STUN server configuration or ICE candidate gathering is broken.


cold_load_latency

Attribute Value
Tier required Yellow
Emitter emitColdLoadLatency(ms)
When fired After a cold workspace load completes (new device, first visit, cache miss)
Slot Content
blobs[0] 'cold_load_latency'
blobs[1] workspace_id
blobs[2] peer_id
doubles[0] Load latency in ms (rounded)
doubles[2] Timestamp (ms)

Bug pattern: p99 climbs above 3 s indicates D1 snapshot fetch is regressing. If p50 is stable but p99 spikes, suspect timeout or retry logic. Flat histogram at a suspiciously round number (e.g. exactly 30000 ms) indicates the client hit a timeout sentinel.


connection_error

Attribute Value
Tier required Yellow
Emitter emitConnectionError(msg)
When fired When the sync layer encounters a non-fatal but observable error (WebSocket close, DO unavailable, etc.)
Slot Content
blobs[0] 'connection_error'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Error message (truncated to 300 chars)
doubles[2] Timestamp (ms)

Bug pattern: Sustained rate of connection_error without matching sync_connected follow-ups means the reconnect loop is failing. If the message string cluster around a specific phrase (e.g. "WebSocket is closed" or "ice failed"), that phrase is the regression surface.


sync_connected

Attribute Value
Tier required Yellow
Emitter emitSyncConnected(tier)
When fired When a peer successfully establishes sync on any transport (P2P or DO relay)
Slot Content
blobs[0] 'sync_connected'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Transport tier string (e.g. T1, T2, T3)
doubles[2] Timestamp (ms)

Bug pattern: If sync_connected stops appearing for a workspace that was previously active, the peer lost connectivity and did not recover. Absence of sync_connected across a fleet of peers simultaneously indicates an infrastructure outage (DO worker, D1, or TURN service).


sync_disconnected

Attribute Value
Tier required Yellow
Emitter emitSyncDisconnected(reason?)
When fired When a peer loses its sync connection
Slot Content
blobs[0] 'sync_disconnected'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Disconnect reason (optional, truncated to 100 chars)
doubles[2] Timestamp (ms)

Bug pattern: High sync_disconnected rate with no matching sync_connected recovery indicates the reconnect logic is broken or the server is rejecting reconnects. Pair with connection_error to distinguish client-side failures from server-side rejections.


op_applied

Attribute Value
Tier required Yellow
Emitter emitOpApplied(opType, latencyMs, outcome)
When fired After each Yjs op is applied locally — the core user-activity signal
Slot Content
blobs[0] 'op_applied'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Op type string (e.g. 'insertSection', truncated to 80 chars)
doubles[0] Apply latency in ms (rounded)
doubles[2] Timestamp (ms)
outcome field 'ok' / 'error' / 'rejected' (not a blob slot; in raw JSON)

Bug pattern: Op rate flatlines (near-zero op_applied rate for an active workspace) indicates sync is broken — ops are not flowing through. A spike in outcome='error' or outcome='rejected' indicates a CRDT schema validation regression. p99 apply latency climbing above 100 ms on a well-connected client indicates a Yjs GC or IndexedDB write bottleneck.


agent_tool_call

Attribute Value
Tier required Yellow
Emitter emitAgentToolCall(toolName, outcome, latencyMs)
When fired When an MCP agent invokes a tool on the workspace
Slot Content
blobs[0] 'agent_tool_call'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Tool name (truncated to 80 chars)
doubles[0] Call latency in ms (rounded)
doubles[2] Timestamp (ms)
outcome field 'ok' / 'error'

Bug pattern: A sustained outcome='error' rate for a specific tool name indicates that tool's implementation has regressed. High latency on agent_tool_call indicates the agent is blocked on slow workspace state resolution.


ai_op_result

Attribute Value
Tier required Yellow
Emitter emitAiOpResult(providerId, tokenCount, outcome)
When fired After an AI provider returns a result for an LLM operation
Slot Content
blobs[0] 'ai_op_result'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Provider ID (truncated to 40 chars, e.g. groq, mistral)
doubles[0] Token count
doubles[2] Timestamp (ms)
outcome field 'ok' / 'error'

Bug pattern: A spike in outcome='error' for a specific provider indicates an API key expiry, provider outage, or cascade misconfiguration. High and growing token counts for a fixed operation type indicate prompt bloat.


api_call

Attribute Value
Tier required Yellow
Emitter emitApiCall(endpoint, statusCode, latencyMs)
When fired After each server API call completes
Slot Content
blobs[0] 'api_call'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Endpoint pattern (truncated to 120 chars, e.g. /api/workspaces)
doubles[0] Latency in ms (rounded)
doubles[2] Timestamp (ms)
statusCode field HTTP status code

Bug pattern: p99 latency climbing for a specific endpoint indicates a D1 query regression or Worker CPU budget issue. A spike in statusCode=5xx for any endpoint indicates a server error; cross-reference with api_error events for the error message.


api_error

Attribute Value
Tier required Yellow
Emitter emitApiError(endpoint, statusCode, msg)
When fired When a server API call returns a non-2xx status
Slot Content
blobs[0] 'api_error'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Endpoint pattern (truncated to 120 chars)
doubles[0] Status code (cast to double, e.g. 404.0)
doubles[2] Timestamp (ms)
reason field Error message (truncated to 200 chars)

Bug pattern: A sudden appearance of 404 errors on a previously stable endpoint indicates a route removal or rename regression. 429 errors indicate a client-side rate limit being hit; check the cascade retry logic.


Attribute Value
Tier required Yellow
Emitter emitPageNavigate(route)
When fired On SvelteKit navigation (route change)
Slot Content
blobs[0] 'page_navigate'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Route string (truncated to 80 chars)
doubles[2] Timestamp (ms)

Bug pattern: If page_navigate events stop appearing for an active session, client-side routing has broken. A route that was previously frequently visited disappearing from the distribution indicates a navigation regression — links removed, route renamed, or redirect loop.


Attribute Value
Tier required Yellow
Emitter emitModalOpen(modalId)
When fired When a modal dialog is opened
Slot Content
blobs[0] 'modal_open'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Modal ID string (truncated to 80 chars)
doubles[2] Timestamp (ms)

Bug pattern: modal_open events appearing without a matching modal_close within a reasonable window indicate the close handler is broken or the modal is not rendering (the user can't close it). A modal ID that appears in modal_open events but never in render-success metrics indicates the modal component crashed before mounting.


Attribute Value
Tier required Yellow
Emitter emitModalClose(modalId)
When fired When a modal dialog is closed
Slot Content
blobs[0] 'modal_close'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Modal ID string (truncated to 80 chars)
doubles[2] Timestamp (ms)

js_error

Attribute Value
Tier required Yellow
Emitter Auto-wired via window.onerror and window.onunhandledrejection in initSupport()
When fired On any uncaught JS exception or unhandled Promise rejection
Slot Content
blobs[0] 'js_error'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Error message string — ErrorType: message format (truncated to 300 chars)
doubles[2] Timestamp (ms)

Bug pattern: Any non-zero rate of js_error after a deploy warrants investigation. A spike in a new error message clustered immediately after a deploy SHA indicates a JavaScript regression introduced by that deploy. Cross-reference with op_applied rate: if ops flatlined at the same time an error spike started, the error is in the sync/apply path.


workspace_state_change

Attribute Value
Tier required Yellow
Emitter emitWorkspaceStateChange(snapshot)
When fired When workspace structural state changes (job count, phase distribution, etc.)
Slot Content
blobs[0] 'workspace_state_change'
blobs[1] workspace_id
blobs[2] peer_id
doubles[2] Timestamp (ms)

Note: The stateSnapshot payload carries workspace metadata (job counts, phase distribution) as JSON. This is NOT mapped to blobs/doubles by the server normalizer — it is visible in the raw event JSON before server-side ingestion flattens it. The snapshot must never include job titles, company names, or any CV/CL content.


settings_change

Attribute Value
Tier required Yellow
Emitter emitSettingsChange(category)
When fired When workspace settings are changed
Slot Content
blobs[0] 'settings_change'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Settings category string (truncated to 80 chars)
doubles[2] Timestamp (ms)

perf_marker

Attribute Value
Tier required Yellow
Emitter emitPerfMarker(name, durationMs)
When fired At named performance checkpoints (Typst compile time, snapshot load, etc.)
Slot Content
blobs[0] 'perf_marker'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Marker name (truncated to 80 chars)
doubles[0] Duration in ms (rounded)
doubles[2] Timestamp (ms)

Bug pattern: p99 of a named marker (e.g. typst_compile) climbing indicates a Typst WASM or document complexity regression. A marker that previously appeared regularly disappearing indicates the code path is no longer being reached.


2.3 Red Tier — Verbose Session Replay

Red is for internal development only. All yellow events are included. Red adds session-replay-grade input capture and Yjs wire-level instrumentation.

Rate limits: 20 events/second total (token bucket), flushed every 1 s.

Additional event count: 11 (on top of yellow's 18)


op_round_trip

Attribute Value
Tier required Red
Emitter emitOpRTT(rttMs)
When fired After each Yjs op round-trip completes (send → remote ack → local apply)
Slot Content
blobs[0] 'op_round_trip'
blobs[1] workspace_id
blobs[2] peer_id
doubles[0] Round-trip time in ms (rounded)
doubles[2] Timestamp (ms)

bytes_sent

Attribute Value
Tier required Red
Emitter emitBytesSent(bytes)
When fired After each Yjs update frame is sent over the wire
Slot Content
blobs[0] 'bytes_sent'
blobs[1] workspace_id
blobs[2] peer_id
doubles[0] Byte count for this frame
doubles[2] Timestamp (ms)

bytes_received

Attribute Value
Tier required Red
Emitter emitBytesReceived(bytes)
When fired After each Yjs update frame is received
Slot Content
blobs[0] 'bytes_received'
blobs[1] workspace_id
blobs[2] peer_id
doubles[0] Byte count for this frame
doubles[2] Timestamp (ms)

sync_step

Attribute Value
Tier required Red
Emitter emitSyncStep(step)
When fired When the Yjs sync protocol emits a step-1 or step-2 sync message
Slot Content
blobs[0] 'sync_step'
blobs[1] workspace_id
blobs[2] peer_id
doubles[0] Step number (1 or 2)
doubles[2] Timestamp (ms)

Bug pattern: Repeated step-1/step-2 cycles without settling indicates a convergence loop — two peers are continuously re-syncing without reaching identical state. This points to a Yjs document corruption or a mismatch in Y.Doc provider initialization.


awareness_update

Attribute Value
Tier required Red
Emitter emitAwarenessUpdate(bytes)
When fired When a Yjs Awareness protocol frame is sent or received
Slot Content
blobs[0] 'awareness_update'
blobs[1] workspace_id
blobs[2] peer_id
doubles[1] Awareness frame size in bytes
doubles[2] Timestamp (ms)

keydown

Attribute Value
Tier required Red
Emitter Auto-wired via window.addEventListener('keydown') in registerDomListeners()
When fired On every keydown event in the window
Slot Content
blobs[0] 'keydown'
blobs[1] workspace_id
blobs[2] peer_id
blobs[3] Key name — 'char' for printable characters (content never captured), or the key name for non-printable keys (e.g. Enter, Backspace, ArrowUp, truncated to 30 chars)
doubles[2] Timestamp (ms)
targetType field HTML element tag name of the event target (lowercase)

Privacy note: Single-character keypresses are replaced with the literal string 'char'. The actual character is never stored. Non-printable key names (navigation, modifiers, function keys) are stored as-is.


mouseup

Attribute Value
Tier required Red
Emitter Auto-wired via window.addEventListener('mouseup')
When fired On every mouseup event
Slot Content
blobs[0] 'mouseup'
blobs[1] workspace_id
blobs[2] peer_id
doubles[2] Timestamp (ms)
targetType field HTML element tag name (lowercase)
targetId field data-testid attribute or element id (truncated to 80 chars, empty string if absent)

Privacy note: Mouse coordinates are never captured. Only the element type and test ID are recorded, which identify UI element intent without revealing where on screen the user clicked.


scroll

Attribute Value
Tier required Red
Emitter Auto-wired via window.addEventListener('scroll', ..., { capture: true })
When fired On scroll events, throttled to at most 1 per second
Slot Content
blobs[0] 'scroll'
blobs[1] workspace_id
blobs[2] peer_id
doubles[2] Timestamp (ms)

hover

Attribute Value
Tier required Red
Emitter Auto-wired via window.addEventListener('mouseover')
When fired On mouseover events, throttled to at most 2 per second
Slot Content
blobs[0] 'hover'
blobs[1] workspace_id
blobs[2] peer_id
doubles[2] Timestamp (ms)
targetType field HTML element tag name (lowercase)
targetId field data-testid or element id (truncated to 80 chars)

clipboard_event

Attribute Value
Tier required Red
Emitter Patched navigator.clipboard.readText and writeText in registerDomListeners()
When fired When the app reads from or writes to the clipboard via the Clipboard API
Slot Content
blobs[0] 'clipboard_event'
blobs[1] workspace_id
blobs[2] peer_id
doubles[2] Timestamp (ms)
clipboardAction field 'read' or 'write'

Privacy note: Clipboard contents are never captured. Only the direction (read or write) is recorded.


dom_snapshot

Attribute Value
Tier required Red
Emitter Manual call site in error recovery logic
When fired On error, to capture the serialized active view state for debugging
Slot Content
blobs[0] 'dom_snapshot'
blobs[1] workspace_id
blobs[2] peer_id
doubles[2] Timestamp (ms)
domSnapshot field Compressed JSON string of the active view DOM state

2.4 P2P Tier Event — Dedicated Endpoint

This event is sent to /api/telemetry/p2p rather than /api/telemetry/event. No tier gate is applied (all workspaces report P2P tier data). The event feeds the regression alert model for STUN/TURN coverage.

Event count: 1


p2p_tier

Attribute Value
Tier required None — all workspaces
Emitter reportTierTelemetry() in connection-impl.ts
When fired Once per minute per peer while connected (fire-and-forget from leader tab)
Slot Content
blobs[0] 'p2p_tier'
blobs[1] Workspace ID (wsId)
blobs[2] Peer ID
blobs[3] Transport tier: 'T1' / 'T2' / 'T3'
blobs[4] TURN availability: 'turn_available' or 'stun_only'
doubles[0] Round-trip latency in ms (rounded)
doubles[1] Byte rate inbound (bytes/s, rounded)
doubles[2] Byte rate outbound (bytes/s, rounded)
indexes[0] Transport tier — enables AE dimension filtering on T1/T2/T3

Bug pattern: T1 success rate dropping 10 %+ across the fleet (relative to baseline) indicates a STUN server misconfiguration, ICE candidate gathering regression, or a browser update that changed WebRTC behaviour. The turn_available vs stun_only split distinguishes whether T2 ICE config was offered; if stun_only rises without a corresponding T2 config change, the TURN credential endpoint may be returning errors.


3. Bug-Revealing Patterns

Concrete playbooks for common regression types. Each pattern names the query anchor event, what signal to watch, and how to distinguish root causes.

3.1 STUN/TURN Coverage Regression

Signal: T1 rate in p2p_tier drops 10 %+ over a rolling 24 h window. T3 rate rises correspondingly.

Distinguish root cause:

  • If turn_available='stun_only' rate is stable but T1 success drops → STUN servers are returning bad ICE candidates or timing out.
  • If turn_available='stun_only' rate rises → the TURN credential endpoint is failing; T2 ICE config not being offered.
  • If webrtc_failure spikes at the same time → ICE negotiation is failing outright, not just falling back.

Check: p2p_tier events grouped by blobs[3] (tier) and blobs[4] (TURN label), 24 h rolling.


3.2 JavaScript Regression

Signal: js_error rate spikes above baseline. Correlate with deploy timestamp.

Distinguish root cause:

  • Cluster blobs[3] (error message) to find the dominant error string.
  • If a single new error message dominates → that error is the regression surface; trace to the component by the error type prefix.
  • If op_applied rate drops simultaneously → the error is in the sync/apply path; severity is high.
  • If op_applied is unaffected → the error is in a background or UI path; lower priority.

3.3 Op Rate Flatline (Sync Broken)

Signal: op_applied event rate drops to near-zero for a workspace or fleet that was previously active.

Distinguish root cause:

  • If sync_connected events also stopped → the peer lost its connection and did not reconnect.
  • If sync_connected events are present but ops are absent → the connection is live but updates are not flowing (e.g. Yjs provider not forwarding ops to the sync layer).
  • If js_error events preceded the flatline → the error broke the apply path.

3.4 Modal Render Regression

Signal: modal_open events appearing for a given modalId, with no subsequent modal_close within 60 s AND no corresponding UI-success metric.

Distinguish root cause:

  • If js_error events appear within 1 s of modal_open → the modal component threw on mount.
  • If no errors appear → the modal rendered but the close path is broken (close button missing, event handler unregistered, keyboard trap).
  • Compare pre/post deploy: if the pattern only appears after a specific deploy, that deploy introduced the regression.

3.5 Cold-Load Latency Regression (Perf)

Signal: p99 of doubles[0] in cold_load_latency events climbs above 3 s.

Distinguish root cause:

  • If p50 is stable but p99 climbs → tail-latency regression in D1 snapshot reads (query plan degradation or snapshot size explosion).
  • If both p50 and p99 climb → the entire cold-load path has slowed (Worker startup time, D1 regional latency, snapshot decompression overhead).
  • Cross-reference perf_marker events for snapshot_decompress and ydoc_apply markers if instrumented.

3.6 API Failure Spike

Signal: api_error rate rises for a specific endpoint pattern.

Distinguish root cause:

  • statusCode=404 → route removed or renamed.
  • statusCode=429 → client hitting rate limits; check cascade retry logic.
  • statusCode=500 or 503 → server-side error; check Worker logs and D1 health.
  • statusCode=401 or 403 → auth/access regression (should not occur in a no-auth app; indicates a misconfigured middleware).

4. Axiom APL Query Examples

Events land in Axiom. Query via APL (Axiom Processing Language) in the Axiom web UI or CLI. Dataset names are referenced in APL as ['cv_telemetry'] (production) or ['cv_telemetry_dev'] (staging). All field names are the original JSON keys from the emitted event object.

CLI usage: axiom query "<apl>" --start-time -24h (authenticated as careervector).


4.1 Daily Unique Workspace Heartbeats by Tier

['cv_telemetry']
| where type == 'aggregate_heartbeat'
| summarize heartbeats = count(), unique_tokens = dcount(dailyToken) by tier = tierBucket, bin(_time, 1d)
| sort by _time desc

4.2 WebRTC Tier Distribution (Last 24 Hours)

['cv_telemetry']
| where type == 'p2p_tier'
| summarize peer_reports = count(), avg_latency_ms = avg(latencyMs), p99_latency_ms = percentile(latencyMs, 99)
    by transport_tier = tier, turn_label

4.3 WebRTC Tier Distribution Shift (Regression Alert)

['cv_telemetry']
| where type == 'p2p_tier'
| summarize count() by transport_tier = tier, bin(_time, 1h)
| sort by _time desc

Compare T1 count as a fraction of total per hour. A drop from baseline of more than 10 percentage points is a regression trigger.


4.4 JS Error Rate by Message (Last 48 Hours)

['cv_telemetry']
| where type == 'js_error'
| where _time > ago(48h)
| summarize occurrences = count(), affected_workspaces = dcount(workspaceId) by errorMsg
| sort by occurrences desc
| limit 20

4.5 Op Applied Rate and Outcome Distribution

['cv_telemetry']
| where type == 'op_applied'
| where _time > ago(3d)
| summarize total_ops = count(), avg_latency_ms = avg(latencyMs), p99_latency_ms = percentile(latencyMs, 99)
    by hour = bin(_time, 1h), op_type = opType, outcome
| sort by hour desc

4.6 Cold-Load Latency Percentiles (Perf Regression Check)

['cv_telemetry']
| where type == 'cold_load_latency'
| where _time > ago(14d)
| summarize cold_loads = count(), avg_ms = avg(latencyMs),
    p50_ms = percentile(latencyMs, 50), p95_ms = percentile(latencyMs, 95), p99_ms = percentile(latencyMs, 99)
    by day = bin(_time, 1d)
| sort by day desc

4.7 Modal Open Without Close (Stuck Modal Detection)

['cv_telemetry']
| where type in ('modal_open', 'modal_close')
| where _time > ago(24h)
| summarize opens = countif(type == 'modal_open'), closes = countif(type == 'modal_close') by modalId
| extend unclosed = opens - closes
| where unclosed > 5
| sort by unclosed desc

4.8 API Error Rate by Endpoint

['cv_telemetry']
| where type == 'api_error'
| where _time > ago(24h)
| summarize error_count = count(), affected_workspaces = dcount(workspaceId) by endpoint, statusCode
| sort by error_count desc
| limit 30

4.9 P2P Byte Rate by Workspace (Anomaly Detection)

['cv_telemetry']
| where type == 'p2p_tier'
| where _time > ago(1h)
| summarize avg_inbound_bps = avg(byteRateInbound), avg_outbound_bps = avg(byteRateOutbound),
    peak_inbound_bps = max(byteRateInbound)
    by workspace_id = workspaceId
| sort by peak_inbound_bps desc
| limit 20

4.10 Agent Tool Call Success Rate

['cv_telemetry']
| where type == 'agent_tool_call'
| where _time > ago(7d)
| summarize total_calls = count(), avg_latency_ms = avg(latencyMs) by tool_name = toolName
| sort by total_calls desc

5. Privacy Contract

What each tier captures and does NOT capture

Green Yellow Red
Workspace ID Never — only a daily SHA-256 hash that cannot be reversed Yes — workspace_id attached to all events Yes
Peer ID Never Yes — peer_id attached to all events Yes
Op types Never Yes — op type string, not content Yes
Error messages Never Yes — error type + message, no stack trace Yes
Navigation Never Yes — route pattern Yes
Modal IDs Never Yes Yes
API endpoints Never Yes — pattern only (no query params or body) Yes
Keystroke key names Never Never Non-printable only (Enter, ArrowUp, etc.). Printable chars → 'char'
Mouse coordinates Never Never Never — element type + test ID only
Clipboard contents Never Never Never — action type only (read/write)
CV/CL document content Never Never Never
Job titles / company names Never Never Never
Personal information Never Never Never
Workspace content of any kind Never Never Never
Screenshot / DOM serialization Never Never Only on explicit error capture (dom_snapshot), compressed, no plaintext

At GA, all workspaces default to green. The green bundle physically cannot emit support or verbose events — the code that would do so is in separate Vite chunks that are never loaded for green workspaces. This is the primary defense layer (defense-in-depth #1): absent code cannot leak data.

Since telemetry is now browser-direct to Axiom (no server proxy), there is no server-side enforcement layer for yellow/red rejection. The defense is the bundle split: a green workspace never loads the yellow or red module, so those events never originate. The scoped ingest tokens prevent cross-dataset pollution (each token only writes to its own dataset).

Yellow and red require explicit opt-in. Red is further gated behind a UI interaction: 7 consecutive taps on the footer version string within 3 seconds opens a confirmation prompt. On confirmation, settings.tier is updated to 'red' via the Yjs Y.Doc, which triggers the red bundle to load on the next workspace navigation. The footer badge turns red while active.

Red tier = internal development only. It must never be the default for production workspaces. The DEFAULT_WORKSPACE_TIER wrangler var controls the default on workspace creation.


6. Adding New Events

Recipe

  1. Decide the tier. Green for aggregate-only signals that carry no workspace linkage. Yellow for anything that ties to a workspace ID and supports bug investigation. Red for session-replay-grade signals that are internal-dev-only.

  2. Pick a unique event type name. Snake_case, descriptive, no abbreviations that would require a lookup. Check this catalog — the name must not conflict with an existing event.

  3. Document the blob/double layout. Map your fields to blobs[0..4] and doubles[0..2] following the convention in §1. Assign the type name to blobs[0] and the workspace/token identifier to blobs[1]. Choose indexes[0] (the AE dimension key) — for most new events this should be the event type string.

  4. Add the emitter function to the appropriate tier module (green.ts, yellow.ts, or red.ts). Yellow and red are supersets: add the function to yellow if yellow is the minimum tier, and also add it to red.ts (which replicates yellow's support event stream for self-containment).

  5. Add the server-side normalizer case in /api/telemetry/event/+server.ts. Extend the field extraction logic in the for (const ev of events) loop to map your new event's fields to primary and secondary double slots correctly. If your event introduces a new blob beyond blobs[3], extend the blobs array construction for that event type.

  6. Update this catalog. Add the new event to the appropriate tier section (§2.1, 2.2, or 2.3). Include: tier required, emitter function name, when fired, full slot table, and a bug pattern entry if the event is intended for regression detection.

  7. Write a query. Add a representative SQL query to §4 that demonstrates how to use the new event for its intended purpose.

Naming constraints

  • Event type names are permanent once in production — they appear in Analytics Engine data that persists for 30 days. Do not rename events; add a new one and mark the old one deprecated if a rename is needed.
  • Blob string values are truncated at the server: blobs[3] at 200 chars, endpoint patterns at 120 chars, op types at 80 chars, modal IDs at 80 chars. Design your values to be meaningful within these bounds.
  • Do not add more than 5 blob slots or 3 double slots without updating the AE data point shape comment in +server.ts.

Tier selection checklist

Before choosing a tier, answer these questions:

Question If yes →
Does the event carry workspace_id? Yellow minimum
Does the event capture user input (keystrokes, mouse, clipboard)? Red only
Does the event require sub-second flush cadence? Red only
Is this event intended for GA end users without explicit opt-in? Green only — and strip all workspace/session identifiers
Does the event help answer "what did the user do before this bug"? Yellow or red
Is the event for internal protocol debugging only? Red
Source: wiki/content/architecture/ANALYTICS-EVENT-CATALOG.md