# Shell: Telemetry and analytics

Source: `apps/oshun/web/src/analytics/` (per-surface telemetry helpers,
including `shellNavigationTelemetry.ts`, `studioWorkspaceMountTelemetry.ts`,
`publicAuthFunnelTelemetry.ts`, `recommendationTelemetry.ts`,
`homeContinuationTelemetry.ts`, `librarySaveTelemetry.ts`,
`searchResultTelemetry.ts`, `activityReentryTelemetry.ts`,
`domainLaunchTelemetry.ts`, `nisabaStudyTelemetry.ts`, and the studio-\*
family),
`apps/oshun/web/src/observability/{crash-reporting,error-monitoring,performance-telemetry}.ts`,
`apps/oshun/web/src/components/CookieConsentBanner.tsx`,
`libs/oshun/analytics/src/types.ts` (`OshunEventPayloadMap`),
`libs/oshun/analytics/src/client.ts`,
`libs/oshun/analytics/src/buffered-sink.ts`

Three things converge in this shell: **event taxonomy** (the typed event names
each surface emits), **transports** (console shim today, batched BFF POST for a
few surfaces, more transports queued), and **observability** (crashes, unhandled
rejections, web vitals). Cookie consent gates anything not strictly essential.
Walk this when a new surface adds telemetry or when the production transport
finally lands.

## Event taxonomy

`@oshun/analytics` (`libs/oshun/analytics/src/types.ts`) declares
`OshunEventPayloadMap` — a typed event-name → payload map. Every public surface
adds its events here before the per-surface helper compiles.

- [ ] **Event names** — snake_case, verb-final (`route_transition_completed`,
      `domain_launch_requested`, `studio_workspace_mounted`,
      `public_auth_funnel_viewed`, `recommendation_impression`)
- [ ] **Domain scoping** — `OshunDomain` union:
      `oshun | tara | veritas | nyx     | arete | nisaba | metis | yemaya`;
      events tag domain via the `track(name, payload, { domain })` option
- [ ] **Platform scoping** — `OshunPlatform`:
      `ios | android | web | pwa |     server`
- [ ] **Shell-tab scoping** — `OshunShellTab`:
      `home | explore | activity |     library | profile`
- [ ] **Route-kind scoping** — `OshunRouteKind`: `shell | domain | utility`
- [ ] **Validation** — `validatePayload` runs before each emit so payload drift
      fails fast in tests

## Per-surface telemetry helpers

Every surface owns a thin helper in `apps/oshun/web/src/analytics/`. Helpers are
responsible for normalizing payloads, attaching context, and routing to sinks.

- [ ] **`shellNavigationTelemetry.ts`** — `trackShellStartupMetric`,
      `trackRouteTransitionMetric`; cold/warm shell entry; per-tab view event
- [ ] **`studioWorkspaceMountTelemetry.ts`** —
      `emitStudioWorkspaceMount /     Unmount / Interaction`; only studio
      workspaces; BFF batch sink
- [ ] **`publicAuthFunnelTelemetry.ts`** — five-step funnel (viewed →
      cta_clicked → submitted → completed → failed); failure codes:
      `validation | network | provider_error | unknown`
- [ ] **`recommendationTelemetry.ts`** — impression / tap / feedback;
      cross-domain only; position + visibleCount included
- [ ] **`homeContinuationTelemetry.ts`** — home surface continuation cards (hero
      / daypart / per-domain)
- [ ] **`librarySaveTelemetry.ts`** — item save / unsave with source + surface
- [ ] **`searchResultTelemetry.ts`** — search executed / zero-results / result
      opened
- [ ] **`activityReentryTelemetry.ts`** — activity timeline re-entry into a
      domain
- [ ] **`domainLaunchTelemetry.ts`** — domain launch requested / completed with
      latency
- [ ] **`nisabaStudyTelemetry.ts`** — Nisaba reading + study events
- [ ] **`studio*Telemetry.ts` (40+)** — studio workspace coverage (color system,
      design language, file/media ingestion, etc.); each maps to a single
      workspace surface

## Transports

Two sinks operate today, with a third reserved:

- [ ] **`stdoutSink`** — `console.info('[oshun-analytics]', name, payload)`;
      always-on, source-of-truth in dev and test
- [ ] **`BffTelemetrySink`** (studio workspace mount only) — in-memory queue,
      flushed every 2 s, on `pagehide`, or once the queue hits `MAX_BATCH_SIZE`
      (25); POSTs to `/v1/studio/telemetry/workspace-events` with
      `keepalive: true`; swallows failures (no re-queue)
- [ ] **`BufferedAnalyticsSink`** (`libs/oshun/analytics/src/buffered-sink.ts`)
      — the canonical batched sink with retry + backoff; not yet wired into the
      web app per the comment "Temporary local sink until production telemetry
      transport is connected" in `shellNavigationTelemetry.ts` and
      `recommendationTelemetry.ts`
