--- name: posthog-sdk-patterns description: 'Production-ready PostHog SDK patterns: singleton client, typed events, React hooks, Next.js App Router integration, and Python patterns. Trigger: "posthog SDK patterns", "posthog best practices", "posthog React hook", "posthog Next.js", "posthog typescript". ' allowed-tools: Read, Write, Edit version: 1.12.0 license: MIT author: Jeremy Longshore tags: - saas - posthog - python - typescript compatibility: Designed for Claude Code, also compatible with Codex and OpenClaw --- # PostHog SDK Patterns ## Overview Production-ready patterns for PostHog integrations: singleton client, type-safe event capture, React hooks for feature flags, Next.js App Router provider, and Python integration patterns. ## Prerequisites - `posthog-js` and/or `posthog-node` installed - TypeScript project with strict mode - React/Next.js (for frontend patterns) ## Instructions ### Step 1: Singleton Server Client ```typescript // lib/posthog-server.ts import { PostHog } from 'posthog-node'; let client: PostHog | null = null; export function getPostHogServer(): PostHog { if (!client) { client = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com', personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY, flushAt: 20, flushInterval: 10000, }); } return client; } // Graceful shutdown hook process.on('SIGTERM', async () => { if (client) await client.shutdown(); process.exit(0); }); ``` ### Step 2: Type-Safe Event Capture ```typescript // lib/analytics/events.ts type EventMap = { user_signed_up: { method: 'email' | 'google' | 'github' }; feature_used: { feature_name: string; duration_ms?: number }; subscription_started: { plan: string; interval: 'monthly' | 'annual'; mrr: number }; item_created: { item_type: string; source: 'web' | 'api' }; search_performed: { query: string; results_count: number }; }; export function capture( distinctId: string, event: K, properties: EventMap[K] ) { const ph = getPostHogServer(); ph.capture({ distinctId, event, properties }); } // Usage: fully type-checked capture('user-123', 'subscription_started', { plan: 'pro', interval: 'annual', mrr: 99, }); ``` ### Step 3: React Feature Flag Hook ```typescript // hooks/useFeatureFlag.ts 'use client'; import { useEffect, useState } from 'react'; import posthog from 'posthog-js'; export function useFeatureFlag(flagKey: string): boolean | undefined { const [enabled, setEnabled] = useState(undefined); useEffect(() => { // Check immediately if flags are already loaded const current = posthog.isFeatureEnabled(flagKey); if (current !== undefined) { setEnabled(current); } // Listen for flag updates (initial load or remote changes) posthog.onFeatureFlags(() => { setEnabled(posthog.isFeatureEnabled(flagKey) ?? false); }); }, [flagKey]); return enabled; } // Multivariate variant hook export function useFeatureFlagVariant(flagKey: string): string | undefined { const [variant, setVariant] = useState(undefined); useEffect(() => { posthog.onFeatureFlags(() => { const value = posthog.getFeatureFlag(flagKey); setVariant(typeof value === 'string' ? value : undefined); }); }, [flagKey]); return variant; } // Usage in a component function CheckoutButton() { const newCheckout = useFeatureFlag('new-checkout-v2'); if (newCheckout === undefined) return ; // Loading return newCheckout ? : ; } ``` ### Step 4: Next.js App Router Provider ```typescript // app/providers.tsx 'use client'; import posthog from 'posthog-js'; import { PostHogProvider } from 'posthog-js/react'; import { usePathname, useSearchParams } from 'next/navigation'; import { useEffect, Suspense } from 'react'; function PostHogPageView() { const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { if (pathname) { let url = window.origin + pathname; if (searchParams.toString()) { url += '?' + searchParams.toString(); } posthog.capture('$pageview', { $current_url: url }); } }, [pathname, searchParams]); return null; } export function PHProvider({ children }: { children: React.ReactNode }) { useEffect(() => { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com', capture_pageview: false, // We handle manually in PostHogPageView capture_pageleave: true, loaded: (ph) => { if (process.env.NODE_ENV === 'development') ph.debug(); }, }); }, []); return ( {children} ); } // app/layout.tsx // import { PHProvider } from './providers'; // export default function RootLayout({ children }) { // return {children}; // } ``` ### Step 5: Reverse Proxy for Ad Blocker Bypass ```typescript // next.config.js — proxy PostHog through your domain module.exports = { async rewrites() { return [ { source: '/ingest/static/:path*', destination: 'https://us-assets.i.posthog.com/static/:path*' }, { source: '/ingest/:path*', destination: 'https://us.i.posthog.com/:path*' }, { source: '/ingest/decide', destination: 'https://us.i.posthog.com/decide' }, ]; }, }; // Then in your posthog.init: // posthog.init('phc_...', { api_host: '/ingest' }); ``` ### Step 6: Python Patterns ```python # analytics/posthog_client.py import posthog import os import atexit from functools import lru_cache @lru_cache(maxsize=1) def get_posthog(): """Singleton PostHog client.""" posthog.project_api_key = os.environ['POSTHOG_PROJECT_KEY'] posthog.host = os.getenv('POSTHOG_HOST', 'https://us.i.posthog.com') posthog.personal_api_key = os.getenv('POSTHOG_PERSONAL_API_KEY') posthog.debug = os.getenv('ENVIRONMENT') == 'development' return posthog # Ensure flush on exit atexit.register(lambda: get_posthog().shutdown()) # Type-safe capture helper def track(distinct_id: str, event: str, properties: dict | None = None): ph = get_posthog() ph.capture(distinct_id, event, properties or {}) # Feature flag check def is_enabled(flag_key: str, distinct_id: str, default: bool = False) -> bool: ph = get_posthog() result = ph.feature_enabled(flag_key, distinct_id) return result if result is not None else default ``` ## Error Handling | Pattern | Use Case | Benefit | |---------|----------|---------| | Singleton client | All SDK usage | Prevents multiple initializations | | Type-safe events | Event capture | Compile-time property validation | | Feature flag hooks | React components | Auto-updates on flag changes | | Reverse proxy | Production | Bypasses ad blockers | | Graceful shutdown | Server processes | No lost events on SIGTERM | ## Output - Singleton PostHog server client with shutdown hook - Type-safe event capture with TypeScript generics - React hooks for feature flags (boolean and multivariate) - Next.js App Router provider with pageview tracking - Reverse proxy configuration for ad blocker bypass ## Resources - [posthog-js Reference](https://posthog.com/docs/libraries/js) - [posthog-node Reference](https://posthog.com/docs/libraries/node) - [Next.js Integration](https://posthog.com/docs/libraries/next-js) - [React SDK](https://posthog.com/docs/libraries/react) ## Next Steps Apply patterns in `posthog-core-workflow-a` for real-world usage.