Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions context/commandments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ commandments:
- "If the app sends a Content-Security-Policy (a meta tag or a response header), extend it for the browser SDK in one change: allow the PostHog hosts, or the reverse proxy, in `script-src` and `connect-src`, and `blob:` in `worker-src` for session replay. A partial change fails silently: the SDK loads, but events or recordings never send"
- "When one codebase ships several platform targets (Flutter, React Native, Kotlin Multiplatform), initialize PostHog for every target the project ships. One init call often covers only some of them"

# Routing the browser SDK through a proxy. Skills that set one up carry the `reverse-proxy` tag.
reverse-proxy:
- "A proxy forwards to the project's own region only: `/static/*` and `/array/*` to the assets host (us-assets.i.posthog.com or eu-assets.i.posthog.com), every other path to the ingestion host (us.i.posthog.com or eu.i.posthog.com). Never mix US and EU hosts"
- "Whenever `api_host` points at a proxy, set `ui_host` to the PostHog app host (https://us.posthog.com or https://eu.posthog.com). Without it the toolbar and the recordings player break"
- "A self-hosted proxy sets the `Host` header to the PostHog host it forwards to, forwards the visitor's IP in `X-Forwarded-For`, passes `Authorization`, every `PostHog-*` header, `Content-Type`, `Content-Encoding`, and `User-Agent` unchanged, allows GET and POST, keeps the query string, and accepts request bodies up to 64 MB for session recordings"
- "Never point `api_host` at a managed proxy before `proxy-get` reports its status as `valid`. Until then every request to it fails"
- "Create a managed proxy only after the user confirmed the subdomain. A proxy belongs to the whole organization and counts against its limit"

# Generic JavaScript rules — every JS skill carries the `javascript` tag.
javascript:
- Remember that source code is available in the node_modules directory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ STEP 3: Install and initialize the PostHog SDK. (Skip if PostHog is already set
- If the project keeps more than one lockfile for the same manifest, such as a second Gemfile lock for another bundle, update each one.
- Follow the framework reference for where and how to initialize.
- For posthog-js, set `defaults` to the preset the framework docs reference shows. The example projects can show an older preset.
- For posthog-js, if the framework reference documents a reverse proxy for this framework, set it up as part of this change. Otherwise, suggest one in your final message: a reverse proxy keeps ad blockers from dropping events (https://posthog.com/docs/advanced/proxy.md).
- For posthog-js, if the framework reference documents a reverse proxy for this framework, set it up as part of this change. Otherwise, suggest one in your final message: a reverse proxy keeps ad blockers from dropping events, and the `instrument-reverse-proxy` skill sets one up (https://posthog.com/docs/advanced/proxy.md).

STEP 4: Enable exception autocapture.
- Follow the platform reference to enable exception autocapture. This automatically captures unhandled exceptions without additional code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ STEP 4: Initialize PostHog.
- Follow the framework reference for where and how to initialize. This varies significantly by framework (e.g., instrumentation-client.ts for Next.js 15.3+, AppConfig.ready() for Django, create_app() for Flask).
- Set up the PostHog provider/wrapper component if the framework requires one.
- For posthog-js, set `defaults` to the preset the framework docs reference shows. The example projects can show an older preset.
- For posthog-js, if the framework reference documents a reverse proxy for this framework, set it up as part of this change. Otherwise, suggest one in your final message: a reverse proxy keeps ad blockers from dropping events (https://posthog.com/docs/advanced/proxy.md).
- For posthog-js, if the framework reference documents a reverse proxy for this framework, set it up as part of this change. Otherwise, suggest one in your final message: a reverse proxy keeps ad blockers from dropping events, and the `instrument-reverse-proxy` skill sets one up (https://posthog.com/docs/advanced/proxy.md).

STEP 5: Identify users.
- Add PostHog `identify()` calls on the client side during login and signup events.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ STEP 3: Install the PostHog SDK. (Skip if PostHog is already set up.)
STEP 4: Initialize PostHog. (Skip if PostHog is already set up.)
- Follow the framework reference for where and how to initialize. This varies significantly by framework (e.g., instrumentation-client.ts for Next.js 15.3+, AppConfig.ready() for Django, create_app() for Flask).
- For posthog-js, set `defaults` to the preset the framework docs reference shows. The example projects can show an older preset.
- For posthog-js, if the framework reference documents a reverse proxy for this framework, set it up as part of this change. Otherwise, suggest one in your final message: a reverse proxy keeps ad blockers from dropping events (https://posthog.com/docs/advanced/proxy.md).
- For posthog-js, if the framework reference documents a reverse proxy for this framework, set it up as part of this change. Otherwise, suggest one in your final message: a reverse proxy keeps ad blockers from dropping events, and the `instrument-reverse-proxy` skill sets one up (https://posthog.com/docs/advanced/proxy.md).

STEP 5: Plan event tracking.
- From the project's file list, select between 10 and 15 files that might have interesting business value for event tracking, especially conversion and churn events.
Expand Down
29 changes: 29 additions & 0 deletions context/skills/omnibus/instrument-reverse-proxy/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Aggregated reverse proxy skill - self-hosted where the platform makes it easy, PostHog's managed proxy elsewhere
type: skill
template: description.md
category: integration
description: Route PostHog's browser traffic through the app's own domain, so ad blockers don't drop events, session replays, and feature flag calls. Use when the user asks for a reverse proxy or agrees to one you suggested, for example after adding posthog-js or when events or recordings seem to be missing. Don't set one up unprompted. Prefers a self-hosted proxy where the app's platform makes it a small change, such as Next.js or Cloudflare, and PostHog's managed reverse proxy elsewhere.
tags: [reverse-proxy, javascript_web]
shared_docs:
- https://posthog.com/docs/advanced/proxy.md
variants:
- id: all
display_name: all supported platforms
tags: []
docs_urls:
- https://posthog.com/docs/advanced/proxy/proxy-reference.md
- https://posthog.com/docs/advanced/proxy/cloudflare.md
- https://posthog.com/docs/advanced/proxy/vercel.md
- https://posthog.com/docs/advanced/proxy/nextjs.md
- https://posthog.com/docs/advanced/proxy/nextjs-middleware.md
- https://posthog.com/docs/advanced/proxy/netlify.md
- https://posthog.com/docs/advanced/proxy/nuxt.md
- https://posthog.com/docs/advanced/proxy/sveltekit.md
- https://posthog.com/docs/advanced/proxy/remix.md
- https://posthog.com/docs/advanced/proxy/node.md
- https://posthog.com/docs/advanced/proxy/railway.md
- https://posthog.com/docs/advanced/proxy/nginx.md
- https://posthog.com/docs/advanced/proxy/caddy.md
- https://posthog.com/docs/advanced/proxy/cloudfront.md
- https://posthog.com/docs/advanced/proxy/kubernetes-ingress-controller.md
- https://posthog.com/docs/advanced/proxy/pomerium.md
103 changes: 103 additions & 0 deletions context/skills/omnibus/instrument-reverse-proxy/description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Set up a PostHog reverse proxy

Use this skill to route the PostHog browser SDK's traffic through a domain the app owns. Ad blockers keep lists of analytics domains and block requests to them. Through a proxy, events, session replay data, feature flag calls, surveys, and the SDK's own scripts load from the app's domain, so ad blockers don't drop them.

A proxy is optional, and setting one up can take several steps the user has to do themselves, such as a DNS record. Set one up only when the user asked for it or agreed to your suggestion.

This skill covers the browser SDK: posthog-js and the snippet. Server-side SDKs don't need a proxy, so don't add one for them.

There are two ways to do it:

- **A self-hosted proxy.** A rewrite, edge function, or server route in the app's own hosting forwards a path, such as `/e`, to PostHog. On platforms with built-in rewrites or edge functions, it's a few lines of configuration.
- **PostHog's managed reverse proxy.** The user adds one DNS record for a subdomain, such as `e.example.com`, and PostHog runs the proxy, its TLS certificate, and its routing. It's free on PostHog Cloud, and no proxy code goes into the app.

### Trade-offs

| | Self-hosted proxy | Managed proxy |
| --- | --- | --- |
| Setup | Code or config in the app, deployed with it. No DNS change. | One CNAME record. Needs DNS access and an organization admin. |
| Blocking | A path on the app's own domain has no CNAME, so DNS-level blockers (NextDNS, Pi-hole) can't trace it back to PostHog. | DNS-level blockers that uncloak CNAMEs can see the record points at PostHog. Browser extensions can't. |
| Cost | Requests count against the platform's bandwidth and invocation quotas. Vercel, Netlify, CloudFront, and Railway bill for them, which adds up on high-traffic sites. | Free. Doesn't touch the app's hosting quotas. |
| Maintenance | The app owns it: routing, headers, body size limits. PostHog support can't troubleshoot it. | PostHog maintains it and offers a `proxy-diagnose` check. |
| Compliance | Runs wherever the app runs. Works for HIPAA and EU-only edge requirements. | Runs on Cloudflare, a PostHog subprocessor. Not HIPAA-compliant, and EU edge termination isn't guaranteed. |
| Availability | Any PostHog, including self-hosted. | PostHog Cloud only. An organization gets a limited number of proxies (`proxy-list` returns the limit). |

## Reference files

{references}

The overview reference covers the managed proxy: its steps, troubleshooting, and FAQ. Each platform reference shows a self-hosted proxy for one platform. When this page and a reference disagree on a platform's configuration, follow the reference.

## API tools

The managed proxy steps use PostHog API tools. The same tool names work through the PostHog MCP server (its `exec` tool) and through the hog CLI (`hog api call <tool> '<json input>'`).

- `proxy-list`: the organization's proxies, with their status, and the most the plan allows.
- `proxy-create`: creates a proxy for a domain. Returns its `id` and the `target_cname` for the DNS record.
- `proxy-get`: one proxy's status and message.
- `proxy-diagnose`: checks DNS, CAA records, the certificate, and a live event, and says how to fix each failure.
- `proxy-retry`: starts provisioning again after the user fixed the problem.

Proxies belong to the organization, not the project, and only organization admins can create, retry, diagnose, or delete them. The token needs the `organization:write` scope. With the hog CLI, run `hog auth add organization:write` and ask the user to approve it in the browser. If PostHog rejects the call because the user isn't an admin, give the steps to an admin, or use a self-hosted proxy instead.

## Instructions

Follow these steps IN ORDER:

STEP 1: Check what the app already has.
- Find where the app initializes posthog-js and read its `api_host` and `ui_host`. Check the env files and the hosting configuration that set them too.
- If `api_host` already points at a domain or a path of the app, a proxy exists. Check that it routes as the guidelines below require, fix it if it doesn't, and stop there.
- Find the PostHog Cloud region. Use the host that the tool or CLI which started this work gave you, then the `host` in `posthog.json`, then the current `api_host` (`us.i.posthog.com` is US, `eu.i.posthog.com` is EU). If none of them names it, ask the user: "Are you on PostHog US Cloud or EU Cloud?" Do not assume US Cloud. A proxy that forwards to the wrong region loses every event it receives.
- Self-hosted PostHog has no managed proxy. Go to STEP 3B.
- Run `proxy-list`. If a proxy with status `valid` already exists for a subdomain of this app's domain, use it: go to STEP 4.

STEP 2: Choose the self-hosted or the managed proxy.
- Prefer a self-hosted proxy when the app's hosting makes it a small change:
- Next.js (rewrites in `next.config.*`, or middleware), Nuxt, SvelteKit, or Remix, which have proxy references above.
- An app deployed on Cloudflare (Workers or Pages), Vercel, or Netlify, whose configuration is in the repo.
- A reverse proxy configuration the repo already contains: nginx, Caddy, or a Kubernetes ingress.
- Also use a self-hosted proxy, whatever the platform, when the managed proxy can't serve the app:
- Self-hosted PostHog.
- The app processes protected health information under HIPAA, or needs EU-only edge termination.
- The domain is a Cloudflare Enterprise zone with zone hold enabled.
- The user isn't an organization admin, can't add DNS records, or the organization has as many proxies as its plan allows (`proxy-list`) and none fits this app.
- Prefer the managed proxy for everything else, where a self-hosted proxy means adding a server, a function, or hosting the app doesn't have yet. For example, static hosting without rewrites to other origins, or a framework with no proxy reference above.
- On Vercel, Netlify, CloudFront, or Railway, mention the cost trade-off for a high-traffic site, and offer the managed proxy as the alternative.
- Tell the user which one you chose and why, in one or two sentences from the trade-offs table. Ask before going ahead only when the choice isn't clear from the rules above.

STEP 3A: Set up the managed proxy.
- Pick a subdomain of the domain the app is served from, and confirm it with the user before you create anything. The user must control its DNS, and it should be neutral: no `analytics`, `tracking`, `telemetry`, `posthog`, or `ph` in the name, because ad blockers match those words.
- Run `proxy-create` with `{"domain": "<subdomain>"}`. Keep the `id` and the `target_cname` it returns.
- You can't create DNS records. Give the user the exact record: type `CNAME`, name the subdomain, target the `target_cname`. If their DNS is on Cloudflare, the record must be DNS only (gray cloud), not proxied.
- The status goes from `waiting` to `issuing` to `valid`. That usually takes 2 to 5 minutes after the record exists, and up to 30 minutes when DNS is slow. Check it with `proxy-get`. Don't wait for it in a loop.
- If the status is `erroring` or `timed_out`, or it stays `issuing` for more than 30 minutes, run `proxy-diagnose` and give the user the remediation it returns. After they fix it, run `proxy-retry`.
- Point the SDK at the proxy only once its status is `valid`. Until then, every request to the subdomain fails, and the app would send nothing. If it isn't `valid` yet, make the proxy URL configurable without switching to it: read `api_host` from an environment variable whose default stays PostHog's ingestion host, and tell the user to set the variable once the proxy is live.
- Set `api_host` to `https://<subdomain>` and `ui_host` to the PostHog app host: `https://us.posthog.com` or `https://eu.posthog.com`.

STEP 3B: Set up a self-hosted proxy.
- Find the reference for the platform that serves the app, not the framework it is built with. A Vite app deployed on Cloudflare Workers uses the Cloudflare reference, and a Next.js app on Vercel uses the Next.js rewrites reference. If no reference matches, follow the proxy reference's routing rules on whatever the app runs.
- Use the path the reference gives, or a neutral one such as `/e` or `/ingest`. Avoid the words ad blockers match: `analytics`, `tracking`, `telemetry`, `posthog`, `ph`.
- Forward to the region's hosts: `/static/*` and `/array/*` to the assets host, everything else to the ingestion host. The proxy reference lists the headers, methods, and body size a self-hosted proxy must handle. Check the platform's code against it.
- Do not alter the fundamental architecture of existing files. Add the proxy route next to the app's other routes, rewrites, or worker code.
- Make the local dev server proxy the same path, or keep local development pointed at PostHog directly. Don't leave a path that only works in production.
- Set `api_host` to the proxy path or domain and `ui_host` to the PostHog app host: `https://us.posthog.com` or `https://eu.posthog.com`.

STEP 4: Update the app.
- Read `api_host` and `ui_host` the way the app already reads its PostHog settings, usually environment variables. Never hardcode a key.
- If the app sends a Content-Security-Policy, allow the proxy in `script-src` and `connect-src`.
- List any new environment variable in `.env.example`, or the env template the project already uses, with a placeholder value.

STEP 5: Verify the proxy.
- Check routing without writing data. A `GET` of `<proxy>/array/<project token>/config.js` should return JavaScript, and a `POST` of `{"api_key": "<project token>", "distinct_id": "proxy-check"}` to `<proxy>/flags/?v=2` should return JSON with a `flags` key. Neither creates events.
- Avoid sending test events to the user's project. If you must, give them an obvious name and tell the user which events you sent.
- posthog-js's bot filter drops every capture from an automated browser, while flag calls and asset loads still succeed. A Playwright or Puppeteer test that shows no events in PostHog doesn't mean the proxy is broken. Check the proxy's responses instead.
- For the managed proxy, `proxy-get` returning `valid` and the two checks above passing is enough.

STEP 6: Hand off.
- List the files you changed and the approach you chose, with the reason.
- List what the user still has to do, with exact values: the DNS record, environment variables in the hosting platform, and the wait for the managed proxy to become `valid`.
- Tell the user how to check it after the next deploy: in the browser's Network tab, PostHog requests go to the proxy and return `200`.

## Guidelines

Read `references/COMMANDMENTS.md` before STEP 3A or STEP 3B, and follow every rule in it.
Loading