- [ ] **`oshun-analytics:event` `CustomEvent`** — `publicAuthFunnelTelemetry`
      dispatches a `window` event in parallel with the stdout shim, so a future
      listener (e.g., a dev-tools panel or a forwarder bootstrapped in
      `OshunProviders`) can subscribe without changing helpers

## Cookie-consent gating

`CookieConsentBanner.tsx` exposes `useCookieConsent()` and persists a
`StoredConsent` blob keyed by region (`global | eu | uk | california | cn`).
`DEFAULT_PREFERENCES` is
`{ essential: true, analytics: false, functional: false, marketing: false }`.

- [ ] **Essential telemetry** (crash reports, network failures, auth-flow
      counters) — fires regardless of consent; never includes optional
      personalization signals
- [ ] **Analytics events** — `OshunEventPayloadMap` events fire only after
      `preferences.analytics === true`
- [ ] **Marketing events** — only after `preferences.marketing === true`;
      currently not wired
- [ ] **Region default** — EU/UK/CN/California start essential-only; "global"
      still requires opt-in for analytics
- [ ] **Re-open consent** — Profile → Privacy must surface the same banner so
      users can change later (verify the deep link)

## Observability sinks

`apps/oshun/web/src/observability/` — distinct from the analytics taxonomy
because crashes and vitals fire on a different lifecycle (no consent gate for
crash-level failures; web vitals respect analytics consent).

- [ ] **`crash-reporting.ts`** — `initializeCrashReporting`, `captureException`,
      `captureDiagnosticsSnapshot`; POSTs to `/v1/web/crashes` and
      `/v1/web/diagnostics` with `keepalive: true`; hooks `unhandledrejection`;
      periodic memory snapshot every 120 s
      (`jsHeapSizeLimit / totalJsHeapSize / usedJsHeapSize`)
- [ ] **`error-monitoring.ts`** — broader error capture
      (`global_handler |     unhandled_promise | error_boundary | manual`);
      attaches PII-scrubbed message, source map references, `isStandalone`,
      `connectionType`, `commitHash`, `appVersion`; web vitals payload
      (`lcpMs / fidMs / cls /     ttfbMs / inpMs`) emitted per route
- [ ] **`performance-telemetry.ts`** — startup timing
      (`startupMs / fcpMs /     domInteractiveMs`), per-route transition timing,
      periodic diagnostics snapshots; sample interval 120 s

## SR / live-region considerations

- [ ] **No screen-reader announcement** on telemetry events — they are silent
      diagnostics
- [ ] **`role="status"` is reserved** for toast and notifications-center changes
      (see [`05-notifications.md`](./05-notifications.md)); telemetry must not
      steal that channel
- [ ] **`role="alert"` is reserved** for error toasts and the Trust & Safety
      flag error inline message; analytics failures must stay invisible

## Forbidden patterns

- [ ] **No raw PII** in event payloads — no email, phone, full name, free-text
      content. The Trust & Safety flag rationale is _never_ re-emitted as
      telemetry; only the category, failure code, and target kind
- [ ] **No other user's identifiers** — never put `targetUserId` in an event
      payload when the target is a different user. Use a hashed or
      relationship-scoped identifier
- [ ] **No URL query strings with secrets** — `pageUrl` strips query params in
      the error-monitoring payload; per-surface helpers must match (see
      `normalizeSameOriginPath` in `publicAuthFunnelTelemetry.ts`)
- [ ] **No third-party tags** — there is no GA / Segment / Amplitude tag in the
      web app; all events flow through `@oshun/analytics`. If a domain proposes
      one, escalate before merging
- [ ] **No silent throws** — analytics sinks `console.info` then return; they
      must never break a render path or invalidate a query

## Cross-references

- [04-auth-session.md](./04-auth-session.md) — auth funnel + cookie-consent
- [07-trust-safety.md](./07-trust-safety.md) — flag pipeline telemetry
- Taxonomy:
  [`libs/oshun/analytics/src/types.ts`](../../libs/oshun/analytics/src/types.ts)
- Architecture: [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#observability-evaluation-and-release-gates)

## Open questions / known gaps

- [ ] Production transport for `shellNavigationTelemetry` and
      `recommendationTelemetry` — both still emit only to `stdoutSink`
- [ ] BFF ingest endpoints beyond `/v1/studio/telemetry/workspace-events`,
      `/v1/web/crashes`, and `/v1/web/diagnostics` not yet shipped; the
      customer-surface taxonomy has no canonical sink in V1
- [ ] No documented retention / sampling policy for the BFF sinks; the buffered
      sink supports it but the dev/local POST handlers do not
- [ ] `BufferedAnalyticsSink` is implemented in the lib but never instantiated
      from the web app — replace the per-surface ad-hoc stdout sinks
- [ ] Marketing-cookie wiring is absent; if marketing events are ever needed the
      consent UI is ready but the helper layer is not
