PostHog Analytics Skill
Load with: base.md + [framework].md
For implementing product analytics with PostHog - event tracking, user identification, feature flags, and project-specific dashboards.
Sources: PostHog Docs | Product Analytics | Feature Flags
Philosophy
Measure what matters, not everything.
Analytics should answer specific questions:
Are users getting value? (activation, retention) Where do users struggle? (funnels, drop-offs) What features drive engagement? (feature usage) Is the product growing? (acquisition, referrals)
Don't track everything. Track what informs decisions.
Installation Next.js (App Router) npm install posthog-js
// lib/posthog.ts import posthog from 'posthog-js';
export function initPostHog() { if (typeof window !== 'undefined' && !posthog.__loaded) { 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', // Only create profiles for identified users capture_pageview: false, // We'll handle this manually for SPA capture_pageleave: true, loaded: (posthog) => { if (process.env.NODE_ENV === 'development') { posthog.debug(); } }, }); } return posthog; }
export { posthog };
// app/providers.tsx 'use client';
import { useEffect } from 'react'; import { usePathname, useSearchParams } from 'next/navigation'; import { initPostHog, posthog } from '@/lib/posthog';
export function PostHogProvider({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const searchParams = useSearchParams();
useEffect(() => { initPostHog(); }, []);
// Track pageviews
useEffect(() => {
if (pathname) {
let url = window.origin + pathname;
if (searchParams.toString()) {
url += ?${searchParams.toString()};
}
posthog.capture('$pageview', { $current_url: url });
}
}, [pathname, searchParams]);
return <>{children}</>; }
// app/layout.tsx import { PostHogProvider } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) { return (
React (Vite/CRA) // src/posthog.ts import posthog from 'posthog-js';
posthog.init(import.meta.env.VITE_POSTHOG_KEY, { api_host: import.meta.env.VITE_POSTHOG_HOST || 'https://us.i.posthog.com', person_profiles: 'identified_only', });
export { posthog };
// src/main.tsx import { PostHogProvider } from 'posthog-js/react'; import { posthog } from './posthog';
ReactDOM.createRoot(document.getElementById('root')!).render(
Python (FastAPI/Flask) pip install posthog
analytics/posthog_client.py
import posthog from functools import lru_cache
@lru_cache() def get_posthog(): posthog.project_api_key = os.environ["POSTHOG_API_KEY"] posthog.host = os.environ.get("POSTHOG_HOST", "https://us.i.posthog.com") posthog.debug = os.environ.get("ENV") == "development" return posthog
Usage
def track_event(user_id: str, event: str, properties: dict = None): ph = get_posthog() ph.capture( distinct_id=user_id, event=event, properties=properties or {} )
def identify_user(user_id: str, properties: dict): ph = get_posthog() ph.identify(user_id, properties)
Node.js (Express/Hono) npm install posthog-node
// lib/posthog.ts import { PostHog } from 'posthog-node';
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, { host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com', });
// Flush on shutdown process.on('SIGTERM', () => posthog.shutdown());
export { posthog };
// Usage
export function trackEvent(userId: string, event: string, properties?: Record
export function identifyUser(userId: string, properties: Record
Environment Variables
.env.local (Next.js) - SAFE: These are meant to be public
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxxxxx NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
.env (Backend) - Keep private
POSTHOG_API_KEY=phc_xxxxxxxxxxxxxxxxxxxx POSTHOG_HOST=https://us.i.posthog.com
Add to credentials.md patterns:
'POSTHOG_API_KEY': r'phc_[A-Za-z0-9]+',
User Identification When to Identify // Identify on signup async function handleSignup(email: string, name: string) { const user = await createUser(email, name);
posthog.identify(user.id, { email: user.email, name: user.name, created_at: user.createdAt, plan: 'free', });
posthog.capture('user_signed_up', { signup_method: 'email', }); }
// Identify on login async function handleLogin(email: string) { const user = await authenticateUser(email);
posthog.identify(user.id, { email: user.email, name: user.name, plan: user.plan, last_login: new Date().toISOString(), });
posthog.capture('user_logged_in'); }
// Reset on logout function handleLogout() { posthog.capture('user_logged_out'); posthog.reset(); // Clears identity }
User Properties // Standard properties to track interface UserProperties { // Identity email: string; name: string;
// Lifecycle created_at: string; plan: 'free' | 'pro' | 'enterprise';
// Engagement onboarding_completed: boolean; feature_count: number;
// Business company_name?: string; company_size?: string; industry?: string; }
// Update properties when they change posthog.capture('$set', { $set: { plan: 'pro' }, });
Event Tracking Patterns Event Naming Convention // Format: [object]_[action] // Use snake_case, past tense for actions
// ✅ Good event names 'user_signed_up' 'feature_created' 'subscription_upgraded' 'onboarding_completed' 'invite_sent' 'file_uploaded' 'search_performed' 'checkout_started' 'payment_completed'
// ❌ Bad event names 'click' // Too vague 'ButtonClick' // Not snake_case 'user signup' // Spaces 'creatingFeature' // Not past tense
Core Events by Category // === AUTHENTICATION === posthog.capture('user_signed_up', { signup_method: 'google' | 'email' | 'github', referral_source: 'organic' | 'paid' | 'referral', });
posthog.capture('user_logged_in', { login_method: 'google' | 'email' | 'magic_link', });
posthog.capture('user_logged_out');
posthog.capture('password_reset_requested');
// === ONBOARDING === posthog.capture('onboarding_started');
posthog.capture('onboarding_step_completed', { step_name: 'profile' | 'preferences' | 'first_action', step_number: 1, total_steps: 3, });
posthog.capture('onboarding_completed', { duration_seconds: 120, steps_skipped: 0, });
posthog.capture('onboarding_skipped', { skipped_at_step: 2, });
// === FEATURE USAGE === posthog.capture('feature_used', { feature_name: 'export' | 'share' | 'duplicate', context: 'dashboard' | 'editor', });
posthog.capture('[resource]_created', { resource_type: 'project' | 'document' | 'team', // Resource-specific properties });
posthog.capture('[resource]_updated', { resource_type: 'project', fields_changed: ['name', 'description'], });
posthog.capture('[resource]_deleted', { resource_type: 'project', });
// === BILLING === posthog.capture('pricing_page_viewed', { current_plan: 'free', });
posthog.capture('checkout_started', { plan: 'pro', billing_period: 'monthly' | 'annual', price: 29, });
posthog.capture('subscription_upgraded', { from_plan: 'free', to_plan: 'pro', mrr_change: 29, });
posthog.capture('subscription_downgraded', { from_plan: 'pro', to_plan: 'free', reason: 'too_expensive' | 'missing_features' | 'not_using', });
posthog.capture('subscription_cancelled', { plan: 'pro', reason: 'string', feedback: 'string', });
// === ERRORS === posthog.capture('error_occurred', { error_type: 'api_error' | 'validation_error' | 'network_error', error_message: 'string', error_code: 'string', page: '/dashboard', });
React Hook for Tracking // hooks/useTrack.ts import { useCallback } from 'react'; import { posthog } from '@/lib/posthog';
export function useTrack() {
const track = useCallback((event: string, properties?: Record
return { track }; }
// Usage function CreateProjectButton() { const { track } = useTrack();
const handleCreate = async () => { track('project_creation_started');
try {
const project = await createProject();
track('project_created', {
project_id: project.id,
template_used: project.template,
});
} catch (error) {
track('project_creation_failed', {
error_message: error.message,
});
}
};
return ; }
Feature Flags Setup // Check feature flag (client-side) import { useFeatureFlagEnabled } from 'posthog-js/react';
function NewFeature() { const showNewUI = useFeatureFlagEnabled('new-dashboard-ui');
if (showNewUI) {
return
// With payload import { useFeatureFlagPayload } from 'posthog-js/react';
function PricingPage() { const pricingConfig = useFeatureFlagPayload('pricing-experiment'); // pricingConfig = { price: 29, showAnnual: true }
return
Server-Side (Next.js) // app/dashboard/page.tsx import { PostHog } from 'posthog-node'; import { cookies } from 'next/headers';
async function getFeatureFlags(userId: string) { const posthog = new PostHog(process.env.POSTHOG_API_KEY!);
const flags = await posthog.getAllFlags(userId); await posthog.shutdown();
return flags; }
export default async function Dashboard() { const cookieStore = cookies(); const userId = cookieStore.get('user_id')?.value;
const flags = await getFeatureFlags(userId);
return (
A/B Testing // Track experiment exposure function ExperimentComponent() { const variant = useFeatureFlagEnabled('checkout-experiment');
useEffect(() => { posthog.capture('experiment_viewed', { experiment: 'checkout-experiment', variant: variant ? 'test' : 'control', }); }, [variant]);
return variant ?
Project-Specific Dashboards SaaS Product
Essential SaaS Dashboards
1. Acquisition Dashboard
Questions answered: Where do users come from? What converts?
Insights to create: - [ ] Signups by source (daily/weekly trend) - [ ] Signup conversion rate by landing page - [ ] Time from first visit to signup - [ ] Signup funnel: Visit → Signup Page → Form Start → Complete
2. Activation Dashboard
Questions answered: Are new users getting value?
Insights to create: - [ ] Onboarding completion rate - [ ] Time to first key action - [ ] Activation rate (% reaching "aha moment" in first 7 days) - [ ] Drop-off by onboarding step - [ ] Feature adoption in first session
3. Engagement Dashboard
Questions answered: How are users using the product?
Insights to create: - [ ] DAU/WAU/MAU trends - [ ] Feature usage heatmap - [ ] Session duration distribution - [ ] Actions per session - [ ] Power users vs casual users
4. Retention Dashboard
Questions answered: Are users coming back?
Insights to create: - [ ] Retention cohorts (D1, D7, D30) - [ ] Churn rate by plan - [ ] Reactivation rate - [ ] Last action before churn - [ ] Features correlated with retention
5. Revenue Dashboard
Questions answered: Is the business growing?
Insights to create: - [ ] MRR trend - [ ] Upgrades vs downgrades - [ ] Trial to paid conversion - [ ] Revenue by plan - [ ] LTV by acquisition source
E-Commerce
Essential E-Commerce Dashboards
1. Conversion Funnel
Insights to create: - [ ] Full funnel: Browse → PDP → Add to Cart → Checkout → Purchase - [ ] Cart abandonment rate - [ ] Checkout drop-off by step - [ ] Payment failure rate
2. Product Performance
Insights to create: - [ ] Product views → purchases (by product) - [ ] Add to cart rate by category - [ ] Search → purchase correlation - [ ] Cross-sell effectiveness
3. Customer Dashboard
Insights to create: - [ ] Repeat purchase rate - [ ] Average order value trend - [ ] Customer lifetime value - [ ] Purchase frequency distribution
Content/Media
Essential Content Dashboards
1. Consumption Dashboard
Insights to create: - [ ] Content views by type - [ ] Read/watch completion rate - [ ] Time on content - [ ] Scroll depth distribution
2. Engagement Dashboard
Insights to create: - [ ] Shares by content - [ ] Comments per article - [ ] Save/bookmark rate - [ ] Return visits to same content
3. Growth Dashboard
Insights to create: - [ ] New vs returning visitors - [ ] Email signup rate - [ ] Referral traffic sources
AI/LLM Application
Essential AI App Dashboards
1. Usage Dashboard
Insights to create: - [ ] Queries per user per day - [ ] Token usage distribution - [ ] Response time p50/p95 - [ ] Error rate by query type
2. Quality Dashboard
Insights to create: - [ ] User feedback (thumbs up/down) - [ ] Regeneration rate (user asked for new response) - [ ] Edit rate (user modified AI output) - [ ] Follow-up query rate
3. Cost Dashboard
Insights to create: - [ ] Token cost per user - [ ] Cost by model - [ ] Cost by feature - [ ] Efficiency trends (value/cost)
Creating Dashboards Using PostHog MCP When setting up analytics for a project:
- First, check existing dashboards:
-
Use
dashboards-get-allto list current dashboards -
Create project-appropriate dashboards:
-
Use
dashboard-createwith descriptive name -
Create insights for each dashboard:
- Use
query-runto test queries - Use
insight-create-from-queryto save -
Use
add-insight-to-dashboardto organize -
Set up key funnels:
- Signup funnel
- Onboarding funnel
- Purchase/conversion funnel
Dashboard Creation Workflow // Example: Creating SaaS dashboards via MCP
// 1. Create dashboard const dashboard = await mcp_posthog_dashboard_create({ name: "Activation Metrics", description: "Track new user activation and onboarding", tags: ["saas", "activation"], });
// 2. Create insights const signupFunnel = await mcp_posthog_query_run({ query: { kind: "InsightVizNode", source: { kind: "FunnelsQuery", series: [ { kind: "EventsNode", event: "user_signed_up", name: "Signed Up" }, { kind: "EventsNode", event: "onboarding_started", name: "Started Onboarding" }, { kind: "EventsNode", event: "onboarding_completed", name: "Completed Onboarding" }, { kind: "EventsNode", event: "first_value_action", name: "First Value" }, ], dateRange: { date_from: "-30d" }, }, }, });
// 3. Save and add to dashboard const insight = await mcp_posthog_insight_create_from_query({ name: "Signup to Activation Funnel", query: signupFunnel.query, favorited: true, });
await mcp_posthog_add_insight_to_dashboard({ insightId: insight.id, dashboardId: dashboard.id, });
Privacy & Compliance GDPR Compliance // Opt-out handling export function handleCookieConsent(consent: boolean) { if (consent) { posthog.opt_in_capturing(); } else { posthog.opt_out_capturing(); } }
// Check consent status const hasConsent = posthog.has_opted_in_capturing();
// Initialize with consent check posthog.init(key, { opt_out_capturing_by_default: true, // Require explicit opt-in respect_dnt: true, // Respect Do Not Track });
Data to Never Track // ❌ NEVER track these posthog.capture('event', { password: '...', // Credentials credit_card: '...', // Payment info ssn: '...', // Government IDs medical_info: '...', // Health data full_address: '...', // Detailed location });
// ✅ OK to track posthog.capture('event', { country: 'US', // General location plan: 'pro', // Product info feature_used: 'export', // Usage });
Property Sanitization // lib/analytics.ts const SENSITIVE_KEYS = ['password', 'token', 'secret', 'credit', 'ssn'];
function sanitizeProperties(props: Record
export function safeCapture(event: string, properties?: Record
Testing Analytics Development Mode // Disable in development if (process.env.NODE_ENV === 'development') { posthog.opt_out_capturing(); // Or use debug mode posthog.debug(); }
E2E Testing // playwright/fixtures.ts import { test as base } from '@playwright/test';
export const test = base.extend({ page: async ({ page }, use) => { // Mock PostHog to capture events await page.addInitScript(() => { window.capturedEvents = []; window.posthog = { capture: (event, props) => { window.capturedEvents.push({ event, props }); }, identify: () => {}, reset: () => {}, }; }); await use(page); }, });
// In tests test('tracks signup event', async ({ page }) => { await page.goto('/signup'); await page.fill('[name=email]', 'test@example.com'); await page.click('button[type=submit]');
const events = await page.evaluate(() => window.capturedEvents); expect(events).toContainEqual({ event: 'user_signed_up', props: expect.objectContaining({ signup_method: 'email' }), }); });
Debugging PostHog Toolbar // Enable toolbar for debugging posthog.init(key, { // ... loaded: (posthog) => { if (process.env.NODE_ENV === 'development') { posthog.debug(); // Toolbar available via PostHog dashboard } }, });
Event Debugging // Log all events in development posthog.init(key, { _onCapture: (eventName, eventData) => { if (process.env.NODE_ENV === 'development') { console.log('PostHog Event:', eventName, eventData); } }, });
Quick Reference Event Checklist by User Lifecycle
Must-Track Events
Acquisition
- [ ]
page_viewed(automatic with capture_pageview) - [ ]
user_signed_up - [ ]
user_logged_in
Activation
- [ ]
onboarding_started - [ ]
onboarding_step_completed - [ ]
onboarding_completed - [ ]
first_[key_action](your "aha moment")
Engagement
- [ ]
[feature]_used - [ ]
[resource]_created - [ ]
search_performed - [ ]
invite_sent
Revenue
- [ ]
pricing_page_viewed - [ ]
checkout_started - [ ]
subscription_upgraded - [ ]
subscription_cancelled
Retention
- [ ]
session_started - [ ]
feature_[x]_used(power features)
Dashboard Templates Project Type Key Dashboards SaaS Acquisition, Activation, Engagement, Retention, Revenue E-Commerce Conversion Funnel, Product Performance, Customer LTV Content Consumption, Engagement, Growth AI/LLM Usage, Quality, Cost Mobile App Installs, Onboarding, DAU/MAU, Crashes Properties to Always Include // Auto-enriched by PostHog $current_url $browser $device_type $os
// Add these yourself user_plan // 'free' | 'pro' | 'enterprise' user_role // 'admin' | 'member' company_id // For B2B feature_context // Where in the app