open-design/apps/web/tests/analytics
lefarcen 88dee44892
feat(analytics): always-on $exception capture with early window hooks (#2521)
PostHog Error tracking was missing the vast majority of real exceptions:

  1. posthog-js's capture_exceptions: true is silenced by opt_out_capturing,
     so every opted-out user vanished from the error feed even though we
     could perfectly safely keep collecting their stacks (the consent
     toggle's user copy gates analytics, not safety telemetry).
  2. posthog-js is dynamically imported only after /api/analytics/config
     resolves AND the user has consented. Errors thrown during the first
     1-2 seconds (React hydration, early effects) had no listener to
     catch them.

Net effect: 14d $exception count was 54 events / 10 users across ~5k DAU,
producing the misleading 99.93% crash-free curve in PostHog's dashboard.

This PR makes exception capture independent of both gates:

  - apps/web/src/analytics/error-tracking.ts (new): own window.error +
    unhandledrejection handlers, in-memory buffer (capped at 50 entries),
    direct fetch to https://<host>/i/v0/e/ with the public phc_ key. Same
    scrub layer as the posthog-js path so file paths still get redacted.
  - apps/web/app/[[...slug]]/client-app.tsx: installErrorHandlers() at
    module-load, before React or any feature code can throw.
  - apps/web/src/analytics/provider.tsx: bootstrapExceptionTracking() in
    the identity useEffect, parallel to getAnalyticsClient() — runs
    regardless of consent state, fetches /api/analytics/config, hands the
    phc_ key + host + distinctId to the error tracker so buffered events
    can flush.
  - apps/web/src/analytics/client.ts: capture_exceptions: false so
    posthog-js stops also emitting $exception (would have produced
    duplicate events server-side); also re-bridges the error-tracking
    context inside the loaded() callback so future events inherit the
    fully-resolved appVersion / sessionId.
  - apps/daemon/src/server.ts + packages/contracts: /api/analytics/config
    now returns key + host even when consent=false. enabled still reflects
    only the analytics consent toggle (posthog-js full autocapture stays
    off when enabled=false), but the always-on error tracker can read key
    directly. Forks without POSTHOG_KEY still get key=null and the whole
    pipeline becomes a no-op — fork-safe by construction.
  - apps/web/src/analytics/scrub.ts: regex fix so packaged-mac paths like
    /Applications/Open Design.app/Contents/Resources/apps/web/... (which
    contain a space) get fully rewritten to app://apps/web/...; previously
    the [^\s] guard stopped at 'Open' and leaked the install dir.

Validation:

  - pnpm --filter @open-design/web typecheck: pass
  - pnpm --filter @open-design/web test: 199 files / 1823 tests pass
    (includes 8 new error-tracking.test.ts cases for buffer cap, hook
    install, scrub, and direct dispatch)
  - pnpm --filter @open-design/daemon test: 250 files / 2971 tests pass
  - pnpm guard: pass

After release/v0.8.0 ships and rolls out, expect the crash-free curve to
drop from the artificial 99.93% to a realistic 95-98% — that's not a
regression, it's the first time we're measuring it.
2026-05-21 13:07:26 +08:00
..
error-tracking.test.ts feat(analytics): always-on $exception capture with early window hooks (#2521) 2026-05-21 13:07:26 +08:00