Skip to content

fix(analytics): capture posthog events only on the production host - #576

Merged
TheMeinerLP merged 1 commit into
mainfrom
fix/posthog-production-only
Oct 23, 2026
Merged

TheMeinerLP merged 1 commit into
mainfrom
fix/posthog-production-only

Conversation

@TheMeinerLP

Copy link
Copy Markdown
Collaborator

Problem

PostHog (EU project) receives pageviews from non-production origins, for example http://localhost:8787/en/bluemap. The posthog block in nuxt.config.ts ships the SDK on every build, and the consent layer opts in by default. So pnpm dev, pnpm preview, pnpm preview:prod and the CI jobs in seo.yml (Lighthouse, seo:check, cache-check, the concurrency check against localhost:8787) all send real events. The CI jobs are the likely source of most of that traffic.

Design

The gate compares window.location.hostname with the production host. That host comes from the production-only siteUrl runtime config (the $production block), so there is no second copy of the domain, and dev and preview builds have no production host at all.

  • layers/base/plugins/posthog-production-gate.client.ts (dependsOn: ['posthog']): on any other host it calls $clientPosthog.opt_out_capturing(). posthog-js checks the opt-out at capture time, so this covers the router's $pageview, autocapture, pageleave and session recording.
  • layers/consent/utils/consentController.ts: a new analyticsHost port makes applyAnalytics(allowed && analyticsHost). An accepted or stored opt-in never switches capture on off production. Decisions are still stored and the banner still closes.
  • layers/base/utils/analyticsHost.ts: the pure helpers productionHostname and isAnalyticsHost.

Why not the alternatives:

  • posthog.disabled in nuxt-posthog needs a build-time value. The same production bundle is served on localhost:8787, so the host is only known in the browser.
  • clientOptions alone cannot stop the router.afterEach capture or a consent opt-in.

Known limit: the banner still appears on localhost, and a choice made there is stored but has no effect. posthog-js still calls /flags on localhost; that is a feature-flag lookup, not an event.

Tests

  • tests/analytics/analytics-host.spec.ts: the production host matches; localhost, 127.0.0.1, localhost:8787, a workers.dev alias, and an empty or missing host do not; the production hostname is read from the site URL.
  • tests/consent/controller.spec.ts: off the production host, accepting or restoring a stored opt-in never opts PostHog in, and the decision is still stored.
  • Existing consent and analytics specs are unchanged and green.

Locally: pnpm test 190 files / 1786 tests passing; pnpm quality no regression; pnpm typecheck shows only the three pre-existing errors; pnpm build passes.

Verified in a headless browser

Built with NODE_ENV=production pnpm build, served with pnpm preview:prod. Playwright (Chromium) loaded /en/bluemap, clicked a link and a button, then accepted the cookie banner.

  • http://localhost:8787: zero capture requests (no event, batch or snapshot calls). PostHog reports opted out, and still opted out after accepting the banner.
  • Positive control, --host-resolver-rules="MAP onelitefeather.net 127.0.0.1" on http://onelitefeather.net:8787: capture requests are sent, and accepting the banner opts in, so production behaviour is unchanged.

https://claude.ai/code/session_01Px57iMQNzBFqJ4Y9aZf8Dt

PostHog received events from localhost, 127.0.0.1, the 8787 preview and workers.dev previews: the SDK was enabled on every build and the consent layer opted in by default.

Off the production host (siteUrl from the production runtime config, so dev and preview builds have none) a base plugin opts PostHog out, covering pageview, autocapture, pageleave and recording. The consent controller never opts in there, including from a stored choice. Production behaviour is unchanged.

Claude-Session: https://claude.ai/code/session_01Px57iMQNzBFqJ4Y9aZf8Dt
@TheMeinerLP
TheMeinerLP merged commit dfce43e into main Oct 23, 2026
8 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