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 —
OshunDomainunion:oshun | tara | veritas | nyx | arete | nisaba | metis | yemaya; events tag domain via thetrack(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 —
validatePayloadruns 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, onpagehide, or once the queue hitsMAX_BATCH_SIZE(25); POSTs to/v1/studio/telemetry/workspace-eventswithkeepalive: 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" inshellNavigationTelemetry.tsandrecommendationTelemetry.ts -
oshun-analytics:eventCustomEvent—publicAuthFunnelTelemetrydispatches awindowevent in parallel with the stdout shim, so a future listener (e.g., a dev-tools panel or a forwarder bootstrapped inOshunProviders) 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 —
OshunEventPayloadMapevents fire only afterpreferences.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/crashesand/v1/web/diagnosticswithkeepalive: true; hooksunhandledrejection; 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 (see05-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
targetUserIdin an event payload when the target is a different user. Use a hashed or relationship-scoped identifier - No URL query strings with secrets —
pageUrlstrips query params in the error-monitoring payload; per-surface helpers must match (seenormalizeSameOriginPathinpublicAuthFunnelTelemetry.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.infothen return; they must never break a render path or invalidate a query
Cross-references#
- 04-auth-session.md — auth funnel + cookie-consent
- 07-trust-safety.md — flag pipeline telemetry
- Taxonomy:
libs/oshun/analytics/src/types.ts - Architecture:
V1/ARCHITECTURE.md
Open questions / known gaps#
- Production transport for
shellNavigationTelemetryandrecommendationTelemetry— both still emit only tostdoutSink - BFF ingest endpoints beyond
/v1/studio/telemetry/workspace-events,/v1/web/crashes, and/v1/web/diagnosticsnot 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
-
BufferedAnalyticsSinkis 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