PostHog Implementation
Replace the existing Umami analytics integration with PostHog Cloud (US, free tier) in a single migration. PostHog becomes the single source of truth for product analytics — pageviews, custom events, funnels, and session replay. Meta Pixel (fbq), Meta Conversions API (server-side, see apps/api/src/lib/meta/), and Google Ads (gtag) conversion fan-out remain untouched; they serve ad-platform attribution and pull their match data directly from the application DB, not from any analytics tool. Sentry continues to own errors and performance. Metabase continues to own SQL-based business reporting on the production DB.
Ad attribution is unaffected by this migration. Meta CAPI (capi-client.ts, hash-identity.ts, fbc.ts) sources hashed email/phone/name from the user record, fbc cookie from _fbc, and the browser↔server dedupe via eventID (already wired in tracking.ts and useCheckoutFlow.tsx). PostHog is never in that path.
The motivating question this spec answers: of users who click an ad, where in the funnel do they bounce, and why? Funnel events are already instrumented (see apps/web/src/lib/tracking.ts). Session replay — the diagnostic layer for "why" — is what Umami can't provide and PostHog can.
Why replace, not layer
Umami today provides pageviews, ~18 named custom events, server-side adblock-proof tracking for payments, and cookieless privacy defaults. PostHog is a strict superset: it provides every Umami capability plus session replay, advanced funnels, and feature flags (the last deferred). Nothing in this codebase reads from Umami's storage — Metabase queries the production DB and Stripe FDW only (analytics/README.md). Keeping both means a permanent fan-out for no benefit. Reversal cost if PostHog disappoints: re-add a script tag and a 125-line file.
Scope
In scope (v1):
posthog-jsinitialized inapps/web/src/app/providers.tsx(or layout — TBD)- All existing
trackEvent(...)calls inapps/web/src/lib/tracking.tsrouted to PostHog instead of Umami; Meta Pixel + Google Ads branches unchanged - Server-side parity: new
apps/api/src/lib/analytics/posthog.tswith the same exported function signatures as the currentumami.ts(trackPaymentCompleted,trackSubscriptionCreated,trackSubscriptionCancelled,trackServerEvent) - Session replay enabled with input masking + payment-route exclusion
posthog.identify(userId)bridge onsignup-completedandloginevents to connect anonymous browsing → identified user- Single PostHog project for production; events suppressed when
NODE_ENV !== 'production'(mirrors current Umami gate atapps/api/src/lib/analytics/umami.ts:33) - Removal of:
<script src="cloud.umami.is/script.js">fromlayout.tsx,apps/api/src/lib/analytics/umami.ts,@umami/nodedependency,window.umamitypings intracking.ts
Out of scope (deferred):
- Feature flags, A/B experiments, surveys (no current use case)
- Self-hosting (free tier covers projected volume)
- Dev/staging PostHog projects (single prod project; dev events suppressed)
- New event taxonomy or renames — keep existing kebab-case event names exactly as-is to avoid dashboard churn
Decisions resolved
| Decision | Choice | Rationale |
|---|---|---|
| Replace vs layer | Replace in single PR | Reversal is git revert; permanent fan-out earns nothing |
| Parallel-run window | None | Detectable failures resolve in minutes; longer windows just defer cleanup |
| Hosting | PostHog Cloud US, free tier | 1M events/mo + 5K replays/mo covers projected volume |
| Event names | Keep existing kebab-case | No dashboard churn, no in-flight events lost |
| Server-side library | posthog-node |
Direct replacement for @umami/node |
| Project isolation | Single prod project; dev events suppressed | Same policy as Umami today |
Identification
posthog.identify(userId)called withuser_idonly — no email, name, or location set as person properties.- Trigger points: on
signup-completed, onlogin, and on app init when there is already an authenticated session. posthog.reset()called on logout to prevent analytics leakage across users on a shared device.- Anon→identified bridge handled automatically by PostHog when
identifyis called on a session that previously had events as anonymous ($anon_distinct_id).
PostHog identity is independent of Meta CAPI, Google Ads, and Sentry user contexts; each maintains its own pipeline.
Session replay
PostHog defaults are the bar. Specifically:
maskAllInputs: true(default) — every form input value is redacted in replays.- Network capture off (default) — no fetch/XHR payloads recorded.
- Console capture off (default) — no console output recorded.
disable_session_recordingtoggled on entry to/checkout/*routes — payment forms shouldn't be replayed even with inputs masked.
No data-private tagging or maskTextSelector rules in v1. If rendered text (emails, names) in replays ever becomes a compliance issue, it's a reactive fix, not a v1 design decision.
Out of scope: privacy compliance debt
Independent of this migration, the existing privacy policy (apps/web/src/app/(marketing)/privacy/page.tsx) has CCPA/CPRA gaps that pre-date PostHog: stated "no-sell, no-share" conflicts with Meta Pixel/CAPI/Google Ads sharing, references a cookie banner that doesn't exist, missing "Do Not Sell or Share" footer link, no GPC signal handling, stale third-party list. These are tracked in a separate work item and do not block this migration — PostHog is first-party product analytics and doesn't broaden the existing sharing surface.
Reverse proxy
PostHog client SDK requests are reverse-proxied through the Metrognome domain to bypass adblockers. Next.js rewrites in apps/web/next.config.js map /ingest/static/* → https://us-assets.i.posthog.com/static/* and /ingest/* → https://us.i.posthog.com/*. The client SDK is configured with api_host: '/ingest' and ui_host: 'https://us.posthog.com'.
File map
| Purpose | Path |
|---|---|
| PostHog JS singleton + init | apps/web/src/lib/posthog-client.ts |
| Client provider + NODE_ENV gate | apps/web/src/app/providers.tsx |
| Session replay route guard | apps/web/src/lib/posthog-route-guard.ts |
| Client event tracking + identify/reset | apps/web/src/lib/tracking.ts |
| Server-side analytics module | apps/api/src/lib/analytics/posthog.ts |
| Analytics barrel | apps/api/src/lib/analytics/index.ts |
| Next.js reverse proxy rewrites | apps/web/next.config.js |
Status
Shipped in PR #672 on 2026-05-02. Live smoke test (PostHog dashboard events, session replay suppression on /checkout/*, funnel creation) requires Vercel preview with NEXT_PUBLIC_POSTHOG_KEY + POSTHOG_API_KEY set — Aaron to verify post-deploy.