PostHog Event Tracking: Setup Guide | OpsBlu Docs

PostHog Event Tracking: Setup Guide

Set up PostHog event tracking for pageviews, clicks, forms, video, scroll depth, e-commerce funnels, and error capture

Overview

Event tracking is the cornerstone of product analytics. Every meaningful user interaction, business outcome, and system event should be captured and analyzed. This guide covers how to implement event tracking systematically across your product.

The goal: track everything that matters, ignore what doesn't, and maintain consistency as your product evolves.

Event Tracking Strategy

What to Track

User lifecycle events:

  • Signup, login, logout
  • Onboarding completion
  • Profile updates
  • Account deletion

Feature engagement:

  • Feature activation
  • Feature usage frequency
  • Feature-specific actions
  • Feature abandonment

Conversion funnels:

  • Add to cart
  • Checkout initiated
  • Payment completed
  • Trial started
  • Subscription created

Errors and issues:

  • API errors
  • Form validation failures
  • Payment failures
  • Feature unavailable

Content interaction:

  • Page views
  • Content viewed (articles, videos)
  • Search queries
  • Downloads

Implementation Patterns

Page View Tracking

Automatic (default):

posthog.init('YOUR_API_KEY', {
  capture_pageview: true  // Tracks pageviews automatically
});

Manual (SPAs):

// Initialize without auto-pageview
posthog.init('YOUR_API_KEY', {
  capture_pageview: false
});

// Track manually on route change
function trackPageView(pageName) {
  posthog.capture('$pageview', {
    $current_url: window.location.href,
    $pathname: window.location.pathname,
    page_title: document.title,
    page_name: pageName
  });
}

// React Router example
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';

function App() {
  const location = useLocation();

  useEffect(() => {
    trackPageView(location.pathname);
  }, [location]);

  return <YourApp />;
}

Click Tracking

Autocapture (recommended for exploration):

posthog.init('YOUR_API_KEY', {
  autocapture: true  // Automatically tracks all clicks
});

Manual (recommended for precision):

// Track specific button clicks
document.querySelector('#signup-button').addEventListener('click', () => {
  posthog.capture('signup_button_clicked', {
    button_location: 'hero',
    button_text: 'Start Free Trial',
    page_url: window.location.href
  });
});

// React example
function SignupButton() {
  const handleClick = () => {
    posthog.capture('signup_button_clicked', {
      button_location: 'hero',
      button_text: 'Start Free Trial'
    });
  };

  return <button Free Trial</button>;
}

Form Tracking

Form submission:

document.querySelector('#contact-form').addEventListener('submit', (e) => {
  e.preventDefault();

  const formData = new FormData(e.target);

  posthog.capture('contact_form_submitted', {
    form_name: 'contact',
    form_location: 'footer',
    has_message: formData.get('message').length > 0,
    message_length: formData.get('message').length
    // Don't send actual form content for privacy
  });

  // Submit form
  submitForm(formData);
});

Form field interactions:

// Track which fields users interact with
document.querySelectorAll('input, textarea, select').forEach(field => {
  field.addEventListener('focus', () => {
    posthog.capture('form_field_focused', {
      field_name: field.name,
      field_type: field.type,
      form_name: field.closest('form')?.name
    });
  });
});

Video Tracking

Video playback events:

const video = document.querySelector('video');

video.addEventListener('play', () => {
  posthog.capture('video_played', {
    video_id: video.dataset.videoId,
    video_title: video.dataset.title,
    video_duration_seconds: video.duration,
    playback_position_seconds: video.currentTime
  });
});

video.addEventListener('pause', () => {
  posthog.capture('video_paused', {
    video_id: video.dataset.videoId,
    playback_position_seconds: video.currentTime,
    watch_duration_seconds: video.currentTime
  });
});

video.addEventListener('ended', () => {
  posthog.capture('video_completed', {
    video_id: video.dataset.videoId,
    video_duration_seconds: video.duration,
    completion_percent: 100
  });
});

// Track viewing milestones
const milestones = [0.25, 0.50, 0.75];
video.addEventListener('timeupdate', () => {
  const progress = video.currentTime / video.duration;

  milestones.forEach(milestone => {
    if (progress >= milestone && !video.dataset[`milestone_${milestone}`]) {
      video.dataset[`milestone_${milestone}`] = 'true';

      posthog.capture('video_progress', {
        video_id: video.dataset.videoId,
        milestone_percent: milestone * 100
      });
    }
  });
});

Scroll Depth Tracking

let maxScrollDepth = 0;

window.addEventListener('scroll', () => {
  const scrollPercent = Math.round(
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  );

  if (scrollPercent > maxScrollDepth) {
    maxScrollDepth = scrollPercent;
  }
});

// Track on page exit
window.addEventListener('beforeunload', () => {
  posthog.capture('page_scroll_depth', {
    max_scroll_percent: maxScrollDepth,
    page_url: window.location.href,
    page_title: document.title
  });
});

// Or track milestones
const scrollMilestones = [25, 50, 75, 100];
let trackedMilestones = [];

window.addEventListener('scroll', () => {
  const scrollPercent = Math.round(
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  );

  scrollMilestones.forEach(milestone => {
    if (scrollPercent >= milestone && !trackedMilestones.includes(milestone)) {
      trackedMilestones.push(milestone);

      posthog.capture('scroll_milestone_reached', {
        milestone_percent: milestone,
        page_url: window.location.href
      });
    }
  });
});

E-commerce Tracking

Complete purchase funnel:

// 1. Product viewed
posthog.capture('product_viewed', {
  product_id: 'SKU_123',
  product_name: 'Wireless Headphones',
  product_category: 'Electronics > Audio',
  price: 199.99,
  currency: 'USD',
  in_stock: true
});

// 2. Added to cart
posthog.capture('product_added_to_cart', {
  product_id: 'SKU_123',
  product_name: 'Wireless Headphones',
  price: 199.99,
  quantity: 1,
  cart_total: 199.99,
  cart_item_count: 1,
  source: 'product_page'
});

// 3. Cart viewed
posthog.capture('cart_viewed', {
  cart_total: 199.99,
  cart_item_count: 1,
  product_ids: ['SKU_123']
});

// 4. Checkout started
posthog.capture('checkout_started', {
  cart_total: 199.99,
  cart_item_count: 1,
  checkout_step: 1
});

// 5. Shipping info entered
posthog.capture('checkout_step_completed', {
  checkout_step: 1,
  step_name: 'shipping_info',
  shipping_method: 'standard'
});

// 6. Payment info entered
posthog.capture('checkout_step_completed', {
  checkout_step: 2,
  step_name: 'payment_info',
  payment_method: 'credit_card'
});

// 7. Purchase completed
posthog.capture('purchase_completed', {
  order_id: 'ORDER_12345',
  revenue: 199.99,
  tax: 16.00,
  shipping: 10.00,
  total: 225.99,
  currency: 'USD',
  item_count: 1,
  payment_method: 'credit_card',
  is_first_purchase: true
});

Error Tracking

Client-side errors:

window.addEventListener('error', (event) => {
  posthog.capture('javascript_error', {
    error_message: event.message,
    error_filename: event.filename,
    error_line: event.lineno,
    error_column: event.colno,
    error_stack: event.error?.stack,
    page_url: window.location.href
  });
});

// Promise rejections
window.addEventListener('unhandledrejection', (event) => {
  posthog.capture('unhandled_promise_rejection', {
    error_message: event.reason?.message || String(event.reason),
    error_stack: event.reason?.stack,
    page_url: window.location.href
  });
});

API errors:

async function fetchData(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      posthog.capture('api_error', {
        endpoint: url,
        status_code: response.status,
        status_text: response.statusText,
        method: 'GET'
      });
    }

    return await response.json();
  } catch (error) {
    posthog.capture('api_error', {
      endpoint: url,
      error_message: error.message,
      error_type: 'network_error'
    });

    throw error;
  }
}

Search Tracking

document.querySelector('#search-form').addEventListener('submit', (e) => {
  e.preventDefault();

  const query = e.target.querySelector('input[name="q"]').value;

  posthog.capture('search_performed', {
    search_query: query,
    search_location: 'header',
    query_length: query.length
  });

  // Execute search
  performSearch(query);
});

