Skip to content

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-js initialized in apps/web/src/app/providers.tsx (or layout — TBD)
  • All existing trackEvent(...) calls in apps/web/src/lib/tracking.ts routed to PostHog instead of Umami; Meta Pixel + Google Ads branches unchanged
  • Server-side parity: new apps/api/src/lib/analytics/posthog.ts with the same exported function signatures as the current umami.ts (trackPaymentCompleted, trackSubscriptionCreated, trackSubscriptionCancelled, trackServerEvent)
  • Session replay enabled with input masking + payment-route exclusion
  • posthog.identify(userId) bridge on signup-completed and login events to connect anonymous browsing → identified user
  • Single PostHog project for production; events suppressed when NODE_ENV !== 'production' (mirrors current Umami gate at apps/api/src/lib/analytics/umami.ts:33)
  • Removal of: <script src="cloud.umami.is/script.js"> from layout.tsx, apps/api/src/lib/analytics/umami.ts, @umami/node dependency, window.umami typings in tracking.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 with user_id only — no email, name, or location set as person properties.
  • Trigger points: on signup-completed, on login, 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 identify is 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_recording toggled 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.