PostHog Event Tracking: Autocapture, Custom Events, | OpsBlu Docs

PostHog Event Tracking: Autocapture, Custom Events,

How to set up PostHog event tracking with autocapture, custom events, group analytics, and user identification.

Overview

PostHog is built around events. Every user interaction, every API call, every conversion, it's all captured as discrete events that build a complete picture of how users move through your product. Unlike traditional web analytics that focus on pageviews, PostHog treats everything as an event, giving you granular control over what you track and how you analyze it.

The platform offers two complementary approaches: autocapture, which tracks interactions automatically without code changes, and custom events, which let you define exactly what matters to your product. Together, these approaches give you both speed and precision.

PostHog's event model is flexible and forgiving. There are no rigid schemas to set up before tracking. You can start capturing events immediately, then refine your taxonomy as you learn what matters. This makes PostHog ideal for fast-moving teams that need analytics now, not after weeks of planning.

Event Model

How PostHog Events Work

Every event in PostHog consists of:

  1. Event Name: The action that occurred (e.g., "button clicked", "purchase completed")
  2. Properties: Key-value pairs providing context (e.g., {"button_text": "Sign Up", "page_url": "/pricing"})
  3. Distinct ID: Unique identifier for the user (anonymous ID before login, user ID after)
  4. Timestamp: When the event occurred (automatically captured)
  5. Person Properties: Attributes about the user (e.g., {"plan": "Pro", "signup_date": "2024-01-15"})

Event Structure:

{
  "event": "purchase completed",
  "properties": {
    "order_id": "ORDER_12345",
    "revenue": 99.99,
    "currency": "USD",
    "product_count": 3,
    "$current_url": "https://example.com/checkout/success",
    "$browser": "Chrome",
    "$device_type": "Desktop"
  },
  "distinct_id": "user_abc123",
  "timestamp": "2024-01-20T14:30:00Z"
}

Key Concepts

  • Event: A discrete action or occurrence
  • Properties: Context about the event (custom) and metadata (automatic)
  • Distinct ID: Identifier that connects events to users
  • Person Properties: Attributes that describe the user across all events
  • Session: A period of continuous activity (automatically tracked)

Property Naming Conventions

PostHog uses a simple convention for property names:

  • Custom properties: Use lowercase with underscores (e.g., product_id, subscription_tier)
  • PostHog properties: Prefix with $ (e.g., $current_url, $browser, $device_type)
  • Feature flags: Prefix with $feature/ (e.g., $feature/new-dashboard)

Autocapture

What Gets Captured Automatically

PostHog's autocapture is one of its killer features. Once you add the PostHog snippet to your site, it automatically tracks:

  • Clicks: Every click on buttons, links, and interactive elements
  • Form submissions: When users submit forms
  • Page views: Navigation between pages (including SPA route changes)
  • Page exits: When users leave your site
  • Rage clicks: When users frantically click the same element
  • Dead clicks: Clicks that don't trigger any action

Each autocaptured event includes rich context:

// Autocaptured click event
{
  "event": "$autocapture",
  "properties": {
    "$event_type": "click",
    "$elements": [
      {
        "tag_name": "button",
        "text": "Sign Up Now",
        "classes": ["btn", "btn-primary"],
        "attr__id": "signup-cta",
        "nth_child": 1,
        "nth_of_type": 1
      }
    ],
    "$current_url": "https://example.com/pricing",
    "$host": "example.com",
    "$pathname": "/pricing"
  }
}

Enabling Autocapture

JavaScript/TypeScript:

posthog.init('YOUR_PROJECT_API_KEY', {
  api_host: 'https://app.posthog.com',
  autocapture: true  // Enabled by default
});

React:

import posthog from 'posthog-js';

posthog.init('YOUR_PROJECT_API_KEY', {
  api_host: 'https://app.posthog.com',
  autocapture: true,
  capture_pageview: true,  // Track route changes in SPAs
  capture_pageleave: true  // Track when users leave pages
});

Configuring Autocapture

You can fine-tune what gets autocaptured:

posthog.init('YOUR_PROJECT_API_KEY', {
  autocapture: {
    // Don't capture clicks on elements with these classes
    css_selector_blacklist: ['.private-data', '.ph-no-capture'],

    // Only capture clicks on elements matching this selector
    css_selector_whitelist: ['.trackable'],

    // Don't capture form submissions
    capture_forms: false,

    // Don't capture dead clicks
    capture_dead_clicks: false,

    // Don't capture rage clicks
    capture_rage_clicks: false,

    // Capture only first click on an element per session
    capture_copied_text: true
  }
});

Disabling autocapture on specific elements:

<!-- Add ph-no-capture class to any element -->
<button class="ph-no-capture">Don't Track This</button>

<!-- Or use data attribute -->
<div data-ph-capture-attribute="false">
  Private content that won't be tracked
</div>

When to Use Autocapture

Great for:

  • Getting started quickly without instrumentation
  • Capturing general user behavior patterns
  • Watching recordings and understanding user flows
  • Discovering which UI elements users interact with
  • Early-stage products where you don't know what to track yet

Less ideal for:

  • Tracking business-critical events (use custom events instead)
  • Precise conversion tracking
  • Events that don't involve UI interaction (like background processes)
  • Complex interactions that need specific context

Custom Events

Custom events give you precise control over what you track and when. They're essential for tracking business outcomes, complex interactions, and events that don't map to simple UI interactions.

Tracking Custom Events

JavaScript:

// Basic event
posthog.capture('button_clicked');

// Event with properties
posthog.capture('video_watched', {
  video_id: 'intro_tutorial',
  video_title: 'Getting Started with PostHog',
  duration_seconds: 180,
  completion_percent: 95,
  quality: '1080p'
});

// Event with monetary value
posthog.capture('purchase_completed', {
  order_id: 'ORDER_12345',
  revenue: 149.99,
  currency: 'USD',
  product_count: 2,
  payment_method: 'credit_card',
  discount_applied: true,
  discount_amount: 15.00
});

React:

import { usePostHog } from 'posthog-js/react';

function CheckoutButton() {
  const posthog = usePostHog();

  const handleCheckout = () => {
    posthog.capture('checkout_started', {
      cart_value: 99.99,
      item_count: 3,
      source: 'product_page'
    });

    // Proceed with checkout...
  };

  return <button
}

Node.js (Server-side):

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

const posthog = new PostHog(
  'YOUR_PROJECT_API_KEY',
  { host: 'https://app.posthog.com' }
);

// Track server-side event
posthog.capture({
  distinctId: 'user_123',
  event: 'subscription_renewed',
  properties: {
    plan: 'Pro Annual',
    amount: 299.99,
    billing_cycle: 'yearly',
    renewal_count: 3
  }
});

// Always shutdown on exit
await posthog.shutdown();

Python:

from posthog import Posthog

posthog = Posthog(
  'YOUR_PROJECT_API_KEY',
  host='https://app.posthog.com'
)

# Track event
posthog.capture(
  distinct_id='user_123',
  event='api_request_completed',
  properties={
    'endpoint': '/api/v1/users',
    'method': 'GET',
    'status_code': 200,
    'response_time_ms': 45,
    'cache_hit': True
  }
)

Mobile (iOS - Swift):

import PostHog

// Track event
PHGPostHog.shared()?.capture(
  "level_completed",
  properties: [
    "level_name": "Desert Storm",
    "level_number": 8,
    "score": 9500,
    "time_seconds": 145,
    "stars_earned": 3
  ]
)

Mobile (Android - Kotlin):

import com.posthog.PostHog

PostHog.capture(
  event = "level_completed",
  properties = mapOf(
    "level_name" to "Desert Storm",
    "level_number" to 8,
    "score" to 9500,
    "time_seconds" to 145,
    "stars_earned" to 3
  )
)

Event Naming Best Practices

Good:

  • Use descriptive, action-based names: purchase_completed, video_watched, subscription_upgraded
  • Use snake_case for consistency
  • Be specific: checkout_started not just checkout
  • Include the outcome: form_submitted_success vs form_submitted_error

Avoid:

  • Generic names: event, action, click
  • Technical names: handleClick_component_line_42
  • PascalCase or camelCase (stick to snake_case)
  • Including variable data: video_abc123_watched

Ecommerce Tracking

Product Viewing & Discovery

// Product list viewed
posthog.capture('product_list_viewed', {
  category: 'Electronics',
  list_type: 'search_results',
  search_query: 'wireless headphones',
  product_count: 24,
  sort_order: 'price_low_high',
  filters_applied: ['in_stock', 'free_shipping', '4_stars_plus']
});

// Product viewed
posthog.capture('product_viewed', {
  product_id: 'PROD_12345',
  product_name: 'Wireless Noise-Cancelling Headphones',
  price: 199.99,
  currency: 'USD',
  category: 'Electronics > Audio > Headphones',
  brand: 'AudioTech',
  in_stock: true,
  inventory_count: 42,
  image_count: 8,
  review_count: 342,
  rating_average: 4.6
});

Cart Management

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

// Product removed from cart
posthog.capture('product_removed_from_cart', {
  product_id: 'PROD_12345',
  removal_reason: 'found_better_price',
  cart_total: 0,
  cart_item_count: 0
});

// Cart viewed
posthog.capture('cart_viewed', {
  cart_total: 459.97,
  cart_item_count: 3,
  product_ids: ['PROD_12345', 'PROD_67890', 'PROD_11111'],
  has_discount: true,
  discount_code: 'SAVE15'
});

Checkout Flow

// Checkout started
posthog.capture('checkout_started', {
  cart_total: 459.97,
  cart_item_count: 3,
  checkout_step: 1,
  checkout_type: 'guest',
  has_account: false
});

// Checkout step completed
posthog.capture('checkout_step_completed', {
  step_number: 2,
  step_name: 'shipping_info',
  shipping_method: 'standard',
  estimated_delivery_days: 5,
  shipping_cost: 0
});

// Payment info entered
posthog.capture('payment_info_entered', {
  payment_method: 'credit_card',
  card_type: 'visa',
  save_payment_info: true,
  billing_same_as_shipping: true
});

// Order completed
posthog.capture('order_completed', {
  order_id: 'ORDER_12345',
  revenue: 459.97,
  tax: 36.80,
  shipping: 0,
  discount: 68.99,
  total: 427.78,
  currency: 'USD',
  item_count: 3,
  payment_method: 'credit_card',
  shipping_method: 'standard',
  discount_code: 'SAVE15',
  is_first_purchase: true,
  products: [
    {
      product_id: 'PROD_12345',
      quantity: 2,
      price: 199.99
    },
    {
      product_id: 'PROD_67890',
      quantity: 1,
      price: 59.99
    }
  ]
});

Subscription Events

// Trial started
posthog.capture('trial_started', {
  plan_name: 'Pro',
  trial_length_days: 14,
  trial_price: 0,
  full_price_monthly: 29.99,
  full_price_yearly: 299.99,
  billing_cycle_selected: 'monthly'
});

// Subscription created
posthog.capture('subscription_created', {
  plan_name: 'Pro Annual',
  plan_price: 299.99,
  billing_cycle: 'yearly',
  payment_method: 'credit_card',
  converted_from_trial: true,
  discount_applied: false
});

// Subscription renewed
posthog.capture('subscription_renewed', {
  plan_name: 'Pro Monthly',
  renewal_count: 6,
  total_revenue_lifetime: 179.94,
  next_billing_date: '2024-07-01',
  renewal_type: 'automatic'
});

// Subscription cancelled
posthog.capture('subscription_cancelled', {
  plan_name: 'Pro Monthly',
  cancellation_reason: 'too_expensive',
  months_subscribed: 6,
  total_revenue: 179.94,
  offered_discount: true,
  accepted_discount: false,
  feedback_provided: true,
  cancellation_type: 'immediate'
});

// Subscription upgraded
posthog.capture('subscription_upgraded', {
  old_plan: 'Basic',
  new_plan: 'Pro',
  price_difference: 20.00,
  upgrade_trigger: 'feature_gate',
  prorated_charge: 15.00
});

User Identification

Anonymous vs Identified Users

PostHog tracks users in two states:

  1. Anonymous: Before login, PostHog generates a random distinct_id and stores it in a cookie
  2. Identified: After login, you set a distinct_id (typically your database user ID)

PostHog automatically merges the anonymous and identified profiles, preserving all pre-login events.

Identifying Users

JavaScript:

// When user signs up or logs in
posthog.identify(
  'user_123',  // Unique user ID from your database
  {
    // User properties
    email: 'user@example.com',
    name: 'Jane Doe',
    plan: 'Pro',
    signup_date: '2024-01-15',
    total_logins: 1
  }
);

Best Practices:

  • Use permanent IDs (database primary keys, UUIDs)
  • Don't use email addresses as IDs (they can change)
  • Call identify() as soon as the user authenticates
  • Include key user properties in the identify call
  • Don't call identify() on every page load (only on login/signup)

Setting User Properties

JavaScript:

// Set or update properties
posthog.people.set({
  plan: 'Enterprise',
  company: 'Acme Corp',
  employee_count: 500,
  industry: 'Technology'
});

// Set properties only once (won't overwrite)
posthog.people.set_once({
  first_login_date: '2024-01-15',
  initial_referrer: document.referrer,
  signup_source: 'google_ad'
});

// Increment numeric properties
posthog.people.increment({
  login_count: 1,
  videos_watched: 1
});

// Append to list properties
posthog.people.append({
  purchase_history: 'ORDER_12345'
});

Server-side (Node.js):

posthog.identify({
  distinctId: 'user_123',
  properties: {
    email: 'user@example.com',
    plan: 'Pro',
    $set: {
      company: 'Acme Corp'
    },
    $set_once: {
      first_seen: '2024-01-15'
    }
  }
});

User Logout

When users log out, you should reset the distinct_id:

// Reset to new anonymous ID
posthog.reset();

// The next user on this device will get a fresh anonymous ID

This is especially important for shared devices or public computers.

Group Analytics (B2B)

For B2B products, track company/organization-level events:

// Associate user with a company
posthog.group('company', 'company_id_123', {
  name: 'Acme Corporation',
  plan: 'Enterprise',
  employee_count: 500,
  industry: 'Technology',
  signup_date: '2023-06-01',
  mrr: 5000
});

// All subsequent events include company context
posthog.capture('feature_used', {
  feature_name: 'advanced_reports'
});

// You can analyze at the company level in PostHog

Property Types & Limits

Supported Data Types

  • String: "Pro Plan", "user@example.com"
  • Number: 29.99, 42, 3.14159
  • Boolean: true, false
  • Timestamp: "2024-01-20T14:30:00Z", 1705760000
  • Array: ["tag1", "tag2", "tag3"]
  • Object: {"key": "value"} (limited nesting recommended)

Limits & Best Practices

  • Event name: Keep under 200 characters
  • Property name: Keep under 200 characters
  • Property value: Strings under 64KB (recommend under 1KB)
  • Properties per event: No hard limit (recommend under 50 for performance)
  • Array length: No hard limit (recommend under 100 items)

Super Properties

Super properties are set once and included automatically with every event:

// Register super properties
posthog.register({
  app_version: '2.1.0',
  environment: 'production',
  deployment_region: 'us-east-1'
});

// These properties are now included in all events
posthog.capture('button_clicked');  // Includes app_version, environment, deployment_region

// Register super properties only once
posthog.register_once({
  initial_utm_source: 'google',
  initial_utm_campaign: 'summer_sale'
});

// Remove super properties
posthog.unregister('environment');

Debugging Event Tracking

Enable Debug Mode

JavaScript:

posthog.init('YOUR_PROJECT_API_KEY', {
  api_host: 'https://app.posthog.com',
  debug: true  // Enables verbose console logging
});

Check Events in Browser DevTools

Console:

// See all events
posthog.debug();

// Check current user
console.log(posthog.get_distinct_id());

// Check super properties
console.log(posthog.persistence.properties());

Network Tab:

  • Filter for posthog.com or your self-hosted domain
  • Look for /e/ endpoint (event ingestion)
  • Inspect request payload to see event data

Live Event Stream in PostHog

  1. Navigate to Activity in PostHog
  2. Filter by your user ID or event name
  3. See events arrive in real-time (within seconds)
  4. Click event to inspect full payload

Event Validation

Test Mode:

// Send test event
posthog.capture('test_event', {
  test: true,
  timestamp: new Date().toISOString()
});

// Check PostHog dashboard for event within 10 seconds

Best Practices

Event Design

Do:

  • Track events that represent user intent and outcomes
  • Include relevant context in properties
  • Use consistent naming conventions
  • Track both success and failure states
  • Document your event taxonomy

Don't:

  • Track events just because you can
  • Include personally identifiable information (PII) without consent
  • Use events for logging or debugging (use logs instead)
  • Create redundant events
  • Track sensitive data (passwords, credit cards, SSNs)

Performance

  • PostHog batches events automatically (every 10 seconds or 20 events)
  • Events are sent asynchronously, so tracking doesn't block page rendering
  • The SDK is ~30KB gzipped, with minimal performance impact
  • For high-frequency events, consider sampling or throttling

Privacy & Compliance

Do:

// Respect user consent
if (userHasConsented) {
  posthog.opt_in_capturing();
} else {
  posthog.opt_out_capturing();
}

// Disable persistence if needed
posthog.init('YOUR_PROJECT_API_KEY', {
  persistence: 'memory'  // Don't use cookies
});

// Mask sensitive elements
posthog.init('YOUR_PROJECT_API_KEY', {
  mask_all_text: true,  // Masks all text in recordings
  mask_all_element_attributes: true
});

Don't:

  • Track email, phone, address without explicit consent
  • Ignore GDPR, CCPA, or other privacy regulations
  • Assume default settings are compliant
  • Track children without parental consent

Additional Resources: