PostHog Integration Skill

This skill covers the full setup and usage of PostHog in web projects, including analytics, error tracking, source maps, and API-based querying.

Configuration

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:

Step 1: Register App Domains

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.

Which domains to register

Register production domains only. Do not add localhost or development URLs — dev traffic should not appear in analytics.

Using the PostHog API

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: