← Back to articles
Tooling PostHogAnalyticsTypeScript

PostHog: Product Analytics That Developers Actually Control

· 7 min read

Google Analytics tells you how many people visited your marketing page. It doesn’t tell you why users abandon your onboarding flow at step 3, which feature drives retention, or whether the new checkout redesign actually converts better. PostHog is built for that level of product insight. It combines event tracking, funnels, session replays, feature flags, and A/B testing in a single platform that developers integrate directly into the codebase. No tag managers, no third-party scripts injected by marketing, no black box between your code and your data.

Event tracking: capturing what matters

PostHog captures events. Every button click, page view, form submission, or API call can become a tracked event with structured properties. The SDK is lightweight and works across React, Next.js, Node.js, and every major platform.

import posthog from 'posthog-js';

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
  capture_pageview: false,
});

export default posthog;
import posthog from '@/lib/posthog';

function PricingCard({ plan }: { plan: Plan }) {
  function handleSubscribe() {
    posthog.capture('subscription_started', {
      plan: plan.name,
      price: plan.price,
      billing_cycle: plan.interval,
    });
  }

  return <button onClick={handleSubscribe}>Subscribe to {plan.name}</button>;
}

Setting capture_pageview: false disables automatic page view tracking, which is the right default for SPAs where navigation doesn’t trigger full page loads. You handle page views manually with the Next.js router or a layout component, giving you control over what counts as a page view.

Event properties are where the real value lives. Tracking “subscription_started” alone tells you how many people subscribe. Adding plan, price, and billing_cycle lets you answer which plan converts best, whether annual billing outperforms monthly, and how pricing changes affect conversion over time.

Identifying users across sessions

Anonymous events are useful for aggregate trends, but product analytics becomes powerful when events are tied to specific users. posthog.identify links all past and future events to a user identity.

import { useEffect } from 'react';
import posthog from '@/lib/posthog';

export function usePostHogIdentity(user: User | null) {
  useEffect(() => {
    if (user) {
      posthog.identify(user.id, {
        email: user.email,
        name: user.name,
        plan: user.plan,
        created_at: user.createdAt,
      });
    } else {
      posthog.reset();
    }
  }, [user]);
}

The second argument sets user properties that persist across sessions. When the user logs out, posthog.reset() clears the identity and starts a new anonymous session. This prevents events from one user bleeding into another on shared devices.

User properties make every analysis richer. Instead of “500 people clicked the export button,” you can filter by plan, signup date, or any custom property to find patterns that matter.

Funnels: finding where users drop off

A funnel tracks a sequence of events and shows where users abandon the flow. Onboarding completion, checkout conversion, feature adoption, any multi-step process can be measured.

// Step 1: user lands on onboarding
posthog.capture('onboarding_started');

// Step 2: user completes profile
posthog.capture('profile_completed', {
  fields_filled: ['name', 'company', 'role'],
});

// Step 3: user creates first project
posthog.capture('first_project_created', {
  template_used: 'blank',
});

// Step 4: user invites team member
posthog.capture('team_member_invited');

In the PostHog dashboard, you create a funnel with these four events in order. PostHog shows the conversion rate between each step, the median time between steps, and which user segments convert best. If 70% of users complete their profile but only 30% create a project, the problem is clear and specific.

Funnels also support breakdown by property. You might discover that users who choose a template convert at 60% while blank-project users convert at 20%. That’s a product decision backed by data, not intuition.

Feature flags: ship safely

Feature flags let you enable functionality for specific users, percentages, or conditions without deploying new code. PostHog’s flags are evaluated server-side or client-side with near-zero latency.

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

function Dashboard() {
  const showNewDashboard = useFeatureFlagEnabled('new-dashboard-v2');

  return showNewDashboard ? <NewDashboard /> : <LegacyDashboard />;
}
import { PostHog } from 'posthog-node';

const posthog = new PostHog(process.env.POSTHOG_API_KEY!);

export async function getFeatureFlag(userId: string, flag: string) {
  return posthog.isFeatureEnabled(flag, userId);
}

