← Back to Blog
Next.js2026-05-11·75 min read

PostHog Custom Events: How I Tracked a $59 Payment Funnel from Page View to Stripe Checkout

By Diven Rastdus

Revenue Leakage Detection: Implementing Conversion Funnels in Next.js with PostHog

Current Situation Analysis

Launching a monetization page without granular instrumentation is equivalent to flying blind. Many SaaS teams deploy pricing pages and integration flows, relying on aggregate traffic metrics to gauge success. This approach creates a critical blind spot: you can see how many users arrive, but you cannot identify where intent dissipates.

The industry pain point is conversion ambiguity. When revenue drops to zero, teams often waste days debugging payment gateways, server webhooks, or pricing tiers. In reality, the leakage usually occurs in the user interface—users may never see the call-to-action (CTA), or they may abandon the flow due to friction before reaching the payment step.

Standard analytics tools provide pageview counts but lack the context of user behavior within the page. Without custom event tracking, you cannot distinguish between a user who read the entire value proposition and one who bounced after three seconds. This gap leads to misdiagnosed problems and prolonged revenue stagnation.

PostHog addresses this by allowing developers to instrument specific user interactions as events. The platform's free tier supports up to 1 million events per month, which is sufficient for most early-stage SaaS products. By mapping the user journey to discrete events, teams can construct conversion funnels that reveal exact drop-off points. This shifts debugging from speculation to data-driven analysis, reducing the time-to-insight from weeks to minutes.

WOW Moment: Key Findings

Implementing a custom event funnel transforms raw traffic data into actionable conversion intelligence. The following comparison illustrates the operational difference between basic analytics and funnel instrumentation.

Metric Basic Traffic Analytics PostHog Funnel Instrumentation
Visibility Total pageviews and session duration Step-by-step conversion rates and drop-off percentages
Actionability "100 visitors, 0 sales" "100 visitors, 40 saw pricing, 5 started checkout, 0 paid"
Debug Time Days of guessing (Stripe, copy, pricing) Minutes of pinpointing the exact UI bottleneck
Cost Efficiency Free tier usage wasted on noise High signal-to-noise ratio; efficient event usage
Revenue Attribution None Direct correlation between UI interactions and payment intent

This finding matters because it enables leakage localization. Instead of optimizing the entire page, you can focus engineering and design resources on the specific step where users abandon. For example, if the funnel shows a high drop-off between "Pricing Visible" and "Checkout Started," the issue is likely value proposition or pricing clarity, not the payment processor.

Core Solution

The implementation requires a structured approach to event schema design, SDK initialization, and component-level tracking. This solution uses Next.js App Router with the PostHog React SDK.

1. Event Schema Design

Define events that represent critical transitions in the user journey. Avoid over-tracking; focus on steps that indicate intent or progression. A robust schema for a monetization page includes:

  • landing_viewed: Fires when the pricing page component mounts.
  • pricing_visible: Fires when the pricing section enters the viewport.
  • demo_engaged: Fires when a user requests a demo or interacts with proof elements.
  • payment_started: Fires when the user initiates the checkout flow.

Each event should carry properties that provide context, such as plan_tier, amount, and currency. This allows for segmentation and revenue attribution later.

2. Provider Initialization

PostHog's React SDK must initialize on the client side. In Next.js App Router, this requires a client component wrapped in a Suspense boundary to prevent blocking server-side rendering. Omitting Suspense can cause pageview events to vanish silently.

// src/providers/AnalyticsProvider.tsx
'use client';

import posthog from 'posthog-js';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { useEffect } from 'react';

interface AnalyticsProviderProps {
  children: React.ReactNode;
}

export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
  useEffect(() => {
    const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
    if (!apiKey) return;

    posthog.init(apiKey, {
      api_host: 'https://us.i.posthog.com',
      capture_pageview: true,
      capture_pageleave: true,
      // Enable autocapture for basic interactions if needed
      autocapture: false, 
    });
  }, []);

  return <PHProvider client={posthog}>{children}</PHProvider>;
}

Integrate the provider in the root layout with a Suspense wrapper:

// app/layout.tsx
import { Suspense } from 'react';
import { AnalyticsProvider } from '@/providers/AnalyticsProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Suspense fallback={null}>
          <AnalyticsProvider>{children}</AnalyticsProvider>
        </Suspense>
      </body>
    </html>
  );
}

