This skill covers the full setup and usage of PostHog in web projects, including analytics, error tracking, source maps, and API-based querying.
This skill uses four environment variables. Before proceeding with any step, verify they are set in the project's environment (e.g., .env, hosting platform secrets, CI environment):
| Variable | Purpose | Where to find it |
|---|---|---|
POSTHOG_PERSONAL_API_KEY |
Personal API key for server-side / CLI operations. Never expose in frontend code. | PostHog → Settings → Personal API Keys |
POSTHOG_PROJECT_ID |
Numeric project ID | The number in your PostHog project URL |
POSTHOG_HOST |
PostHog API host — used for all server-side API calls (token fetch, domain registration, querying data) | https://us.posthog.com (US) or https://eu.posthog.com (EU), or your self-hosted URL |
POSTHOG_INGEST_HOST |
PostHog ingest host — where the browser SDK sends events. Use your reverse proxy URL if you have one. | https://us.i.posthog.com (US) or https://eu.i.posthog.com (EU), or your proxy URL |
If any are missing, ask the user to provide them before continuing. Remind them:
phc_) is what goes in the frontend — it can only send events. Fetch it via the API (see Step 2).POSTHOG_HOST is the API host (us.posthog.com) for server-side calls. POSTHOG_INGEST_HOST is the ingest host (us.i.posthog.com) where the browser SDK sends events. These are different hosts — don't mix them up.PostHog has two domain lists managed via the project API:
| Setting | API field | Purpose |
|---|---|---|
| Authorized Domains | app_urls |
Domains for Web Analytics breakdowns and toolbar access |
| Recording Domains | recording_domains |
Domains where session replays are captured (only needed if recording is enabled) |
Always register app_urls. Only register recording_domains if session replays are enabled.
Register production domains only. Do not add localhost or development URLs — dev traffic should not appear in analytics.
Read the current app_urls, merge in new ones, and PATCH back:
node -e "
(async () => {
const host = process.env.POSTHOG_HOST;
const projectId = process.env.POSTHOG_PROJECT_ID;
const apiKey = process.env.POSTHOG_PERSONAL_API_KEY;
const projRes = await fetch(\\`\\${host}/api/projects/\\${projectId}/\\`, {
headers: { Authorization: \\`Bearer \\${apiKey}\\` }
});
const project = await projRes.json();
const existing = project.app_urls || [];
// Replace with the project's actual production domains
const newDomains = [
'<https://example.com>',
// '<https://app.example.com>',
];
const allDomains = [...new Set([...existing, ...newDomains])];
const updateRes = await fetch(\\`\\${host}/api/projects/\\${projectId}/\\`, {
method: 'PATCH',
headers: {
Authorization: \\`Bearer \\${apiKey}\\`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ app_urls: allDomains })
});
const result = await updateRes.json();
console.log('Status:', updateRes.status);
console.log('Updated app_urls:', JSON.stringify(result.app_urls));
})().catch(e => console.error(e));
"
Notes: