posthog-sdk-patterns

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".

claude-codecodexopenclaw
3 Tools
posthog-pack Plugin
saas packs Category

Allowed Tools

ReadWriteEdit

Provided by Plugin

posthog-pack

Claude Code skill pack for PostHog (24 skills)

saas packs v1.0.0
View Plugin

Installation

This skill is included in the posthog-pack plugin:

/plugin install posthog-pack@claude-code-plugins-plus

Click to copy

Instructions

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


// 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.

Ready to use posthog-pack?