V1 Web PWA · Surface walkthrough

Shell: Telemetry and analytics

A per-surface walkthrough of the V1 Web PWA: layout, states, interactions, data, and cross-references.

unspecified
9sections5 minread

On this page

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 scopingOshunDomain union: oshun | tara | veritas | nyx | arete | nisaba | metis | yemaya; events tag domain via the track(name, payload, { domain }) option
  • Platform scopingOshunPlatform: ios | android | web | pwa | server
  • Shell-tab scopingOshunShellTab: home | explore | activity | library | profile
  • Route-kind scopingOshunRouteKind: shell | domain | utility
  • ValidationvalidatePayload 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.tstrackShellStartupMetric, trackRouteTransitionMetric; cold/warm shell entry; per-tab view event
  • studioWorkspaceMountTelemetry.tsemitStudioWorkspaceMount / 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:

  • stdoutSinkconsole.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 CustomEventpublicAuthFunnelTelemetry 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

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 eventsOshunEventPayloadMap 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.tsinitializeCrashReporting, 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); 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 secretspageUrl 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#

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