PostHog Analytics: Implementation and API Guide | OpsBlu Docs

PostHog Analytics: Implementation and API Guide

Technical guide to PostHog event tracking, feature flags, session recording, self-hosted deployment, HogQL queries, and the Exports API.

How PostHog Works

PostHog is an event-based product analytics platform. Every user interaction is stored as an event with a timestamp, a distinct_id (user identifier), and a set of key-value properties. Events flow through an ingestion pipeline into ClickHouse (cloud) or PostgreSQL/ClickHouse (self-hosted), where they are queryable via the PostHog UI, HogQL, or the API.

The data model has three core entities:

  • Events -- Timestamped actions with arbitrary properties. Schema-on-read; no predefined schema required.
  • Persons -- Identified by distinct_id. Multiple distinct_ids can be merged into a single person via posthog.identify() or posthog.alias().
  • Groups -- Optional entity for account-level or organization-level analytics. Events can be associated with a group in addition to a person.

Autocapture records clicks, form submissions, and pageviews automatically by instrumenting DOM events. Custom events supplement autocapture for business-specific actions like purchases or feature usage.

Feature flags evaluate on the client or server side. The PostHog SDK fetches flag values on initialization and caches them locally. Server-side evaluation uses the /decide endpoint or local evaluation via the feature flag definitions file.

Session recordings capture DOM mutations via a MutationObserver-based approach (rrweb), compressing snapshots and streaming them to PostHog for playback.


Installing the Tracking Script

JavaScript (Browser)

Load PostHog.js via the snippet or npm:

<script>
  !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
  posthog.init('YOUR_PROJECT_API_KEY', {
    api_host: 'https://us.i.posthog.com', // or https://eu.i.posthog.com, or your self-hosted URL
    person_profiles: 'identified_only'
  });
</script>

npm / yarn

npm install posthog-js
import posthog from 'posthog-js';

posthog.init('YOUR_PROJECT_API_KEY', {
  api_host: 'https://us.i.posthog.com',
  person_profiles: 'identified_only',
  autocapture: true,           // DOM event capture
  capture_pageview: true,      // automatic $pageview events
  capture_pageleave: true,     // $pageleave on unload
  session_recording: {
    maskAllInputs: true,       // redact form fields
    maskTextSelector: '.sensitive'
  }
});

Node.js (Server-Side)

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

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

client.capture({
  distinctId: 'user-123',
  event: 'subscription_created',
  properties: { plan: 'pro', mrr: 49.00 }
});

await client.shutdown(); // flush before exit

Self-Hosted (Docker)

git clone https://github.com/PostHog/posthog.git
cd posthog
docker compose -f docker-compose.hobby.yml up -d

Point your SDK's api_host to http://your-server:8000. For production, deploy via the Helm chart on Kubernetes:

helm repo add posthog https://posthog.github.io/charts-clickhouse/
helm install posthog posthog/posthog \
  --set cloud=aws \
  --set ingress.hostname=posthog.example.com

Event Tracking and Data Collection

Custom Events

// Track a purchase event
posthog.capture('purchase_completed', {
  product_id: 'sku-4421',
  product_name: 'Widget Pro',
  price: 29.99,
  currency: 'USD',
  quantity: 2
});

// Track with a timestamp (backfilling)
posthog.capture('import_event', {
  $timestamp: '2025-12-01T10:00:00Z',
  source: 'csv_import'
});

// Track with groups (requires group analytics)
posthog.capture('report_exported', {
  format: 'pdf',
  $groups: { company: 'company-abc' }
});

Autocapture Events

PostHog automatically captures these event types when autocapture: true:

Event Trigger
$autocapture Click on <a>, <button>, <input>, <select>, <textarea>, or elements with [role=button]
$pageview Page load (if capture_pageview: true)
$pageleave Page unload
$rageclick 3+ clicks on the same element within 5 seconds

Autocapture properties include $element_tag, $element_text, $element_href, and CSS selectors. Disable autocapture on specific elements:

<button data-ph-no-capture>Do Not Track This</button>

Super Properties

Set properties that attach to every subsequent event:

posthog.register({
  app_version: '2.4.1',
  environment: 'production'
});

// Set once (only if not already set)
posthog.register_once({ first_seen_version: '2.4.1' });

// Remove a super property
posthog.unregister('environment');

Identity and User Tracking

Identifying Users

Connect anonymous and authenticated sessions:

// Before login: PostHog uses an anonymous distinct_id (UUID)
// After login: identify merges the anonymous ID with the real user ID
posthog.identify('user-db-id-123', {
  email: 'user@example.com',
  name: 'Jane Doe',
  plan: 'enterprise'
});

After identify(), PostHog merges the anonymous person record with the identified person. All prior events from the anonymous session are retroactively associated with the identified user.

Aliasing

Link two known IDs (e.g., pre- and post-migration user IDs):

posthog.alias('new-user-id', 'old-user-id');

Resetting Identity

On logout, reset to generate a new anonymous ID:

posthog.reset();

Group Analytics

Associate events with an organization or account:

// Assign the current user to a group
posthog.group('company', 'company-abc', {
  name: 'Acme Corp',
  industry: 'SaaS',
  employee_count: 150
});

// Events now include company group context
posthog.capture('feature_used', { feature: 'dashboard' });

Group analytics enable queries like "which companies have the highest activation rate" rather than just individual user metrics.

Person Properties

Set properties on the person record directly:

posthog.people.set({ subscription_status: 'active' });
posthog.people.set_once({ first_purchase_date: '2025-06-15' });

API and Data Export

HogQL Queries

PostHog exposes a SQL-like query language (HogQL) over the events table:

-- Top 10 events by volume in the last 7 days
SELECT event, count() as cnt
FROM events
WHERE timestamp > now() - interval 7 day
GROUP BY event
ORDER BY cnt DESC
LIMIT 10
-- Funnel: signup to purchase within 7 days
SELECT
  person_id,
  min(if(event = 'signed_up', timestamp, null)) as signup_time,
  min(if(event = 'purchase_completed', timestamp, null)) as purchase_time
FROM events
WHERE event IN ('signed_up', 'purchase_completed')
  AND timestamp > now() - interval 30 day
GROUP BY person_id
HAVING signup_time IS NOT NULL
  AND purchase_time IS NOT NULL
  AND purchase_time - signup_time < interval 7 day

Execute via API:

curl -X POST 'https://us.i.posthog.com/api/projects/PROJECT_ID/query/' \
  -H 'Authorization: Bearer phx_your_personal_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "query": {
      "kind": "HogQLQuery",
      "query": "SELECT event, count() FROM events WHERE timestamp > now() - interval 1 day GROUP BY event ORDER BY count() DESC LIMIT 5"
    }
  }'

Events API

Retrieve raw events:

# List events for a person
curl 'https://us.i.posthog.com/api/projects/PROJECT_ID/events/?person_id=USER_ID&limit=100' \
  -H 'Authorization: Bearer phx_your_personal_api_key'

Batch Export API

Export events to a data warehouse:

# Create a batch export to S3
curl -X POST 'https://us.i.posthog.com/api/projects/PROJECT_ID/batch_exports/' \
  -H 'Authorization: Bearer phx_your_personal_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "S3 Export",
    "destination": {
      "type": "S3",
      "config": {
        "bucket_name": "my-posthog-exports",
        "region": "us-east-1",
        "prefix": "posthog/",
        "aws_access_key_id": "AKIA...",
        "aws_secret_access_key": "..."
      }
    },
    "interval": "hour"
  }'

Feature Flags API

# Evaluate flags for a user
curl -X POST 'https://us.i.posthog.com/decide/?v=3' \
  -H 'Content-Type: application/json' \
  -d '{
    "api_key": "YOUR_PROJECT_API_KEY",
    "distinct_id": "user-123"
  }'
// Client-side flag check
if (posthog.isFeatureEnabled('new-checkout-flow')) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}

// Multivariate flag
const variant = posthog.getFeatureFlag('pricing-page-test');
// Returns 'control', 'variant-a', or 'variant-b'

// Flag with payload
const config = posthog.getFeatureFlagPayload('onboarding-steps');
// Returns JSON payload attached to the flag

Common Issues

Events Not Appearing

Check initialization order. PostHog must initialize before any capture() calls:

// Wrong: capture before init
posthog.capture('test_event');
posthog.init('key', { api_host: '...' });

// Correct: init first
posthog.init('key', { api_host: '...' });
posthog.capture('test_event');

Verify the network request reaches PostHog. Open DevTools > Network, filter for i.posthog.com. A 200 response confirms delivery. A 401 means the API key is wrong.

Autocapture Missing Elements

Autocapture only instruments specific tags: a, button, form, input, select, textarea, and elements with role="button". Clicks on <div> or <span> elements are not captured unless they have a matching role attribute.

To capture clicks on custom elements, add the attribute:

<div role="button" class="custom-cta">Click Me</div>

Distinct ID Conflicts

If a user logs in on two different browsers, posthog.identify() merges those anonymous IDs into one person. If you call identify() with different real user IDs from the same browser without calling reset() first, the second user's events will be attributed to the first user.

Always call posthog.reset() on logout before identifying a new user.

Self-Hosted ClickHouse Memory

Self-hosted ClickHouse can OOM on large queries. Set memory limits in docker-compose.yml:

clickhouse:
  environment:
    - MAX_MEMORY_USAGE=8000000000  # 8GB per query
    - MAX_MEMORY_USAGE_FOR_ALL_QUERIES=12000000000

Session Recordings Not Playing

Recordings require the rrweb library bundled with PostHog.js. If you load PostHog via a custom build that strips rrweb, recordings will not function. Also verify session_recording is not disabled in posthog.init():

posthog.init('key', {
  session_recording: {
    // Ensure this is NOT set to false:
    // disable_session_recording: true  // remove this line
  }
});

Content Security Policy headers can also block recording. Add connect-src and script-src for your PostHog host.


Platform-Specific Considerations

Experiments (A/B Testing)

Experiments are built on feature flags. Create an experiment in the PostHog UI, which generates a multivariate flag. The SDK returns a variant string:

const variant = posthog.getFeatureFlag('checkout-experiment');

if (variant === 'test') {
  showNewCheckoutLayout();
} else {
  showControlLayout();
}

// Track the goal metric
posthog.capture('checkout_completed', { revenue: 59.99 });

PostHog calculates statistical significance using a Bayesian model and reports results on the experiment page.

Surveys

Trigger in-product surveys based on user behavior:

// Surveys are configured in the PostHog UI
// The SDK automatically renders them when conditions match
// To manually check for available surveys:
posthog.getActiveMatchingSurveys((surveys) => {
  console.log('Active surveys:', surveys);
});

Data Warehouse Imports

PostHog can import data from external sources (Stripe charges, Salesforce contacts, Zendesk tickets) into the data warehouse, making it queryable alongside event data in HogQL:

-- Join PostHog events with imported Stripe charges
SELECT e.person_id, s.amount, s.currency
FROM events e
JOIN stripe_charges s ON e.properties.$stripe_customer_id = s.customer_id
WHERE e.event = 'purchase_completed'

Rate Limits

The capture endpoint accepts up to 1,000 events per batch request. The API has per-project rate limits: 240 requests/minute for the query API, 600 requests/minute for the events API. Self-hosted deployments have no rate limits beyond infrastructure capacity.