---
name: posthog
description: "PostHog analytics integration for Next.js apps. Use when: adding analytics, creating dashboards, tracking custom events, debugging CSP issues, creating insights via API. Covers initialization, event tracking, dashboard creation, and troubleshooting."
metadata:
  openclaw:
    emoji: "📊"
    requires:
      bins: ["curl", "jq"]
---

# PostHog Skill

Integrate PostHog analytics into Next.js applications.

## Credentials

**Personal API Key Location:** `~/.config/posthog/api-key`
- Required for dashboard/insight management via API
- NOT the same as the public project key (`NEXT_PUBLIC_POSTHOG_KEY`)

```bash
# Read the API key
POSTHOG_KEY=$(cat ~/.config/posthog/api-key)

# Use in API calls
curl -H "Authorization: Bearer $POSTHOG_KEY" \
  "https://us.posthog.com/api/projects/{PROJECT_ID}/..."
```

**Project Keys (in app `.env.local`):**
```env
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx  # Public key for client SDK
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
```

---

## Next.js Integration

### CRITICAL: Next.js 14 vs 15

**Next.js 14.x:** Initialize PostHog directly in the provider component (NOT in `instrumentation-client.ts`)

**Next.js 15+:** Can use `instrumentation-client.ts` with `export function register()`

### Installation

```bash
npm install posthog-js
```

### PostHog Provider (Next.js 14)

Create `src/components/providers/PostHogProvider.tsx`:

```tsx
'use client';

import posthog from 'posthog-js';
import { PostHogProvider as PHProvider, usePostHog } from 'posthog-js/react';
import { useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { usePathname, useSearchParams } from 'next/navigation';
import { Suspense } from 'react';

// Initialize PostHog on module load (browser only)
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
    person_profiles: 'identified_only',
    capture_pageview: false, // Manual for SPA navigation
    capture_pageleave: true,
    autocapture: true,
    disable_session_recording: true, // Enable if needed
    persistence: 'localStorage+cookie',
  });
}

// Track pageviews on route change
function PostHogPageView() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const posthogClient = usePostHog();

  useEffect(() => {
    if (pathname && posthogClient) {
      let url = window.origin + pathname;
      if (searchParams?.toString()) {
        url = url + '?' + searchParams.toString();
      }
      posthogClient.capture('$pageview', { '$current_url': url });
    }
  }, [pathname, searchParams, posthogClient]);

  return null;
}

// Identify users when logged in
function PostHogIdentify() {
  const { data: session, status } = useSession();
  const posthogClient = usePostHog();

  useEffect(() => {
    if (status === 'authenticated' && session?.user?.email && posthogClient) {
      posthogClient.identify(session.user.email, {
        email: session.user.email,
        name: session.user.name || undefined,
      });
    } else if (status === 'unauthenticated' && posthogClient) {
      posthogClient.reset();
    }
  }, [session, status, posthogClient]);

  return null;
}

export function PostHogProvider({ children }: { children: React.ReactNode }) {
  return (
    <PHProvider client={posthog}>
      <Suspense fallback={null}>
        <PostHogPageView />
      </Suspense>
      <PostHogIdentify />
      {children}
    </PHProvider>
  );
}
```

### Add to Providers

```tsx
// src/components/Providers.tsx
import { PostHogProvider } from './providers/PostHogProvider';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider>
      <PostHogProvider>
        {children}
      </PostHogProvider>
    </SessionProvider>
  );
}
```

---

## CRITICAL: Content Security Policy (CSP)

PostHog loads external scripts that MUST be allowed in CSP.

### next.config.mjs

```js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      // REQUIRED for PostHog:
      "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://us-assets.i.posthog.com https://*.posthog.com",
      "connect-src 'self' https://us.i.posthog.com https://*.posthog.com",
      // Other directives...
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: blob: https:",
    ].join('; ')
  }
];
```

**Without these CSP rules, you'll see console errors:**
```
Loading the script 'https://us-assets.i.posthog.com/static/...' violates 
the Content Security Policy directive: "script-src 'self'..."
```

---

## Custom Events Library

Create a typed events library for consistency:

```tsx
// src/lib/posthog-events.ts
import posthog from 'posthog-js';

function capture(event: string, properties?: Record<string, unknown>) {
  if (typeof window !== 'undefined' && posthog?.capture) {
    posthog.capture(event, properties);
  }
}

export const trackEvent = {
  // Product events
  productCreated: (props: { slug: string; name: string; type?: string }) => 
    capture('product_created', props),
  
  productViewed: (props: { slug: string; phase: string }) => 
    capture('product_viewed', props),
  
  // Build events
  buildStarted: (props: { slug: string; buildType?: string }) => 
    capture('build_started', props),
  
  buildCompleted: (props: { slug: string; success: boolean }) => 
    capture('build_completed', props),
  
  // Chat events
  chatMessageSent: (props: { slug: string; phase: string }) => 
    capture('chat_message_sent', props),
  
  // Add more as needed...
};
```

**Usage:**
```tsx
import { trackEvent } from '@/lib/posthog-events';

// In component
trackEvent.productCreated({ slug: 'my-app', name: 'My App', type: 'manual' });
```

---

## Dashboard & Insight API

### CRITICAL: Query Format

PostHog UI requires queries wrapped in `InsightVizNode`:

```json
// ❌ WRONG - Will show blank or wrong data in UI
{
  "kind": "TrendsQuery",
  "series": [...]
}

// ✅ CORRECT - Works in PostHog UI
{
  "kind": "InsightVizNode",
  "source": {
    "kind": "TrendsQuery",
    "series": [...]
  }
}
```

### Create Dashboard

```bash
POSTHOG_KEY=$(cat ~/.config/posthog/api-key)
PROJECT_ID=316683

curl -X POST -H "Authorization: Bearer $POSTHOG_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Dashboard",
    "description": "Dashboard description"
  }' \
  "https://us.posthog.com/api/projects/$PROJECT_ID/dashboards/"
```

### Create Insight (Correct Format)

```bash
# Line chart for custom event
curl -X POST -H "Authorization: Bearer $POSTHOG_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Products Created Over Time",
    "dashboards": [DASHBOARD_ID],
    "query": {
      "kind": "InsightVizNode",
      "source": {
        "kind": "TrendsQuery",
        "series": [{
          "kind": "EventsNode",
          "math": "total",
          "name": "Products",
          "event": "product_created"
        }],
        "interval": "day",
        "dateRange": {"date_from": "-30d"},
        "trendsFilter": {"display": "ActionsLineGraph"},
        "version": 2
      }
    }
  }' \
  "https://us.posthog.com/api/projects/$PROJECT_ID/insights/"
```

### Display Types

| Display | Use For |
|---------|---------|
| `ActionsLineGraph` | Time series trends |
| `BoldNumber` | Single metric/KPI |
| `ActionsBarValue` | Comparison bars |
| `ActionsPie` | Distribution/breakdown |
| `ActionsTable` | Detailed data table |

### Math Types

| Math | Description |
|------|-------------|
| `total` | Total count of events |
| `dau` | Daily active users |
| `weekly_active` | Weekly active users |
| `monthly_active` | Monthly active users |
| `unique_users` | Unique users count |

### Breakdown Filter

```json
{
  "breakdownFilter": {
    "breakdown": "property_name",
    "breakdown_type": "event"  // or "person", "cohort"
  }
}
```

---

## Troubleshooting

### Events not appearing

1. **Check CSP** — Look for CSP errors in browser console
2. **Check Network tab** — Should see requests to `us.i.posthog.com`
3. **Check initialization** — Add `console.log` in PostHog loaded callback
4. **Wait for processing** — Events take 1-2 minutes to appear in dashboards

### Dashboards showing blank

1. **Check query format** — Must use `InsightVizNode` wrapper
2. **Check event names** — Must match exactly (case-sensitive)
3. **Check date range** — Ensure events exist in the selected period
4. **Edit insight in UI** — If it shows wrong event type, query format is wrong

### Debug in browser console

```js
// Check if PostHog is loaded
console.log(posthog);

// Manually capture event
posthog.capture('test_event', { test: true });

// Check identified user
console.log(posthog.get_distinct_id());
```

---

## Scripts

### Check Dashboard Insights

```bash
# scripts/check-dashboard.sh
POSTHOG_KEY=$(cat ~/.config/posthog/api-key)
PROJECT_ID=${1:-316683}
DASHBOARD_ID=${2}

curl -s -H "Authorization: Bearer $POSTHOG_KEY" \
  "https://us.posthog.com/api/projects/$PROJECT_ID/dashboards/$DASHBOARD_ID/" | \
  jq '.tiles[] | {name: .insight.name, event: .insight.query.source.series[0].event}'
```

### Fix Insight Query Format

```bash
# scripts/fix-insight.sh
POSTHOG_KEY=$(cat ~/.config/posthog/api-key)
PROJECT_ID=${1:-316683}
INSIGHT_ID=$2
EVENT_NAME=$3
DISPLAY=${4:-ActionsLineGraph}

curl -X PATCH -H "Authorization: Bearer $POSTHOG_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"query\": {
      \"kind\": \"InsightVizNode\",
      \"source\": {
        \"kind\": \"TrendsQuery\",
        \"series\": [{
          \"kind\": \"EventsNode\",
          \"math\": \"total\",
          \"event\": \"$EVENT_NAME\"
        }],
        \"dateRange\": {\"date_from\": \"-30d\"},
        \"trendsFilter\": {\"display\": \"$DISPLAY\"},
        \"version\": 2
      }
    }
  }" \
  "https://us.posthog.com/api/projects/$PROJECT_ID/insights/$INSIGHT_ID/"
```

---

## Symphony-Specific

**Project ID:** 316683
**API Host:** us.posthog.com (US cloud)

**Custom Dashboards:**
- Product Lifecycle: 1287840
- Build & Design: 1287841
- Sales Pipeline: 1287842
- User Engagement: 1287843

**Events tracked:**
- `product_created` — Product creation (with `type` property)
- `product_viewed` — Product page view (with `phase` property)
- `chat_message_sent` — Chat messages (with `phase` property)
- `build_started` — Build initiated (with `buildType` property)
- `design_approved` — Design approved
- `design_regenerated` — Design regenerated
- `deal_won` / `deal_lost` — Sales outcomes
- `campaign_brief_generated` — Marketing briefs
