Skip to content

feat(web): add PostHog analytics - #114

Merged
StanGirard merged 2 commits into
mainfrom
conductor/posthog-captuto
Oct 15, 2026
Merged

StanGirard merged 2 commits into
mainfrom
conductor/posthog-captuto

Conversation

@StanGirard

@StanGirard StanGirard commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds PostHog product analytics to the Next.js web app in apps/web (the captuto.com site). PostHog was not set up anywhere in the repo before this.

  • SDK: posthog-js is started from a client component (components/providers/PostHogAnalytics.tsx) in the root layout. The setup code is in lib/analytics/posthog.ts.
  • Pageviews: capture_pageview: 'history_change', so App Router client navigations are captured as $pageview, not just full page loads. Autocapture and the other SDK defaults are unchanged.
  • Identify / reset: the provider subscribes to Supabase onAuthStateChange:
    • INITIAL_SESSION, SIGNED_IN and USER_UPDATED → posthog.identify(user.id, { email, name }). name comes from user_metadata.full_name or name and is only sent when present.
    • SIGNED_OUT → posthog.reset(). This covers every sign-out path (sidebar, header, password reset, account deletion), because they all go through the shared browser Supabase client.
  • signed_up: captured on /signup after supabase.auth.signUp succeeds, right after identifying the new user. It is only sent for accounts that were just created:
    • Skipped when Supabase returns no identities, which means the email is already confirmed.
    • Skipped when the confirmation email was sent 60s or more after the account was created, which means a resend for an existing unconfirmed account.
  • Reverse proxy: Next.js rewrites send /ingest/static/* and /ingest/array/* to https://us-assets.i.posthog.com and /ingest/* to https://us.i.posthog.com. ui_host is https://us.posthog.com. skipTrailingSlashRedirect: true is set because PostHog's API paths use trailing slashes. This setting applies to the whole app, so URLs with a trailing slash are no longer redirected.

Env vars

Name Required Default
NEXT_PUBLIC_POSTHOG_KEY No In production builds (NODE_ENV=production), falls back to the public "Captuto" project key. In dev and test, nothing is sent unless this is set.
NEXT_PUBLIC_POSTHOG_HOST No /ingest (the same-origin proxy above)

Both are documented in apps/web/.env.example. No secrets are added. The fallback is the public client key only.

Verification

  • pnpm install --frozen-lockfile: pass
  • pnpm lint: pass, no warnings
  • pnpm type-check: pass
  • pnpm test:run: pass (web: 405 tests, extension: 71, skills: 7). New tests are in apps/web/lib/analytics/posthog.test.ts.
  • next build (with placeholder Supabase env): pass
  • Checked by hand with agent-browser against ./scripts/dev-start.sh --local, with NEXT_PUBLIC_POSTHOG_KEY set and PostHog's event endpoints mocked:
    • Initial $pageview, plus a $pageview on the client-side navigation from / to /login. Config, flags and surveys requests went through /ingest.
    • Sign-up sent $identify and signed_up. The PostHog distinct_id became the Supabase user id. Re-checked after the dedupe fix.
    • Sign-out reset to a new anonymous distinct_id. Sign-in sent $identify. Reloading kept the identified state without sending another $identify.
  • With NEXT_PUBLIC_POSTHOG_KEY unset in local dev, there were no /ingest requests and no PostHog cookie.
  • Headless Chrome is dropped by PostHog's built-in bot filter (it checks the HeadlessChrome user agent and navigator.webdriver), so the browser checks used a desktop Chrome user agent with automation flags turned off.

CI

  • Latest commit: 36ac5cf
  • Status: all checks passed (Lint, Type-Check & Test; Validate Supabase Migrations; Build Extension; Build Desktop (macOS)). Supabase Preview was skipped.

Review Gate

  • review-code-dev (deep, frontend): no P0, P1 or P2 findings. One P3 was fixed: signed_up could fire twice when the same unconfirmed email signed up again.

Risk

  • skipTrailingSlashRedirect applies to the whole app. Pages, middleware auth checks and security headers still apply to URLs with a trailing slash.
  • posthog-js is bundled into the shared client JavaScript, so every page loads it.

Human Review Notes

  • User emails are now sent to PostHog as person properties. Deleting an account does not delete the PostHog person.
  • There is no cookie consent banner. PostHog sets a first-party ph_* cookie for every visitor.
  • Session replay and other features turned on in PostHog's remote config are controlled in the PostHog project settings, not in this code.

🤖 Generated with Claude Code

Capture pageviews (including App Router navigations), identify Supabase
users, reset on sign-out, and send a signed_up event on account creation.
Events go through a same-origin /ingest rewrite to PostHog US cloud.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@supabase

supabase Bot commented Oct 15, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ehspsyvpriznjojowqfr because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Supabase signUp also succeeds for an existing unconfirmed email by resending
the confirmation, which returned the real user and fired a second signed_up.
Skip users whose confirmation was sent a minute or more after creation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@StanGirard
StanGirard merged commit 35e1b23 into main Oct 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant