How to Add Product Analytics with PostHog
·APIScout Team
posthogproduct analyticsfeature flagstutorialapi integration
How to Add Product Analytics with PostHog
PostHog is the open-source product analytics platform. One tool replaces five: analytics, session replay, feature flags, A/B testing, and surveys. Self-host it or use their cloud — either way, you own your data.
What You'll Build
- Event tracking (pageviews, clicks, custom events)
- Feature flags (gradual rollouts, A/B tests)
- Session replay (watch real user sessions)
- Funnel analysis (signup → activation → conversion)
- User identification and properties
Prerequisites: React/Next.js app, PostHog account (free: 1M events/month, 5K sessions).
1. Setup
Install
npm install posthog-js posthog-node
Initialize (Client-Side)
// lib/posthog.ts
import posthog from 'posthog-js';
export function initPostHog() {
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
person_profiles: 'identified_only',
capture_pageview: false, // We'll handle this manually
capture_pageleave: true,
});
}
return posthog;
}
PostHog Provider (Next.js)
// app/providers.tsx
'use client';
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { useEffect } from 'react';
export function PHProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/ingest', // Proxy through your domain (avoids ad blockers)
person_profiles: 'identified_only',
});
}, []);
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
Add to Layout
// app/layout.tsx
import { PHProvider } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<PHProvider>{children}</PHProvider>
</body>
</html>
);
}
Proxy Configuration (Avoid Ad Blockers)
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: '/ingest/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/ingest/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
];
},
};
2. Event Tracking
Automatic Pageviews
// components/PostHogPageview.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { usePostHog } from 'posthog-js/react';
import { useEffect } from 'react';
export function PostHogPageview() {
const pathname = usePathname();
const searchParams = useSearchParams();
const posthog = usePostHog();
useEffect(() => {
if (pathname && posthog) {
let url = window.origin + pathname;
if (searchParams.toString()) {
url += `?${searchParams.toString()}`;
}
posthog.capture('$pageview', { $current_url: url });
}
}, [pathname, searchParams, posthog]);
return null;
}
Custom Events
import { usePostHog } from 'posthog-js/react';
function PricingPage() {
const posthog = usePostHog();
const handlePlanSelect = (plan: string) => {
posthog.capture('plan_selected', {
plan_name: plan,
plan_price: getPlanPrice(plan),
source: 'pricing_page',
});
};
const handleCTAClick = () => {
posthog.capture('cta_clicked', {
cta_text: 'Start Free Trial',
page: 'pricing',
});
};
return (
<div>
<button onClick={() => handlePlanSelect('pro')}>Choose Pro</button>
<button onClick={handleCTAClick}>Start Free Trial</button>
</div>
);
}
User Identification
// After login
posthog.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
company: user.company,
});
// After logout
posthog.reset();
Server-Side Tracking
// lib/posthog-server.ts
import { PostHog } from 'posthog-node';
const posthogServer = new PostHog(process.env.POSTHOG_KEY!, {
host: 'https://us.i.posthog.com',
});
// Track server-side events
posthogServer.capture({
distinctId: userId,
event: 'subscription_created',
properties: {
plan: 'pro',
amount: 2900,
currency: 'usd',
billing_period: 'monthly',
},
});
// Flush before process exits
await posthogServer.shutdown();
3. Feature Flags
Create a Flag
In PostHog Dashboard → Feature Flags → New Feature Flag:
- Key:
new-pricing-page - Rollout: 50% of users (or specific conditions)
Check Flag (Client)
import { usePostHog, useFeatureFlagEnabled } from 'posthog-js/react';
function PricingPage() {
const newPricing = useFeatureFlagEnabled('new-pricing-page');
if (newPricing) {
return <NewPricingPage />;
}
return <OldPricingPage />;
}
Check Flag (Server)
// In API route or server component
import { posthogServer } from '@/lib/posthog-server';
const isEnabled = await posthogServer.isFeatureEnabled(
'new-pricing-page',
userId
);
Flag with Payload
// PostHog dashboard: set payload as JSON
const flagPayload = posthog.getFeatureFlagPayload('pricing-experiment');
// Returns: { discount: 20, cta_text: "Save 20% Today" }
4. Session Replay
Enable Recording
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/ingest',
session_recording: {
maskAllInputs: true, // Privacy: mask form inputs
maskTextSelector: '.sensitive', // Mask specific elements
},
});
Control Recording
// Start recording manually
posthog.startSessionRecording();
// Stop recording
posthog.stopSessionRecording();
// Only record for specific users
if (user.plan === 'enterprise') {
posthog.startSessionRecording();
}
5. Funnels and Insights
Define Events for Funnel
Track these events to build a conversion funnel:
// Step 1: Visit pricing page
posthog.capture('pricing_page_viewed');
// Step 2: Select a plan
posthog.capture('plan_selected', { plan: 'pro' });
// Step 3: Enter payment info
posthog.capture('payment_info_entered');
// Step 4: Complete purchase
posthog.capture('purchase_completed', { plan: 'pro', amount: 29 });
In PostHog Dashboard → Insights → New Insight → Funnel:
- Step 1:
pricing_page_viewed - Step 2:
plan_selected - Step 3:
payment_info_entered - Step 4:
purchase_completed
Key Insights to Set Up
| Insight Type | Configuration | Business Value |
|---|---|---|
| Funnel | Signup → Activation → Conversion | Conversion optimization |
| Retention | "Login" event, weekly cohorts | Stickiness measurement |
| Trends | Daily active users, feature usage | Growth tracking |
| Paths | User navigation patterns | UX optimization |
| Stickiness | How many days/week users return | Engagement depth |
6. Group Analytics
Track company-level metrics (B2B):
// Associate user with a company
posthog.group('company', 'company_123', {
name: 'Acme Corp',
plan: 'enterprise',
employee_count: 500,
});
// Events are now attributed to both user and company
posthog.capture('feature_used', { feature: 'api_dashboard' });
Self-Hosted vs Cloud
| Factor | Cloud | Self-Hosted |
|---|---|---|
| Setup | Instant | Docker Compose or Kubernetes |
| Maintenance | PostHog handles it | You manage updates |
| Data location | US/EU | Your infrastructure |
| Pricing | Usage-based | Free (infrastructure costs only) |
| Scale | Unlimited | Depends on your infra |
| Best for | Most teams | Strict data residency requirements |
Self-Hosted Quick Start
# Docker Compose (recommended for small teams)
git clone https://github.com/PostHog/posthog.git
cd posthog
docker compose -f docker-compose.hobby.yml up -d
Pricing (Cloud)
| Feature | Free | Paid |
|---|---|---|
| Events | 1M/month | $0.00031/event after 1M |
| Session replay | 5,000/month | $0.005/recording after 5K |
| Feature flags | 1M requests | $0.0001/request after 1M |
| Surveys | 250 responses | $0.002/response after 250 |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Tracking everything | Noise, higher costs | Track 15-20 key events max |
| Not identifying users | Can't connect pre/post-login behavior | Call identify() after auth |
| Client-only tracking | Missed server events | Use posthog-node for server events |
| Not using proxy | Ad blockers block tracking (30-40%) | Proxy through your domain |
| No event naming convention | Messy data, hard to query | Use snake_case consistently |
| Recording everything | Privacy concerns, storage costs | Only record opt-in or specific flows |
Choosing analytics? Compare PostHog vs Mixpanel vs Amplitude on APIScout — open-source vs proprietary, features, and pricing.