PostHog Implementation — Implementation Plan
Status: done
Implements spec.md. Closes the divergence noted in Known gaps.
Goal
Migrate from Umami to PostHog Cloud in a single PR — replace client + server analytics, add session replay, no parallel run.
Status
- [x] 1. Add PostHog deps + env config
- [x] 2. Reverse proxy via Next.js rewrites
- [x] 3. PostHog client provider in
providers.tsx - [x] 4. Session replay route exclusion for
/checkout/* - [x] 5. Swap umami → posthog in client
trackEvent+ remove Umami script tag - [x] 6. Identification + reset (signup, login, app init, logout)
- [x] 7. Server-side
posthog.tsmodule with parity signatures - [x] 8. Switch analytics barrel from
./umamito./posthog - [x] 9. Delete Umami remnants (server module, dep, tests)
- [x] 10. Verification + first PostHog acquisition funnel
- [x] 11. Spec follow-up (close Known gaps)
Driving citations
- Spec Scope — what's in/out for v1
- Spec Identification —
user_idonly, reset on logout - Spec Session replay — defaults +
/checkout/*exclusion - Spec Reverse proxy —
/ingest/*rewrites
Work breakdown
1. Add PostHog deps + env config
- Files touched:
apps/web/package.json,apps/api/package.json,.env,.env.test,apps/api/scripts/lib/load-env.ts(verify), rootpnpm-lock.yaml - Install
posthog-jsinapps/web,posthog-nodeinapps/api. Uninstall@umami/nodefromapps/api. - Add env var placeholders:
NEXT_PUBLIC_POSTHOG_KEY,NEXT_PUBLIC_POSTHOG_HOST(web client) andPOSTHOG_API_KEY,POSTHOG_HOST(server). - Spec coverage: Scope — In
2. Reverse proxy via Next.js rewrites
- Files touched:
apps/web/next.config.js(or.ts) - Add async
rewrites():/ingest/static/:path*→https://us-assets.i.posthog.com/static/:path*,/ingest/:path*→https://us.i.posthog.com/:path*. SetskipTrailingSlashRedirect: trueif needed for PostHog routing. - Add a smoke test that the rewrites are present in the resolved Next config.
- Spec coverage: Reverse proxy
3. PostHog client provider in providers.tsx
- Files touched:
apps/web/src/app/providers.tsx, possibly newapps/web/src/lib/posthog-client.ts - Initialize
posthog-jswithapi_host: '/ingest',ui_host: 'https://us.posthog.com',person_profiles: 'identified_only',session_recording: { maskAllInputs: true },capture_pageview: true,capture_pageleave: true. - NODE_ENV gate: skip init when not production (mirrors
apps/api/src/lib/analytics/umami.ts:33). - Wrap children in PostHog's
<PostHogProvider>. - Spec coverage: Scope — In, Session replay
4. Session replay route exclusion for /checkout/*
- Files touched: new
apps/web/src/lib/posthog-route-guard.ts(or hook), wired intoproviders.tsx - On pathname change, call
posthog.stopSessionRecording()when path matches/checkout, otherwiseposthog.startSessionRecording(). - Test:
usePathnamemock + assertion on the start/stop calls. - Spec coverage: Session replay
5. Swap umami → posthog in client trackEvent + remove Umami script tag
- Files touched:
apps/web/src/lib/tracking.ts,apps/web/src/lib/tracking.test.ts,apps/web/src/app/layout.tsx - Replace the
window.umami.track(eventName, eventData)branch intrackEvent()withposthog.capture(eventName, eventData). Drop thewindow.umamiglobal declaration. - Remove
<Script src="https://cloud.umami.is/script.js" ...>fromlayout.tsx. - Update tests to assert
posthog.captureis called with the same shape Umami previously received. - Meta Pixel (
fbq) and Google Ads (gtag) branches untouched. - Spec coverage: Scope — In
6. Identification + reset (signup, login, app init, logout)
- Files touched:
apps/web/src/lib/tracking.ts(addidentifycalls insidetrackSignupCompletedandtrackLogin), wherever app init reads the auth session (likelyproviders.tsxor a session hook), and the logout handler (likelyapps/web/src/app/logout/) - Call
posthog.identify(userId)with no person properties on signup, login, and on app init whensession.user.idis present. - Call
posthog.reset()in the logout flow before redirect. - Tests cover identify-on-signup, identify-on-login, reset-on-logout.
- Spec coverage: Identification
7. Server-side posthog.ts module with parity signatures
- Files touched: new
apps/api/src/lib/analytics/posthog.ts, newapps/api/src/lib/analytics/posthog.test.ts - Re-implement these exact exports with identical signatures to
umami.ts:trackServerEvent,trackPaymentCompleted,trackSubscriptionCreated,trackSubscriptionCancelled. - Use
posthog-nodelazy-init pattern (mirrors currentgetClient()inumami.ts:15). NODE_ENV gate. - Tests are a port of
umami.test.tsto the new module. - Spec coverage: Scope — In
8. Switch analytics barrel from ./umami to ./posthog
- Files touched:
apps/api/src/lib/analytics/index.ts - Change
export { ... } from './umami'toexport { ... } from './posthog'. - Server callers (
PaymentIntentHandler.ts,SubscriptionCreateHandler.ts,StripeWebhookService.ts) require no changes — they import from the barrel. - Verify with typecheck:
pnpm --filter @mg/api typecheck:quick. - Spec coverage: Scope — In
9. Delete Umami remnants
- Files touched: delete
apps/api/src/lib/analytics/umami.ts,apps/api/src/lib/analytics/umami.test.ts. Verify@umami/noderemoved fromapps/api/package.jsonandpnpm-lock.yaml. Grep for any straggler references (@umami,window.umami,cloud.umami.is,UMAMI_) and remove. - Confirm
pnpm installis clean and typecheck passes for both@mg/apiand@mg/web. - Spec coverage: Scope — In
10. Verification + first PostHog acquisition funnel
- No code changes. Smoke test against a Vercel preview deployment with PostHog env vars set.
- Verify: pageviews fire under reverse-proxied
/ingest, custom events fire (triggerlocation-landing-viewed,hourly-studio-clicked,purchase-conversionmanually), identify fires on signup/login, reset fires on logout, session replay records on marketing pages and is suppressed on/checkout/*. - In PostHog UI, build saved funnel:
location-landing-viewed→hourly-studio-clicked→hourly-booked→purchase-conversion, broken down byutm_source. - Spec coverage: motivating question in Overview
11. Spec follow-up
- Files touched:
docs/features/posthog/spec.md,docs/features/README.md - Replace the Known gaps stub with a "Status: shipped in PR #NNN on YYYY-MM-DD" line.
- Add a File map section to the spec listing the canonical code paths now that they exist (
apps/web/src/lib/posthog-client.ts,apps/api/src/lib/analytics/posthog.ts, etc.). - Update
docs/features/README.mdrow to mark plan complete:[done](./plan.md).