Skip to content

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.ts module with parity signatures
  • [x] 8. Switch analytics barrel from ./umami to ./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

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), root pnpm-lock.yaml
  • Install posthog-js in apps/web, posthog-node in apps/api. Uninstall @umami/node from apps/api.
  • Add env var placeholders: NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST (web client) and POSTHOG_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*. Set skipTrailingSlashRedirect: true if 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 new apps/web/src/lib/posthog-client.ts
  • Initialize posthog-js with api_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 into providers.tsx
  • On pathname change, call posthog.stopSessionRecording() when path matches /checkout, otherwise posthog.startSessionRecording().
  • Test: usePathname mock + 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 in trackEvent() with posthog.capture(eventName, eventData). Drop the window.umami global declaration.
  • Remove <Script src="https://cloud.umami.is/script.js" ...> from layout.tsx.
  • Update tests to assert posthog.capture is 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 (add identify calls inside trackSignupCompleted and trackLogin), wherever app init reads the auth session (likely providers.tsx or a session hook), and the logout handler (likely apps/web/src/app/logout/)
  • Call posthog.identify(userId) with no person properties on signup, login, and on app init when session.user.id is 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, new apps/api/src/lib/analytics/posthog.test.ts
  • Re-implement these exact exports with identical signatures to umami.ts: trackServerEvent, trackPaymentCompleted, trackSubscriptionCreated, trackSubscriptionCancelled.
  • Use posthog-node lazy-init pattern (mirrors current getClient() in umami.ts:15). NODE_ENV gate.
  • Tests are a port of umami.test.ts to 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' to export { ... } 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/node removed from apps/api/package.json and pnpm-lock.yaml. Grep for any straggler references (@umami, window.umami, cloud.umami.is, UMAMI_) and remove.
  • Confirm pnpm install is clean and typecheck passes for both @mg/api and @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 (trigger location-landing-viewed, hourly-studio-clicked, purchase-conversion manually), 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-viewedhourly-studio-clickedhourly-bookedpurchase-conversion, broken down by utm_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.md row to mark plan complete: [done](./plan.md).