Posthog sdk patterns
425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill posthog-sdk-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
'Production-ready PostHog SDK patterns: singleton client, typed events,
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
7.6 KB, as published. Nobody here has run it
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-jsand/orposthog-nodeinstalled- 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.