Skip to content

Fix: PostHog Not Working — Events Not Tracking, Feature Flags Stale, or Session Replay Blank

FixDevs ·

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 events

Or feature flags always return the default value:

const isEnabled = posthog.isFeatureEnabled('new-dashboard');
// Always false — even though the flag is enabled in PostHog

Or session replay shows a blank recording:

Session replays list is empty even after multiple page visits

Why 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 eventsposthog.capture() before posthog.init() silently drops events. In Next.js App Router, initialization must happen in a client component.
  • Feature flags load asynchronouslyisFeatureEnabled() returns undefined until flags are fetched from PostHog’s servers. Calling it immediately after init returns undefined, 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 recordSession option must be enabled, and the posthog-js bundle 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 anonymousposthog.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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles