diff --git a/context/commandments.yaml b/context/commandments.yaml index a257979f..2d0f75f8 100644 --- a/context/commandments.yaml +++ b/context/commandments.yaml @@ -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 diff --git a/context/skills/omnibus/instrument-error-tracking/description.md b/context/skills/omnibus/instrument-error-tracking/description.md index f1ca5b47..58180db3 100644 --- a/context/skills/omnibus/instrument-error-tracking/description.md +++ b/context/skills/omnibus/instrument-error-tracking/description.md @@ -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. diff --git a/context/skills/omnibus/instrument-integration/description.md b/context/skills/omnibus/instrument-integration/description.md index d52b32c4..e2be2837 100644 --- a/context/skills/omnibus/instrument-integration/description.md +++ b/context/skills/omnibus/instrument-integration/description.md @@ -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. diff --git a/context/skills/omnibus/instrument-product-analytics/description.md b/context/skills/omnibus/instrument-product-analytics/description.md index fe2a3284..3693ddbb 100644 --- a/context/skills/omnibus/instrument-product-analytics/description.md +++ b/context/skills/omnibus/instrument-product-analytics/description.md @@ -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. diff --git a/context/skills/omnibus/instrument-reverse-proxy/config.yaml b/context/skills/omnibus/instrument-reverse-proxy/config.yaml new file mode 100644 index 00000000..df584a9a --- /dev/null +++ b/context/skills/omnibus/instrument-reverse-proxy/config.yaml @@ -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 diff --git a/context/skills/omnibus/instrument-reverse-proxy/description.md b/context/skills/omnibus/instrument-reverse-proxy/description.md new file mode 100644 index 00000000..25d95698 --- /dev/null +++ b/context/skills/omnibus/instrument-reverse-proxy/description.md @@ -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 ''`). + +- `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": ""}`. 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://` 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 `/array//config.js` should return JavaScript, and a `POST` of `{"api_key": "", "distinct_id": "proxy-check"}` to `/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.