Claude-code-plugins-plus posthog-sdk-patterns
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/posthog-pack/skills/posthog-sdk-patterns" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-posthog-sdk-patterns && rm -rf "$T"
manifest:
plugins/saas-packs/posthog-pack/skills/posthog-sdk-patterns/SKILL.mdsource content
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
and/orposthog-js
installedposthog-node- TypeScript project with strict mode
- React/Next.js (for frontend patterns)
Instructions
Step 1: Singleton Server Client
// 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
// 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<K extends keyof EventMap>( 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
// 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<boolean | undefined>(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<string | undefined>(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 <Skeleton />; // Loading return newCheckout ? <NewCheckout /> : <LegacyCheckout />; }
Step 4: Next.js App Router Provider
// 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 ( <PostHogProvider client={posthog}> <Suspense fallback={null}> <PostHogPageView /> </Suspense> {children} </PostHogProvider> ); } // app/layout.tsx // import { PHProvider } from './providers'; // export default function RootLayout({ children }) { // return <html><body><PHProvider>{children}</PHProvider></body></html>; // }
Step 5: Reverse Proxy for Ad Blocker Bypass
// 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
# 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
Next Steps
Apply patterns in
posthog-core-workflow-a for real-world usage.