Flags work on the client with the React hook and on the server with the Node.js SDK. A typical rollout starts at 10% of users, monitors error rates and key metrics, then gradually increases to 100%. If something breaks, you disable the flag instantly without a rollback deploy.

PostHog ties feature flags to analytics automatically. When a flag is active, every event from that user is tagged with the flag state. You can compare conversion rates, error rates, and engagement between the flag-on and flag-off groups without any extra instrumentation.

A/B testing: measuring impact

A/B tests build on feature flags by adding statistical rigor. You define a hypothesis, split users into control and variant groups, and PostHog calculates whether the difference in a target metric is statistically significant.

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

function CTAButton() {
  const variant = useFeatureFlagVariantKey('cta-experiment');

  const label = variant === 'test' ? 'Start free trial' : 'Get started';
  const color = variant === 'test' ? 'bg-green-600' : 'bg-blue-600';

  return (
    <button className={color} onClick={() => posthog.capture('cta_clicked', { variant })}>
      {label}
    </button>
  );
}

PostHog handles the randomization and ensures each user consistently sees the same variant. The experiment dashboard shows conversion rates for each variant, confidence intervals, and whether the result is significant. No spreadsheets, no manual calculations, no guessing when to call the test.

Session replays: seeing what happened

Session replays record user interactions and let you watch exactly what happened during a session. Clicks, scrolls, form inputs, navigation, rage clicks, everything is captured and replayed in the PostHog dashboard.

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
  capture_pageview: false,
  session_recording: {
    maskAllInputs: true,
    maskTextSelector: '[data-sensitive]',
  },
});

maskAllInputs replaces input values with asterisks in recordings, preventing sensitive data like passwords and credit card numbers from being captured. maskTextSelector lets you mask specific elements by selector.

The real power is linking replays to analytics. When a funnel shows a 40% drop at step 3, you can click through to session replays of users who dropped off and watch what actually happened. Maybe the submit button was below the fold. Maybe an error toast appeared and disappeared too fast. The replay removes speculation.

Server-side tracking with Node.js

Client-side tracking covers UI interactions. Server-side tracking covers business events that shouldn’t depend on the browser: payments processed, emails sent, background jobs completed.

import { PostHog } from 'posthog-node';

const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
  host: process.env.POSTHOG_HOST,
});

export async function processPayment(userId: string, amount: number) {
  const result = await stripe.charges.create({ amount, currency: 'eur' });

  posthog.capture({
    distinctId: userId,
    event: 'payment_processed',
    properties: {
      amount,
      currency: 'eur',
      payment_id: result.id,
      status: result.status,
    },
  });

  return result;
}

Server-side events are more reliable than client-side ones. They can’t be blocked by ad blockers, lost to network failures, or skipped when users close the tab. For revenue-critical events like payments, server-side tracking is the only approach that gives accurate numbers.

Self-hosting: owning your data

PostHog can run entirely on your own infrastructure. No data leaves your servers. No third-party has access to your users’ behavior. This matters for GDPR compliance, enterprise clients with data residency requirements, and teams that simply don’t want their product data in someone else’s cloud.

services:
  posthog:
    image: posthog/posthog:latest
    environment:
      - SECRET_KEY=your-secret-key
      - DATABASE_URL=postgres://posthog:posthog@db:5432/posthog
      - REDIS_URL=redis://redis:6379
    ports:
      - '8000:8000'
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine

  redis:
    image: redis:7-alpine

The self-hosted version has the same features as the cloud version. Event ingestion, dashboards, funnels, feature flags, session replays, all running on infrastructure you control. Updates are pulled as Docker image versions, and the migration path between self-hosted and cloud is straightforward.

Conclusion

PostHog puts product analytics in the codebase where it belongs. Events are tracked in the same functions that handle business logic. Feature flags live next to the components they control. Server-side tracking ensures critical events are never lost. And self-hosting means your data stays yours. For any product that needs to understand user behavior beyond page views and bounce rates, PostHog replaces an entire stack of disconnected tools with a single platform that developers can actually own.