Analytics & Observability (Canonical)¶
This is the single normative rule for PostHog usage in the Open Learning Hub monorepo: product analytics, error tracking, and OpenTelemetry log forwarding. Error-handling policy is in 090-error-handling.mdc; the logger emit contract and log-sink seam are in 091-structured-logging.mdc.
Platform¶
- PostHog is the single observability platform (US Cloud:
https://us.i.posthog.com). - There is no Sentry. Do not add
@sentry/*packages orsentry.*.config.ts. - All wiring lives in the shared package
@open-learning-hub/observability(AGENTS.md). Apps inject config; the package never readsprocess.env.
Package boundary¶
@open-learning-hub/observability/config— types + constants (Edge-safe).@open-learning-hub/observability/client—"use client":PostHogProvider,useAnalytics,useConsent,captureBrowserException. Browser only.@open-learning-hub/observability/server—createServerAnalytics(capture/captureException/flush). Node only; never import from a'use client'module.@open-learning-hub/observability/otel—createOtelLogBridge. Node only, imported only frominstrumentation.ts.@open-learning-hub/observability/log-sink— Edge-safe seam consumed bysrc/lib/log.ts(no OTel/Node imports).
Consent + privacy defaults (required)¶
- Ship a consent gate. PostHog starts in
persistence: 'memory'with capture opted-out until the user accepts; consent is versioned inlocalStorage(olh_ph_consent:v1) and Do-Not-Track is respected. - Browser init defaults:
autocapture: false,disable_session_recording: true,person_profiles: 'identified_only',respect_dnt: true,cross_subdomain_cookie: false,disable_surveys: true. - The consent banner renders only while consent is
unsetand only after the client resolves the stored choice (no hydration flash). - The banner is gated on analytics being active (
isObservabilityActive): with no PostHog key, analytics is inert and the banner never mounts. This keeps the fixed banner from intercepting pointer events in e2e/dev where no key is set.
No PII¶
- Never send emails, names, or other PII as event/exception properties or log
attributes. Pass ids/slugs (e.g.
userId,tenantId,courseSlug). - Server logs forwarded via OTel reuse the redacting
src/lib/log.tsoutput, so redaction happens before anything leaves the process.
Configuration (env, injected by apps)¶
- Server:
POSTHOG_PROJECT_KEY,POSTHOG_HOST(default US Cloud). - Browser:
NEXT_PUBLIC_POSTHOG_KEY,NEXT_PUBLIC_POSTHOG_HOST(default US Cloud). - Browser transport must use same-origin
/ingest/*rewrites innext.config.ts(@open-learning-hub/platform-config/posthog-proxy) so CSP can stayscript-src 'self'/connect-src 'self'. - All optional: when the key is absent (or
NODE_ENV=test), every factory degrades to a no-op. The same PostHog project token (phc_...) powersposthog-js,posthog-node, and the OTLPAuthorization: Bearerheader. - App adapters live in
src/lib/analytics/(config.ts,server.ts,events.ts).
Flush pattern¶
- Server events/logs are batched. In route handlers and server actions, flush
with
after()fromnext/servercallingflushObservability()so data is delivered before the serverless function freezes.
Event taxonomy (initial)¶
Event names are stable and snake_case. Extend the per-app events.ts maps.
- LMS (
LMS_EVENTS):sign_in,sign_up,course_viewed,enrolled,quiz_submitted,csp_violation. - CMS (
CMS_EVENTS):page_created,page_published,widget_added,import_run,csp_violation.
Error tracking¶
- Client:
error.tsx/global-error.tsxcallcaptureBrowserExceptionin an effect.global-error.tsxrenders outside providers/i18n (plain copy). - Server: capture via the
serverAnalytics().captureException(...)seam (e.g.reportCmsError). No-op when PostHog is unconfigured.
Out of scope (follow-ups)¶
- Session replay, dashboards/funnels config, feature flags, and A/B testing.
- Migrating CLI/seed/migrate script logging.