or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
npmpkg:npm/posthog-js@1.335.x

docs

index.md
tile.json

tessl/npm-posthog-js

tessl install tessl/npm-posthog-js@1.335.0

PostHog Browser JS Library is a comprehensive browser analytics and feature management SDK that enables developers to capture user events, track product analytics, manage feature flags, record session replays, and implement feedback mechanisms like surveys and conversations in web applications.

best-practices.mddocs/guides/

PostHog.js Best Practices

Comprehensive best practices for using PostHog.js in production applications, covering initialization, event tracking, feature flags, session recording, privacy, and performance optimization.

Initialization Best Practices

Environment-Specific Configuration

// Create environment-aware configuration
const getConfig = (): Partial<PostHogConfig> => {
    const baseConfig: Partial<PostHogConfig> = {
        api_host: process.env.POSTHOG_API_HOST || 'https://us.i.posthog.com',
        autocapture: true,
        capture_pageview: 'history_change',
        person_profiles: 'identified_only'
    };
    
    if (process.env.NODE_ENV === 'production') {
        return {
            ...baseConfig,
            debug: false,
            session_recording: {
                enabled: true,
                maskAllInputs: true,
                consoleLogRecordingEnabled: false
            },
            advanced_disable_toolbar_metrics: true
        };
    }
    
    return {
        ...baseConfig,
        debug: true,
        session_recording: {
            enabled: true,
            consoleLogRecordingEnabled: true
        }
    };
};

posthog.init(process.env.POSTHOG_API_KEY!, getConfig());

Error-Resilient Initialization

// Handle initialization failures gracefully
function initPostHogSafely() {
    try {
        posthog.init(process.env.POSTHOG_API_KEY!, {
            loaded: (posthog) => {
                console.log('PostHog initialized successfully');
            },
            on_request_error: (response) => {
                // Log but don't crash
                console.error('PostHog error:', response.statusCode);
            }
        });
    } catch (error) {
        console.error('PostHog initialization failed:', error);
        // Application continues without analytics
    }
}

Event Tracking Best Practices

Consistent Event Naming

// Use a consistent naming convention
const EventNames = {
    // User actions (verb_noun)
    BUTTON_CLICKED: 'button_clicked',
    FORM_SUBMITTED: 'form_submitted',
    SEARCH_PERFORMED: 'search_performed',
    
    // Page views
    PAGE_VIEWED: 'page_viewed',
    
    // Business events
    PURCHASE_COMPLETED: 'purchase_completed',
    SUBSCRIPTION_STARTED: 'subscription_started'
} as const;

// Type-safe event tracking
posthog.capture(EventNames.BUTTON_CLICKED, {
    button_id: 'signup',
    button_location: 'header'
});

Rich Event Context

// Include comprehensive context
function trackUserAction(action: string, context: Record<string, any> = {}) {
    posthog.capture(action, {
        ...context,
        // Automatic context
        page_path: window.location.pathname,
        page_url: window.location.href,
        referrer: document.referrer,
        viewport_height: window.innerHeight,
        viewport_width: window.innerWidth,
        timestamp: new Date().toISOString()
    });
}

Avoid Over-Tracking

// ❌ Bad: Track every mouse move
document.addEventListener('mousemove', (e) => {
    posthog.capture('mouse_moved', { x: e.clientX, y: e.clientY });
});

// ✅ Good: Track meaningful interactions
document.querySelectorAll('.cta-button').forEach(button => {
    button.addEventListener('click', () => {
        posthog.capture('cta_clicked', {
            button_text: button.textContent,
            button_location: button.dataset.location
        });
    });
});

Feature Flag Best Practices

Defensive Flag Checking

// Always provide defaults
function getFeature(flagKey: string, defaultValue: boolean = false): boolean {
    const value = posthog.isFeatureEnabled(flagKey);
    return value !== undefined ? value : defaultValue;
}

// Wait for flags before critical decisions
async function initializeCriticalFeature() {
    // Wait for flags to load
    if (!posthog.featureFlags.hasLoadedFlags) {
        await new Promise(resolve => {
            posthog.onFeatureFlags(() => resolve(undefined))();
        });
    }
    
    if (getFeature('critical-feature', false)) {
        await loadCriticalFeature();
    }
}

Flag-Driven Configuration

// Use flags for dynamic configuration
interface AppConfig {
    maxRetries: number;
    timeout: number;
    endpoint: string;
}

function getAppConfig(): AppConfig {
    const flagPayload = posthog.getFeatureFlagPayload('app-config') as Partial<AppConfig>;
    
    const defaults: AppConfig = {
        maxRetries: 3,
        timeout: 5000,
        endpoint: '/api/v1'
    };
    
    return {
        ...defaults,
        ...flagPayload
    };
}

Experiment Tracking

// Track experiment exposure
function trackExperimentExposure(experimentKey: string) {
    const variant = posthog.getFeatureFlag(experimentKey);
    
    if (variant) {
        posthog.capture('experiment_exposure', {
            experiment_key: experimentKey,
            variant: variant,
            session_id: posthog.get_session_id(),
            exposure_time: Date.now()
        });
    }
}

Session Recording Best Practices

Privacy-First Recording

// Configure with privacy in mind
posthog.init('token', {
    session_recording: {
        enabled: true,
        // Mask sensitive inputs
        maskAllInputs: true,
        // Block sensitive sections
        blockSelector: '.sensitive-data, .payment-info, .ssn',
        // Mask specific text
        maskTextSelector: '.email, .phone, .address',
        // Control network capture
        networkPayloadCapture: {
            recordHeaders: false,
            recordBody: ['application/json']  // Only JSON
        }
    }
});

Conditional Recording

// Only record important user flows
const recordedPaths = ['/checkout', '/onboarding', '/dashboard'];

router.on('route', (route) => {
    const shouldRecord = recordedPaths.some(path => 
        route.path.startsWith(path)
    );
    
    if (shouldRecord && !posthog.sessionRecordingStarted()) {
        posthog.startSessionRecording();
    } else if (!shouldRecord && posthog.sessionRecordingStarted()) {
        posthog.stopSessionRecording();
    }
});

Link Recordings to Errors

// Always link session replays to errors
window.addEventListener('error', (event) => {
    posthog.captureException(event.error, {
        session_replay_url: posthog.get_session_replay_url({
            withTimestamp: true,
            timestampLookBack: 30
        }),
        session_id: posthog.get_session_id(),
        page_url: window.location.href,
        user_agent: navigator.userAgent
    });
});

User Identification Best Practices

Progressive Identification

// Identify progressively as you learn more
class UserIdentification {
    identifyAnonymous() {
        // Let PostHog create anonymous ID
        posthog.capture('app_opened');
    }
    
    identifyBasic(userId: string, email: string) {
        // Basic identification after signup
        posthog.alias(userId);
        posthog.identify(userId, {
            email: email
        }, {
            signup_date: new Date().toISOString()
        });
    }
    
    enrichProfile(properties: Record<string, any>) {
        // Add more properties as they become available
        posthog.setPersonProperties(properties);
    }
    
    resetOnLogout() {
        // Always reset on logout
        posthog.reset();
    }
}

Handle Multiple Identity Sources

// Handle OAuth and multiple login methods
async function handleLogin(provider: 'google' | 'github' | 'email', credentials: any) {
    const user = await authenticate(provider, credentials);
    
    // Create alias from anonymous to identified
    posthog.alias(user.id);
    
    // Identify with provider info
    posthog.identify(user.id, {
        email: user.email,
        name: user.name,
        auth_provider: provider,
        verified: user.verified
    }, {
        first_login_provider: provider,
        first_login_date: new Date().toISOString()
    });
    
    // Reload flags for personalized experience
    posthog.reloadFeatureFlags();
}

Privacy & Compliance Best Practices

GDPR-Compliant Implementation

// Implement opt-in by default for GDPR
posthog.init('token', {
    opt_out_capturing_by_default: true,
    opt_out_persistence_by_default: true,
    respect_dnt: true
});

// Show consent banner
function showConsentBanner() {
    // Display UI
    const banner = createConsentBanner({
        onAccept: () => {
            posthog.opt_in_capturing({
                captureEventName: 'gdpr_consent_granted',
                captureProperties: {
                    consent_version: '2.0',
                    consent_date: new Date().toISOString()
                }
            });
            
            // Enable features after consent
            posthog.startSessionRecording();
        },
        onReject: () => {
            posthog.opt_out_capturing();
        }
    });
}

// Check consent on load
if (posthog.get_explicit_consent_status() === 'pending') {
    showConsentBanner();
}

Data Minimization

// Only capture necessary data
posthog.init('token', {
    // Deny list for sensitive properties
    property_denylist: [
        'password',
        'credit_card',
        'ssn',
        'api_key',
        'access_token',
        'secret'
    ],
    // Mask personal data
    mask_personal_data_properties: true,
    custom_personal_data_properties: [
        'email',
        'phone',
        'address',
        'ip_address'
    ]
});

Performance Best Practices

Optimize Bundle Size

