Fix: PostHog Not Working — Events Not Tracking, Feature Flags Stale, or Session Replay Blank
Quick Answer
How to fix PostHog analytics issues — JavaScript and Next.js setup, event capture, feature flags, A/B testing, session replay, user identification, and server-side tracking.
The Problem
posthog.capture() is called but events don’t appear in the dashboard:
posthog.capture('button_clicked', { button: 'signup' });
// PostHog dashboard shows zero eventsOr feature flags always return the default value:
const isEnabled = posthog.isFeatureEnabled('new-dashboard');
// Always false — even though the flag is enabled in PostHogOr session replay shows a blank recording:
Session replays list is empty even after multiple page visitsWhy This Happens
PostHog is a product analytics platform that captures events, feature flags, and session recordings. Common setup issues:
- The PostHog client must be initialized before capturing events —
posthog.capture()beforeposthog.init()silently drops events. In Next.js App Router, initialization must happen in a client component. - Feature flags load asynchronously —
isFeatureEnabled()returnsundefineduntil flags are fetched from PostHog’s servers. Calling it immediately after init returnsundefined, which is falsy. - Ad blockers block PostHog — many ad blockers and privacy extensions block requests to
app.posthog.com. Events and replays are lost when blocked. A reverse proxy solves this. - Session replay requires explicit opt-in — the
recordSessionoption must be enabled, and theposthog-jsbundle must include the recording module. It’s not enabled by default.
Fix 1: Next.js App Router Setup
npm install posthog-js posthog-node// lib/posthog.ts — client-side instance
import posthog from 'posthog-js';
export function initPostHog() {
if (typeof window === 'undefined') return;
if (posthog.__loaded) return; // Already initialized
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',
// Session replay
session_recording: {
maskAllInputs: true, // Mask input values
maskTextSelector: '.sensitive',
},
// Feature flags
bootstrap: {
// Pre-load flags from server (optional — avoids flash)
},
// Autocapture
autocapture: true,
capture_pageview: false, // We'll handle this manually for SPA
capture_pageleave: true,
// Reduce noise
persistence: 'localStorage+cookie',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.debug();
}
},
});
}
export { posthog };// components/PostHogProvider.tsx
'use client';
import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { initPostHog, posthog } from '@/lib/posthog';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
export function PostHogProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
initPostHog();
}, []);
// Track page views on route change
useEffect(() => {
if (pathname && posthog) {
let url = window.origin + pathname;
if (searchParams.toString()) {
url += '?' + searchParams.toString();
}
posthog.capture('$pageview', { $current_url: url });
}
}, [pathname, searchParams]);
return <PHProvider client={posthog}>{children}</PHProvider>;
}// app/layout.tsx
import { PostHogProvider } from '@/components/PostHogProvider';
import { Suspense } from 'react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Suspense fallback={null}>
<PostHogProvider>{children}</PostHogProvider>
</Suspense>
</body>
</html>
);
}Fix 2: Event Tracking
'use client';
import { usePostHog } from 'posthog-js/react';
function SignupButton() {
const posthog = usePostHog();
function handleSignup() {
// Track custom event with properties
posthog.capture('signup_started', {
method: 'email',
referrer: document.referrer,
page: window.location.pathname,
});
}
return <button onClick={handleSignup}>Sign Up</button>;
}
// Identify users after login
function useIdentifyUser(user: { id: string; email: string; name: string; plan: string }) {
const posthog = usePostHog();
useEffect(() => {
if (user) {
posthog.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
});
}
}, [user, posthog]);
}
// Reset on logout
function LogoutButton() {
const posthog = usePostHog();
function handleLogout() {
posthog.reset(); // Clears user identity and starts new session
// ... actual logout logic
}
return <button onClick={handleLogout}>Logout</button>;
}
// Group analytics (for B2B — associate user with organization)
posthog.group('company', 'company-123', {
name: 'Acme Corp',
plan: 'enterprise',
employeeCount: 50,
});Fix 3: Feature Flags
'use client';
import { useFeatureFlagEnabled, useFeatureFlagPayload, usePostHog } from 'posthog-js/react';
// Boolean flag
function Dashboard() {
const isNewDashboard = useFeatureFlagEnabled('new-dashboard');
if (isNewDashboard === undefined) {
return <div>Loading...</div>; // Flags still loading
}
return isNewDashboard ? <NewDashboard /> : <OldDashboard />;
}
// Flag with payload (JSON configuration)
function PricingPage() {
const pricingConfig = useFeatureFlagPayload('pricing-experiment');
if (!pricingConfig) return <DefaultPricing />;
return (
<div>
<h1>{pricingConfig.headline}</h1>
<p>{pricingConfig.description}</p>
<span>${pricingConfig.price}/mo</span>
</div>
);
}
// Multivariate flag
function HeroSection() {
const posthog = usePostHog();
const variant = posthog.getFeatureFlag('hero-experiment');
// variant: 'control' | 'variant-a' | 'variant-b'
switch (variant) {
case 'variant-a':
return <HeroA />;
case 'variant-b':
return <HeroB />;
default:
return <HeroDefault />;
}
}
// Server-side feature flags
// app/page.tsx
import { PostHog } from 'posthog-node';
const posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
});
export default async function Page() {
const flags = await posthogServer.getAllFlags('user-123');
const showBanner = flags['announcement-banner'];
return (
<div>
{showBanner && <AnnouncementBanner />}
</div>
);
}Fix 4: Reverse Proxy (Bypass Ad Blockers)
// next.config.mjs — proxy PostHog requests through your domain
const nextConfig = {
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*',
},
{
source: '/ingest/decide',
destination: 'https://us.i.posthog.com/decide',
},
];
},
};
export default nextConfig;// Update PostHog config to use the proxy
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: '/ingest', // Use your own domain as proxy
ui_host: 'https://us.posthog.com', // For toolbar
});Fix 5: Server-Side Tracking
// lib/posthog-server.ts
import { PostHog } from 'posthog-node';
const posthog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
});
export { posthog as posthogServer };
// Track server-side events
// app/api/checkout/route.ts
import { posthogServer } from '@/lib/posthog-server';
export async function POST(req: Request) {
const { userId, plan } = await req.json();
posthogServer.capture({
distinctId: userId,
event: 'subscription_created',
properties: {
plan,
amount: plan === 'pro' ? 29 : 99,
source: 'api',
},
});
// Flush before serverless function ends
await posthogServer.shutdown();
return Response.json({ success: true });
}Fix 6: A/B Testing with Experiments
'use client';
import { usePostHog } from 'posthog-js/react';
import { useEffect } from 'react';
function CheckoutPage() {
const posthog = usePostHog();
const variant = posthog.getFeatureFlag('checkout-experiment');
// Track experiment exposure
useEffect(() => {
if (variant) {
posthog.capture('$feature_flag_called', {
$feature_flag: 'checkout-experiment',
$feature_flag_response: variant,
});
}
}, [variant, posthog]);
// Track conversion
function handlePurchase() {
posthog.capture('purchase_completed', {
amount: 49.99,
variant,
});
}
if (variant === 'one-step') {
return <OneStepCheckout onPurchase={handlePurchase} />;
}
return <MultiStepCheckout onPurchase={handlePurchase} />;
}Still Not Working?
Events don’t appear in the dashboard — check the browser Network tab for requests to posthog.com or your proxy. If requests are blocked (status 0 or missing), an ad blocker is active. Set up the reverse proxy. Also verify the API key matches your project — keys are project-specific.
Feature flags always return undefined — flags load asynchronously after posthog.init(). Use useFeatureFlagEnabled() which handles the loading state, or use posthog.onFeatureFlags(() => { ... }) to wait for flags. For server-rendered pages, use posthog-node to evaluate flags on the server.
Session replay is empty — verify session_recording is not disabled in the init config. Also check that the PostHog project has session recording enabled (Project Settings → Session Recording). Some content security policies block the recording script.
Identified user events appear as anonymous — posthog.identify() must be called before capture events. If you capture events before identifying, they’re attributed to an anonymous user. After identification, PostHog merges the anonymous and identified profiles, but this can take a few minutes to reflect in the dashboard.
For related analytics and monitoring issues, see Fix: Sentry Not Working and Fix: OpenTelemetry Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Vinxi Not Working — Dev Server Not Starting, Routes Not Matching, or Build Failing
How to fix Vinxi server framework issues — app configuration, routers, server functions, middleware, static assets, and deployment to different platforms.
Fix: Clack Not Working — Prompts Not Displaying, Spinners Stuck, or Cancel Not Handled
How to fix @clack/prompts issues — interactive CLI prompts, spinners, multi-select, confirm dialogs, grouped tasks, cancellation handling, and building CLI tools with beautiful output.