// Track search results
function displaySearchResults(query, results) {
  posthog.capture('search_results_viewed', {
    search_query: query,
    result_count: results.length,
    has_results: results.length > 0
  });

  // Display results
}

// Track search result clicks
function trackSearchResultClick(query, result, position) {
  posthog.capture('search_result_clicked', {
    search_query: query,
    result_id: result.id,
    result_title: result.title,
    result_position: position
  });
}

Feature Flag Usage Tracking

// PostHog automatically tracks feature flag evaluations
// But you can also track when features are actually used

posthog.onFeatureFlags(() => {
  if (posthog.isFeatureEnabled('new-dashboard')) {
    // Show new dashboard
    showNewDashboard();

    // Track that user saw the feature
    posthog.capture('feature_flag_active', {
      flag_key: 'new-dashboard',
      flag_value: true
    });
  }
});

// Track feature-specific interactions
function useNewDashboardFeature(featureName) {
  posthog.capture('new_dashboard_feature_used', {
    feature_name: featureName,
    feature_flag: 'new-dashboard'
  });
}

Server-Side Event Tracking

Node.js API example:

const { PostHog } = require('posthog-node');
const posthog = new PostHog('YOUR_API_KEY');

// Track subscription creation
app.post('/api/subscriptions', async (req, res) => {
  const subscription = await createSubscription(req.user.id, req.body);

  posthog.capture({
    distinctId: req.user.id,
    event: 'subscription_created',
    properties: {
      plan_name: subscription.plan,
      plan_price: subscription.price,
      billing_cycle: subscription.billing_cycle,
      payment_method: subscription.paymentMethod,
      trial_days: subscription.trialDays
    }
  });

  res.json(subscription);
});

// Track background job completion
async function processDataExport(userId, exportId) {
  const startTime = Date.now();

  try {
    await generateExport(userId, exportId);

    posthog.capture({
      distinctId: userId,
      event: 'data_export_completed',
      properties: {
        export_id: exportId,
        duration_ms: Date.now() - startTime,
        status: 'success'
      }
    });
  } catch (error) {
    posthog.capture({
      distinctId: userId,
      event: 'data_export_failed',
      properties: {
        export_id: exportId,
        duration_ms: Date.now() - startTime,
        error_message: error.message,
        status: 'failed'
      }
    });

    throw error;
  }
}

// Always shutdown on process exit
process.on('SIGTERM', async () => {
  await posthog.shutdown();
  process.exit(0);
});

Testing Event Tracking

Manual testing:

// Enable debug mode
posthog.debug = true;

// Perform action that should trigger event
// Check browser console for:
// [PostHog] Event captured: your_event_name

// Check PostHog Activity within 30 seconds

Automated testing (Playwright):

const { test, expect } = require('@playwright/test');

test('tracks signup button click', async ({ page }) => {
  // Intercept PostHog requests
  const events = [];

  page.on('request', request => {
    if (request.url().includes('posthog.com')) {
      const postData = request.postDataJSON();
      if (postData?.batch) {
        events.push(...postData.batch);
      }
    }
  });

  // Navigate and click
  await page.goto('/');
  await page.click('#signup-button');

  // Wait for event to be sent
  await page.waitForTimeout(1000);

  // Verify event was captured
  const signupEvent = events.find(e => e.event === 'signup_button_clicked');
  expect(signupEvent).toBeDefined();
  expect(signupEvent.properties.button_location).toBe('hero');
});

Validation and Testing Procedures

Step 1: Verify Event Tracking Setup

Check PostHog initialization:

// verification-script.js
function verifyPostHogSetup() {
  const checks = {
    loaded: typeof posthog !== 'undefined',
    initialized: false,
    distinctId: null,
    config: null,
    autocapture: null
  };

  if (checks.loaded) {
    checks.initialized = !!posthog._init_session;
    checks.distinctId = posthog.get_distinct_id();
    checks.config = {
      api_host: posthog.config.api_host,
      autocapture: posthog.config.autocapture,
      capture_pageview: posthog.config.capture_pageview,
      loaded: posthog.config.loaded
    };
  }

  console.log('=== PostHog Setup Verification ===');
  console.table(checks);

  if (!checks.loaded) {
    console.error('PostHog not loaded');
    return false;
  }

  if (!checks.initialized) {
    console.warn('PostHog not fully initialized');
    return false;
  }

  console.log('PostHog setup verified');
  return true;
}

// Run on page load
verifyPostHogSetup();

Step 2: Test Event Capture

Create test harness:

// event-test-harness.js
class EventTestHarness {
  constructor() {
    this.capturedEvents = [];
    this.setupInterceptor();
  }

  setupInterceptor() {
    const originalCapture = posthog.capture;
    const self = this;

    posthog.capture = function(eventName, properties) {
      // Record event
      self.capturedEvents.push({
        event: eventName,
        properties: properties,
        timestamp: Date.now(),
        url: window.location.href
      });

      console.log(`Event captured: ${eventName}`, properties);

      // Call original
      return originalCapture.call(this, eventName, properties);
    };

    console.log('Event test harness initialized');
  }

  getEvents() {
    return this.capturedEvents;
  }

  getEventByName(eventName) {
    return this.capturedEvents.filter(e => e.event === eventName);
  }

  assertEventCaptured(eventName, minCount = 1) {
    const events = this.getEventByName(eventName);
    const captured = events.length >= minCount;

    console.assert(
      captured,
      `Expected at least ${minCount} "${eventName}" event(s), found ${events.length}`
    );

    return captured;
  }

  assertPropertyExists(eventName, propertyName) {
    const events = this.getEventByName(eventName);

    if (events.length === 0) {
      console.error(`No events found with name: ${eventName}`);
      return false;
    }

    const hasProperty = events.some(e =>
      e.properties && propertyName in e.properties
    );

    console.assert(
      hasProperty,
      `Expected property "${propertyName}" in event "${eventName}"`
    );

    return hasProperty;
  }

  reset() {
    this.capturedEvents = [];
    console.log('Event test harness reset');
  }

  printReport() {
    console.log('\n=== Event Tracking Report ===');
    console.log(`Total events captured: ${this.capturedEvents.length}\n`);

    const eventCounts = this.capturedEvents.reduce((acc, e) => {
      acc[e.event] = (acc[e.event] || 0) + 1;
      return acc;
    }, {});

    console.log('Events by type:');
    console.table(eventCounts);

    console.log('\nRecent events:');
    console.table(this.capturedEvents.slice(-10));
  }
}

// Usage
const testHarness = new EventTestHarness();

// Perform actions...
// Then verify
testHarness.assertEventCaptured('button_clicked');
testHarness.assertPropertyExists('button_clicked', 'button_name');
testHarness.printReport();

Step 3: Validate Event Properties

Property validation test:

// validate-event-properties.js
function validateEventProperties(eventName, properties, schema) {
  const errors = [];

  // Check required properties
  if (schema.required) {
    schema.required.forEach(prop => {
      if (!(prop in properties)) {
        errors.push(`Missing required property: ${prop}`);
      }
    });
  }

  // Check property types
  if (schema.types) {
    Object.entries(schema.types).forEach(([prop, expectedType]) => {
      if (prop in properties) {
        const actualType = typeof properties[prop];
        if (actualType !== expectedType) {
          errors.push(
            `Property "${prop}": expected ${expectedType}, got ${actualType}`
          );
        }
      }
    });
  }

  // Check for forbidden properties (PII)
  const forbiddenProps = ['password', 'ssn', 'credit_card', 'api_key'];
  Object.keys(properties).forEach(prop => {
    if (forbiddenProps.some(forbidden => prop.includes(forbidden))) {
      errors.push(`Forbidden property detected: ${prop} (potential PII)`);
    }
  });

  if (errors.length > 0) {
    console.error(`Validation errors for "${eventName}":`, errors);
    return false;
  }

  return true;
}

// Define schema
const buttonClickSchema = {
  required: ['button_name', 'button_location'],
  types: {
    button_name: 'string',
    button_location: 'string',
    button_text: 'string'
  }
};

// Validate before tracking
const eventProps = {
  button_name: 'signup_cta',
  button_location: 'hero',
  button_text: 'Start Free Trial'
};

if (validateEventProperties('button_clicked', eventProps, buttonClickSchema)) {
  posthog.capture('button_clicked', eventProps);
}

Step 4: Test Event Flow

User journey tracking test:

// test-event-flow.js
class EventFlowTester {
  constructor() {
    this.journey = [];
    this.startTime = Date.now();
  }

  trackStep(stepName, eventName, properties = {}) {
    const step = {
      stepName,
      eventName,
      properties,
      timestamp: Date.now(),
      timeSinceStart: Date.now() - this.startTime
    };

    this.journey.push(step);

    // Track in PostHog
    posthog.capture(eventName, {
      ...properties,
      journey_step: stepName,
      step_number: this.journey.length,
      time_since_start_ms: step.timeSinceStart
    });

    console.log(`Step ${this.journey.length}: ${stepName}`);
  }

  validateFlow(expectedSteps) {
    const actualSteps = this.journey.map(j => j.stepName);

    const isValid = expectedSteps.every((step, index) =>
      actualSteps[index] === step
    );

    if (!isValid) {
      console.error('Flow validation failed');
      console.log('Expected:', expectedSteps);
      console.log('Actual:', actualSteps);
      return false;
    }

    console.log('Event flow validated successfully');
    return true;
  }

  getReport() {
    return {
      totalSteps: this.journey.length,
      totalDuration: Date.now() - this.startTime,
      steps: this.journey,
      averageStepTime: (Date.now() - this.startTime) / this.journey.length
    };
  }

  printReport() {
    const report = this.getReport();

    console.log('\n=== Event Flow Report ===');
    console.log(`Total steps: ${report.totalSteps}`);
    console.log(`Total duration: ${report.totalDuration}ms`);
    console.log(`Average step time: ${report.averageStepTime.toFixed(2)}ms\n`);
    console.table(report.steps);
  }
}

// Usage - Test signup flow
const flowTest = new EventFlowTester();

flowTest.trackStep('Landing', 'page_viewed', { page: 'home' });
flowTest.trackStep('CTA Click', 'button_clicked', { button: 'signup' });
flowTest.trackStep('Form Start', 'signup_form_started', {});
flowTest.trackStep('Form Submit', 'signup_form_submitted', {});
flowTest.trackStep('Confirmation', 'signup_completed', {});

// Validate expected flow
flowTest.validateFlow([
  'Landing',
  'CTA Click',
  'Form Start',
  'Form Submit',
  'Confirmation'
]);

flowTest.printReport();

Step 5: Browser Testing

Cross-browser event tracking test:

// browser-compatibility-test.js
async function testBrowserCompatibility() {
  const results = {
    browser: navigator.userAgent,
    tests: []
  };

  // Test 1: PostHog loaded
  results.tests.push({
    test: 'PostHog SDK loaded',
    passed: typeof posthog !== 'undefined',
    details: typeof posthog !== 'undefined' ? 'Loaded' : 'Not found'
  });

  // Test 2: Event capture works
  try {
    posthog.capture('browser_test_event', { test: true });
    results.tests.push({
      test: 'Event capture',
      passed: true,
      details: 'Successfully captured test event'
    });
  } catch (e) {
    results.tests.push({
      test: 'Event capture',
      passed: false,
      details: e.message
    });
  }

  // Test 3: Local storage available
  results.tests.push({
    test: 'LocalStorage available',
    passed: typeof localStorage !== 'undefined',
    details: typeof localStorage !== 'undefined' ? 'Available' : 'Not available'
  });

  // Test 4: Cookies enabled
  results.tests.push({
    test: 'Cookies enabled',
    passed: navigator.cookieEnabled,
    details: navigator.cookieEnabled ? 'Enabled' : 'Disabled'
  });

  // Test 5: Network request works
  const networkTest = await new Promise(resolve => {
    const img = new Image();
    const timeout = setTimeout(() => resolve(false), 5000);

    img.onload = () => {
      clearTimeout(timeout);
      resolve(true);
    };

    img.onerror = () => {
      clearTimeout(timeout);
      resolve(false);
    };

    img.src = 'https://app.posthog.com/decide/?v=3';
  });

  results.tests.push({
    test: 'Network connectivity',
    passed: networkTest,
    details: networkTest ? 'PostHog reachable' : 'Connection failed'
  });

  console.log('\n=== Browser Compatibility Test ===');
  console.log(`Browser: ${results.browser}\n`);
  console.table(results.tests);

  const allPassed = results.tests.every(t => t.passed);
  console.log(allPassed ? 'All tests passed' : 'Some tests failed');

  return results;
}

// Run test
testBrowserCompatibility();

Step 6: Production Monitoring

Real-time event monitoring:

// production-event-monitor.js
class ProductionEventMonitor {
  constructor() {
    this.metrics = {
      totalEvents: 0,
      eventTypes: {},
      errors: 0,
      startTime: Date.now()
    };

    this.setupMonitoring();
  }

  setupMonitoring() {
    const originalCapture = posthog.capture;
    const self = this;

    posthog.capture = function(eventName, properties) {
      // Update metrics
      self.metrics.totalEvents++;
      self.metrics.eventTypes[eventName] =
        (self.metrics.eventTypes[eventName] || 0) + 1;

      // Validate event
      if (!eventName || typeof eventName !== 'string') {
        self.metrics.errors++;
        console.error('Invalid event name:', eventName);
      }

      // Check for common issues
      if (properties) {
        // Check for undefined values
        Object.entries(properties).forEach(([key, value]) => {
          if (value === undefined) {
            console.warn(`Undefined value for property "${key}" in event "${eventName}"`);
          }
        });
      }

      // Call original
      return originalCapture.call(this, eventName, properties);
    };

    // Log metrics periodically
    setInterval(() => this.logMetrics(), 60000); // Every minute
  }

  getMetrics() {
    const uptime = Date.now() - this.metrics.startTime;

    return {
      totalEvents: this.metrics.totalEvents,
      eventTypes: this.metrics.eventTypes,
      errors: this.metrics.errors,
      eventsPerMinute: (this.metrics.totalEvents / (uptime / 60000)).toFixed(2),
      uptimeSeconds: Math.floor(uptime / 1000)
    };
  }

  logMetrics() {
    const metrics = this.getMetrics();

    console.log('=== Event Monitoring Metrics ===');
    console.log(`Total events: ${metrics.totalEvents}`);
    console.log(`Events/minute: ${metrics.eventsPerMinute}`);
    console.log(`Errors: ${metrics.errors}`);
    console.log(`Uptime: ${metrics.uptimeSeconds}s\n`);
    console.log('Event types:');
    console.table(metrics.eventTypes);
  }

  reset() {
    this.metrics = {
      totalEvents: 0,
      eventTypes: {},
      errors: 0,
      startTime: Date.now()
    };
  }
}

// Initialize in production
if (typeof posthog !== 'undefined') {
  const monitor = new ProductionEventMonitor();

  // Expose globally for debugging
  window.posthogMonitor = monitor;
}

Troubleshooting

Common Issues and Solutions

Problem Symptoms Root Cause Solution
Events not appearing in PostHog Tracked events don't show in dashboard Ad blocker enabled, network issues, or wrong API key Check browser console for errors; disable ad blockers; verify API key; check network tab
Duplicate events Same event appears multiple times Multiple event listeners or re-renders Use event delegation; debounce handlers; check React useEffect dependencies
Events missing properties Properties not appearing in PostHog Properties undefined at capture time Validate properties before tracking; check for async timing issues
Autocapture not working No automatic click events Autocapture disabled or elements not captured Enable autocapture: true in config; check element attributes
Page views not tracked $pageview events missing Manual pageview tracking in SPA without implementation Call posthog.capture('$pageview') on route changes
Events sent after user leaves Events lost on navigation No time for request to complete Use posthog.flush() before navigation; use sendBeacon
Form submissions not tracked Form events missing Event.preventDefault() before tracking Track event first, then prevent default and submit
Video events inconsistent Video tracking sporadic Event listeners not properly attached Ensure video element loaded before adding listeners
Scroll tracking inaccurate Wrong scroll depth values Calculation errors or timing issues Use debounced scroll handler; verify calculation logic
Error tracking missing context Error events lack useful info Insufficient error details captured Include stack trace, user context, and page state
Search tracking incomplete Search queries not captured Form submission handler missing Track on both input change and form submit
Feature flag events delayed Flag evaluation events late Waiting for flags to load Use posthog.onFeatureFlags() callback
Server-side events not attributed Server events show as different users Distinct ID not passed from client Send distinct_id in API requests from client
Events blocked by CSP PostHog requests blocked Content Security Policy restrictions Add PostHog domain to CSP whitelist
Mobile events inconsistent Tracking unreliable on mobile Network connectivity issues Implement retry logic; use local queue
Debug mode not showing events Console doesn't show event logs Debug mode not enabled Set posthog.debug() or debug: true in config
Properties have wrong types Numbers as strings, etc. Type coercion issues Explicitly cast types before tracking
Events rate limited Some events not processed Exceeding PostHog rate limits Reduce event volume; batch events; upgrade plan
Timestamp issues Events show wrong time Timezone or format problems Use ISO 8601 UTC timestamps
Anonymous users not merging Identified users show as new Missing alias call Use posthog.alias() when identifying users
React events stale data Old state values in events Closure capturing stale state Use refs or functional updates for current values

Debug Commands

Enable debug mode:

// Method 1: In initialization
posthog.init('YOUR_API_KEY', {
  debug: true
});

// Method 2: After initialization
posthog.debug();

// Method 3: In browser console
posthog.debug(true);

Check event queue:

// View pending events
console.log('Pending events:', posthog._events_queue);

// Force flush
posthog.flush();

Inspect configuration:

// View current config
console.log('PostHog config:', posthog.config);

// Check specific settings
console.log('Autocapture:', posthog.config.autocapture);
console.log('API host:', posthog.config.api_host);
console.log('Capture pageview:', posthog.config.capture_pageview);

Test network connectivity:

// Test if PostHog is reachable
fetch('https://app.posthog.com/decide/?v=3')
  .then(res => console.log('PostHog reachable:', res.ok))
  .catch(err => console.error('PostHog unreachable:', err));

Validation Checklist

Before deploying event tracking to production:

  • PostHog SDK loaded and initialized correctly
  • API key validated and working
  • All critical events defined and documented
  • Event naming convention followed (snake_case)
  • Required properties validated before tracking
  • No PII tracked in event properties
  • Page view tracking configured (auto or manual)
  • Form submission tracking implemented
  • Error tracking setup and tested
  • Feature-specific events implemented
  • Conversion funnel events tracked
  • Event flows tested end-to-end
  • Cross-browser compatibility verified
  • Mobile responsiveness tested
  • Network failure handling implemented
  • Debug mode tested in development
  • Events appearing in PostHog dashboard
  • User identification working correctly
  • Anonymous to identified user flow tested
  • Production monitoring implemented

Best Practices

Do:

  • Track events at the moment they happen, not after
  • Include relevant context in properties
  • Use consistent naming conventions
  • Track both success and failure states
  • Test event tracking before production deploy
  • Document what each event means

Don't:

  • Track PII (emails, names) in event properties without hashing
  • Create events for every possible interaction (be selective)
  • Use events for debugging (use proper logging instead)
  • Track the same action multiple times
  • Forget to track error states

Common Patterns

Track with context:

function trackWithContext(event, properties = {}) {
  posthog.capture(event, {
    ...properties,
    page_url: window.location.href,
    page_title: document.title,
    timestamp: new Date().toISOString()
  });
}

Debounced tracking:

const debounce = (fn, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

// Track search input changes (debounced)
const trackSearchInput = debounce((query) => {
  posthog.capture('search_input_changed', {
    query_length: query.length,
    has_query: query.length > 0
  });
}, 500);

searchInput.addEventListener('input', (e) => {
  trackSearchInput(e.target.value);
});

Need help? Check the troubleshooting guide or PostHog event tracking docs.