feature: add PostHog reverse proxy to bypass ad blockers#145
Conversation
Adds a /ingest proxy route so PostHog events are sent through the app's own domain instead of directly to us.i.posthog.com, preventing ad blocker interference. - Vite dev server proxies /ingest → https://us.i.posthog.com - NestJS controller handles /ingest/* in production via native fetch - Unit and e2e tests cover URL routing, body/header forwarding, and error codes
| envDir: path.resolve(__dirname, '../..'), | ||
| server: { | ||
| port: 5173, | ||
| proxy: { |
There was a problem hiding this comment.
If we have the controller, why don't we proxy to it directly?
There was a problem hiding this comment.
It's one fewer network hop (browser → Vite → PostHog vs browser → Vite → NestJS → PostHog)
…able in production
Without this, setGlobalPrefix('api') mounts the PosthogProxyController at
/api/ingest/* while the PostHog SDK and Vite proxy both send to /ingest/*,
causing POST requests to 404 in production.
Replace regex-style `ingest/(.*)` with named wildcard `ingest/*splat` to be compatible with path-to-regexp v10+ used in NestJS v11.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a30163f. Configure here.
| headers: { | ||
| 'content-type': (req.headers['content-type'] as string) ?? 'application/json', | ||
| }, | ||
| body: hasBody ? JSON.stringify(req.body) : undefined, |
There was a problem hiding this comment.
Body always JSON-stringified regardless of content type
Medium Severity
The proxy always re-encodes the body with JSON.stringify(req.body) but faithfully forwards the original content-type header. When a request arrives with a non-JSON content type (e.g., text/plain, which PostHog SDK uses for sendBeacon payloads), Fastify parses req.body as a raw string. Calling JSON.stringify on a string wraps it in quotes and escapes it, producing a double-encoded payload while the forwarded content-type still says text/plain. PostHog would fail to parse this mismatched request. Forwarding the raw body instead of always re-serializing as JSON would handle all content types correctly.
Reviewed by Cursor Bugbot for commit a30163f. Configure here.


Adds a /ingest proxy route so PostHog events are sent through the app's own domain instead of directly to us.i.posthog.com, preventing ad blocker interference.
Summary
Changes
Checklist
roborev review --branchor/roborev-review-branchin Claude Code (internal)Note
Medium Risk
Adds a new unauthenticated
/ingest/*proxy path and adjusts global API prefixing to exclude it, which can affect routing and introduces an outbound request surface area. Risk is moderate but scoped because the upstream host is fixed viaPOSTHOG_HOSTand behavior is covered by unit/e2e tests.Overview
Adds a new
PosthogProxyModuleexposing/ingest/*that forwards requests toPOSTHOG_HOSTviafetch, passing through method, JSON body (for non-GET/HEAD), andcontent-type, and returning upstream status/body/content-type.Updates production routing to keep
/ingest/*outside the/apiglobal prefix, and configures Vite dev server to proxy/ingestto PostHog. Adds unit tests for URL rewriting/headers/body handling and an e2e suite validating happy-path and upstream error status forwarding.Reviewed by Cursor Bugbot for commit a30163f. Bugbot is set up for automated code reviews on this repo. Configure here.