Apply production-ready PostHog SDK patterns for TypeScript and Python. Use when implementing PostHog integrations, refactoring SDK usage, or establishing team coding standards for PostHog. Trigger...
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.
posthog-js and/or posthog-node installedUse Read to inspect the relevant configuration and implementation before proposing changes. Use Write only for a new, explicitly requested artifact inside the target project. Use Edit for minimal changes to existing project files after the evidence pass.
// 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_FEATURE_FLAGS_SECURE_API_KEY,
flushAt: 20,
flushInterval: 10000,
});
}
return client;
}
// Graceful shutdown hook
process.on('SIGTERM', async () => {
if (client) await client.shutdown();
process.exit(0);
});
// 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,
});
// 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 />;
}
// 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>;
// }
// 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/flags', destination: 'https://us.i.posthog.com/flags' },
];
},
};
// Then in your posthog.init:
// posthog.init('phc_...', { api_host: '/ingest' });
# 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_FEATURE_FLAGS_SECURE_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
| 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 |
For a Next.js application, create separate client and server adapters, keep private credentials server-only, type business events, handle pageviews deliberately, and test capture plus flag fallbacks. Verify current framework guidance before copying any proxy or initialization pattern.
See official PostHog references for current authority and verification boundaries.
Apply patterns in posthog-core-workflow-a for real-world usage.