3. Tracking Components

Create reusable components for common tracking patterns. This ensures consistency and reduces boilerplate.

Page View Tracker

Use a component that fires an event on mount. PostHog handles deduplication, so this is safe even in React StrictMode.

// src/components/tracking/PageViewTracker.tsx
'use client';

import { usePostHog } from 'posthog-js/react';
import { useEffect } from 'react';

interface PageViewTrackerProps {
  eventName: string;
  properties?: Record<string, unknown>;
}

export function PageViewTracker({ eventName, properties }: PageViewTrackerProps) {
  const posthog = usePostHog();

  useEffect(() => {
    posthog?.capture(eventName, properties);
  }, [posthog, eventName, properties]);

  return null;
}

Viewport Tracker

Use IntersectionObserver to track when elements become visible. This is more performant than scroll event listeners and accurately measures whether users see critical content.

// src/components/tracking/ViewportTracker.tsx
'use client';

import { usePostHog } from 'posthog-js/react';
import { useEffect, useRef, useCallback } from 'react';

interface ViewportTrackerProps {
  eventName: string;
  threshold?: number;
  properties?: Record<string, unknown>;
  children: React.ReactNode;
}

export function ViewportTracker({
  eventName,
  threshold = 0.5,
  properties,
  children,
}: ViewportTrackerProps) {
  const posthog = usePostHog();
  const ref = useRef<HTMLDivElement>(null);
  const hasFired = useRef(false);

  const handleIntersect = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      const [entry] = entries;
      if (entry.isIntersecting && !hasFired.current) {
        hasFired.current = true;
        posthog?.capture(eventName, properties);
      }
    },
    [posthog, eventName, properties]
  );

  useEffect(() => {
    const observer = new IntersectionObserver(handleIntersect, { threshold });
    const currentRef = ref.current;

    if (currentRef) {
      observer.observe(currentRef);
    }

    return () => {
      if (currentRef) {
        observer.unobserve(currentRef);
      }
    };
  }, [handleIntersect, threshold]);

  return <div ref={ref}>{children}</div>;
}

Action Tracker

For clicks, use inline capture with properties. This pattern captures user intent and context simultaneously.

// src/components/pricing/CheckoutButton.tsx
'use client';

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

interface CheckoutButtonProps {
  planName: string;
  price: number;
  currency: string;
  onClick: () => void;
  children: React.ReactNode;
}

export function CheckoutButton({ planName, price, currency, onClick, children }: CheckoutButtonProps) {
  const posthog = usePostHog();

  const handleClick = () => {
    posthog?.capture('payment_started', {
      plan_tier: planName,
      amount: price,
      currency: currency,
    });
    onClick();
  };

  return (
    <button onClick={handleClick} className="px-6 py-3 bg-blue-600 text-white rounded">
      {children}
    </button>
  );
}

4. Implementation Example

Assemble the components in your pricing page. This structure ensures all critical steps are tracked.

// app/pricing/page.tsx
import { PageViewTracker } from '@/components/tracking/PageViewTracker';
import { ViewportTracker } from '@/components/tracking/ViewportTracker';
import { CheckoutButton } from '@/components/pricing/CheckoutButton';

export default function PricingPage() {
  return (
    <main>
      <PageViewTracker eventName="landing_viewed" />
      
      <section className="hero">
        <h1>Unlock Lifetime Access</h1>
        <p>One-time payment for unlimited features.</p>
      </section>

      <ViewportTracker 
        eventName="pricing_visible" 
        threshold={0.3}
        properties={{ section: 'main_pricing' }}
      >
        <div className="pricing-card">
          <h2>Lifetime Deal</h2>
          <p className="price">$59</p>
          <CheckoutButton 
            planName="lifetime" 
            price={59} 
            currency="USD"
            onClick={() => window.location.href = '/checkout'}
          >
            Buy Now
          </CheckoutButton>
        </div>
      </ViewportTracker>
    </main>
  );
}

5. Funnel Construction

Once events flow into PostHog, navigate to Funnels > New Funnel. Add events in chronological order:

  1. landing_viewed
  2. pricing_visible
  3. payment_started

PostHog calculates conversion rates between steps. Analyze the drop-off to identify bottlenecks. If the conversion from pricing_visible to payment_started is low, investigate the pricing card design, copy clarity, or trust signals.

Pitfall Guide

  1. Missing Suspense Boundary

    • Explanation: Wrapping the PostHog provider in Suspense is mandatory in Next.js App Router. Without it, the provider may block rendering, causing pageview events to fail.
    • Fix: Always wrap the analytics provider in <Suspense fallback={null}> in the root layout.
  2. IntersectionObserver Memory Leaks

    • Explanation: Failing to disconnect the observer can lead to memory leaks, especially in single-page applications where components mount and unmount frequently.
    • Fix: Ensure the cleanup function in useEffect calls observer.disconnect() or observer.unobserve().
  3. Event Sprawl

    • Explanation: Tracking too many events creates noise and complicates analysis. It also risks hitting rate limits or free tier quotas unnecessarily.
    • Fix: Limit tracking to funnel-critical steps. Use properties to add context rather than creating new event types for minor variations.
  4. Omitting Event Properties

    • Explanation: Events without properties lack context. You cannot segment by plan type, price, or user segment later.
    • Fix: Always include relevant properties like plan_tier, amount, and currency in events like payment_started.
  5. Blocking Render with Heavy Logic

    • Explanation: Performing heavy computations inside useEffect or event handlers can degrade performance.
    • Fix: Keep tracking logic lightweight. PostHog's capture method is asynchronous and non-blocking. Avoid synchronous network calls in tracking handlers.
  6. StrictMode Double-Fire Confusion

    • Explanation: React StrictMode double-renders components in development, which might trigger events twice. PostHog deduplicates events by default, but custom logic might not.
    • Fix: Rely on PostHog's built-in deduplication. If using custom deduplication, ensure idempotency keys are handled correctly.
  7. Ignoring Privacy Compliance

    • Explanation: Tracking users without consent can violate GDPR or CCPA regulations.
    • Fix: Implement a cookie consent banner and conditionally initialize PostHog or disable tracking based on user preferences. Use posthog.opt_in_capturing() and posthog.opt_out_capturing().

Production Bundle

Action Checklist

  • Define event schema: List all funnel steps and required properties.
  • Install SDK: Run npm install posthog-js and configure environment variables.
  • Setup Provider: Create AnalyticsProvider with Suspense wrapper in root layout.
  • Implement Trackers: Add PageViewTracker, ViewportTracker, and inline captures to components.
  • Build Funnel: Create a new funnel in PostHog UI with events in order.
  • Test Instrumentation: Verify events appear in PostHog stream during local development.
  • Review Drop-offs: Analyze funnel data to identify conversion bottlenecks.
  • Iterate: Optimize UI based on funnel insights and re-measure.

Decision Matrix

Scenario Recommended Approach Why Cost Impact
Early-Stage SaaS Client-side tracking with PostHog Free Tier Low overhead, sufficient for <1M events/month, fast setup. $0 (up to 1M events).
High-Traffic Enterprise Server-side tracking + PostHog Paid Reduces client payload, ensures data integrity, higher event limits. Scales with event volume.
Privacy-First Product Conditional initialization + Opt-out Ensures GDPR/CCPA compliance, builds user trust. No direct cost; requires consent UI.
Complex Checkout Flow Multi-step funnel with properties Enables detailed analysis of each checkout stage. No extra cost; uses properties efficiently.

Configuration Template

Use environment variables to manage configuration securely.

# .env.local
NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_key
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
// src/config/posthog.ts
export const POSTHOG_CONFIG = {
  apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY || '',
  apiHost: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
  options: {
    capture_pageview: true,
    capture_pageleave: true,
    autocapture: false,
  },
};

Quick Start Guide

  1. Install Dependencies:
    npm install posthog-js
    
  2. Configure Environment: Add NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST to .env.local.
  3. Wrap Root Layout: Add AnalyticsProvider with Suspense to app/layout.tsx.
  4. Add Trackers: Insert PageViewTracker and ViewportTracker into your pricing page components.
  5. Verify Data: Open PostHog, navigate to Activity, and confirm events are flowing. Build your funnel to start analyzing conversions.
PostHog Custom Events: How I Tracked a $59 Payment Funnel from Page View to Stripe Checkout | Codcompass