// Lazy load PostHog for better initial page load
async function loadPostHogAsync() {
    const { default: posthog } = await import('posthog-js');
    
    posthog.init('token', {
        loaded: (ph) => {
            console.log('PostHog loaded asynchronously');
        }
    });
    
    return posthog;
}

// Load after critical content
window.addEventListener('load', () => {
    loadPostHogAsync();
});

Batch Configuration for High Traffic

// Optimize for high-traffic pages
posthog.init('token', {
    request_batching: true,
    batch_size: 100,          // Larger batches
    batch_max_wait_ms: 10000, // Less frequent sends
    disable_compression: false // Keep compression on
});

Selective Feature Loading

// Only load features you need
posthog.init('token', {
    // Disable unused features
    disable_surveys: true,
    disable_product_tours: true,
    disable_conversations: true,
    
    // Disable expensive features on low-end devices
    session_recording: {
        enabled: !isLowEndDevice(),
        recordCanvas: false  // Canvas recording is expensive
    }
});

function isLowEndDevice(): boolean {
    return (
        navigator.deviceMemory && navigator.deviceMemory < 4 ||
        navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4
    );
}

Error Handling Best Practices

Comprehensive Error Tracking

// Set up global error handlers
class ErrorTracker {
    initialize() {
        // Uncaught errors
        window.addEventListener('error', (event) => {
            this.trackError(event.error, {
                type: 'uncaught_error',
                filename: event.filename,
                lineno: event.lineno,
                colno: event.colno
            });
        });
        
        // Unhandled promise rejections
        window.addEventListener('unhandledrejection', (event) => {
            this.trackError(event.reason, {
                type: 'unhandled_rejection',
                promise: String(event.promise)
            });
        });
        
        // Enable PostHog exception autocapture
        posthog.startExceptionAutocapture({
            capture_unhandled_errors: true,
            capture_unhandled_rejections: true,
            capture_console_errors: false  // We handle console separately
        });
    }
    
    trackError(error: Error, context: Record<string, any>) {
        // Deduplicate errors
        const fingerprint = this.getErrorFingerprint(error);
        if (this.recentErrors.has(fingerprint)) {
            return;
        }
        this.recentErrors.add(fingerprint);
        
        posthog.captureException(error, {
            ...context,
            session_replay_url: posthog.get_session_replay_url({
                withTimestamp: true,
                timestampLookBack: 30
            }),
            user_id: posthog.get_distinct_id(),
            session_id: posthog.get_session_id()
        });
    }
    
    private recentErrors = new Set<string>();
    
    private getErrorFingerprint(error: Error): string {
        return `${error.name}:${error.message}:${error.stack?.split('\n')[1]}`;
    }
}

Testing Best Practices

Mock PostHog in Tests

// Create PostHog mock for testing
const mockPostHog = {
    init: jest.fn(),
    capture: jest.fn(),
    identify: jest.fn(),
    isFeatureEnabled: jest.fn(() => false),
    getFeatureFlag: jest.fn(() => undefined),
    onFeatureFlags: jest.fn((cb) => {
        cb({}, {});
        return () => {};
    })
};

// Use in tests
beforeEach(() => {
    (window as any).posthog = mockPostHog;
});

afterEach(() => {
    jest.clearAllMocks();
});

// Test with feature flags
it('shows new feature when flag is enabled', () => {
    mockPostHog.isFeatureEnabled.mockReturnValue(true);
    
    render(<Component />);
    
    expect(screen.getByText('New Feature')).toBeInTheDocument();
});

E2E Testing with PostHog

// Disable PostHog in E2E tests or use test environment
if (process.env.E2E_TEST) {
    posthog.init('token', {
        api_host: 'https://test.posthog.com',
        // Or disable completely
        opt_out_capturing_by_default: true
    });
}

// Override flags for specific test scenarios
if (process.env.TEST_VARIANT === 'control') {
    posthog.featureFlags.overrideFeatureFlags({
        'experiment': 'control'
    });
}

Monitoring Best Practices

Health Checks

// Monitor PostHog health
class PostHogMonitor {
    checkHealth() {
        return {
            initialized: !!posthog,
            capturing: posthog.is_capturing(),
            flagsLoaded: posthog.featureFlags.hasLoadedFlags,
            sessionRecording: posthog.sessionRecordingStarted(),
            config: {
                apiHost: posthog.config.api_host,
                persistence: posthog.config.persistence
            }
        };
    }
    
    logHealthStatus() {
        const health = this.checkHealth();
        console.log('PostHog Health:', health);
        
        if (!health.capturing) {
            console.warn('PostHog not capturing events');
        }
        
        if (!health.flagsLoaded) {
            console.warn('Feature flags not loaded');
        }
    }
}

// Check health on critical paths
async function initializeApp() {
    const monitor = new PostHogMonitor();
    
    // Wait for PostHog to be ready
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    monitor.logHealthStatus();
    
    // Continue with app initialization
}

TypeScript Best Practices

Type-Safe Events and Properties

// Define event types
interface EventMap {
    'button_clicked': {
        button_id: string;
        button_text: string;
        location: string;
    };
    'page_viewed': {
        page_path: string;
        page_title: string;
    };
    'purchase_completed': {
        order_id: string;
        total: number;
        currency: string;
        items: Array<{
            id: string;
            quantity: number;
        }>;
    };
}

// Type-safe capture function
function captureEvent<K extends keyof EventMap>(
    event: K,
    properties: EventMap[K]
): void {
    posthog.capture(event, properties);
}

// Usage with type checking
captureEvent('button_clicked', {
    button_id: 'signup',
    button_text: 'Sign Up',
    location: 'header'
});

// ❌ Type error: missing required properties
// captureEvent('button_clicked', { button_id: 'signup' });

Type-Safe Feature Flags

// Define flag types
interface FeatureFlags {
    'new-dashboard': boolean;
    'button-color': 'blue' | 'green' | 'red';
    'experiment-variant': 'control' | 'variant-a' | 'variant-b';
}

// Type-safe flag getter
function getFeatureFlag<K extends keyof FeatureFlags>(
    key: K
): FeatureFlags[K] | undefined {
    return posthog.getFeatureFlag(key) as FeatureFlags[K] | undefined;
}

// Usage with type inference
const buttonColor = getFeatureFlag('button-color');  // 'blue' | 'green' | 'red' | undefined
const isDashboardNew = getFeatureFlag('new-dashboard');  // boolean | undefined

Production Deployment Checklist

  • Environment-specific configuration for dev/staging/prod
  • GDPR-compliant consent mechanism implemented
  • Session recording privacy settings configured (mask inputs, block selectors)
  • Error tracking with session replay links
  • Feature flags loaded before critical decisions
  • TypeScript types defined for events and flags
  • Performance optimizations applied (batching, selective features)
  • Monitoring and health checks in place
  • Testing strategy for analytics (mocks, E2E)
  • Privacy policy updated to reflect PostHog usage
  • Data retention policies configured in PostHog dashboard
  • Debug mode disabled in production
  • CSP headers allow PostHog domains
  • CORS configured for api_host
  • Ad blocker detection and fallbacks implemented

Common Anti-Patterns to Avoid

Don't Block Initialization

// ❌ Bad: Wait for PostHog before rendering
async function initApp() {
    await initializePostHog();  // Don't block on this
    render(<App />);
}

// ✅ Good: Initialize in parallel
function initApp() {
    initializePostHog();  // Fire and forget
    render(<App />);
}

Don't Check Flags Synchronously on Load

// ❌ Bad: Check flag immediately
function Component() {
    const showNew = posthog.isFeatureEnabled('new-ui');  // May be undefined!
    return showNew ? <NewUI /> : <OldUI />;
}

// ✅ Good: Handle loading state
function Component() {
    const [showNew, setShowNew] = useState<boolean | null>(null);
    
    useEffect(() => {
        posthog.onFeatureFlags((flags) => {
            setShowNew(flags['new-ui'] || false);
        });
    }, []);
    
    if (showNew === null) return <Loading />;
    return showNew ? <NewUI /> : <OldUI />;
}

Don't Identify Without Aliasing

// ❌ Bad: Identify without alias (loses anonymous events)
function onSignup(userId: string) {
    posthog.identify(userId);
}

// ✅ Good: Alias first to link anonymous events
function onSignup(userId: string) {
    posthog.alias(userId);  // Link anonymous ID to user ID
    posthog.identify(userId, {
        signup_date: new Date().toISOString()
    });
}

Don't Ignore Errors

// ❌ Bad: Silently fail
try {
    posthog.capture('event');
} catch (error) {
    // Ignore
}

// ✅ Good: Log and monitor
try {
    posthog.capture('event');
} catch (error) {
    console.error('Analytics error:', error);
    // Track to error monitoring service
    errorMonitor.captureException(error);
}

Additional Resources

  • Initialization - Complete configuration reference
  • Events - Event tracking guide
  • Feature Flags - Feature flag implementation
  • Session Recording - Recording setup and privacy
  • Privacy & Consent - GDPR and privacy compliance
  • Error Tracking - Exception handling
  • Advanced Features - LLM analytics, groups, and more