Skip to main content
Sabo includes pre-configured PostHog analytics with automatic pageview tracking, custom event support, and feature flags. PostHog is optional and only activates when environment variables are set.

Overview

PostHog is an open-source product analytics platform that helps you understand how users interact with your application. Sabo’s PostHog integration provides:
  • Automatic pageview tracking for all routes
  • Custom event tracking via the usePostHog() hook
  • Server-side analytics for API routes and server components
  • Feature flags for gradual feature rollouts
  • Session recording (configurable in PostHog dashboard)
  • Privacy-first analytics with GDPR compliance options
PostHog is completely optional. If you don’t set the environment variables, Sabo will function normally without analytics.
Need a deeper dive into every PostHog feature (insights, dashboards, cohorts)? Keep the official PostHog documentation handy while working through this guide.

Quick Setup

1

Create PostHog account

Sign up for PostHog to get your project API key.
  1. Visit PostHog Cloud or self-host PostHog
  2. Create a new project
  3. Copy your Project API Key from Project Settings
  4. Note your PostHog host URL:
    • Cloud: https://app.posthog.com (US) or https://eu.posthog.com (EU)
    • Self-hosted: Your custom domain
PostHog offers a generous free tier with 1 million events per month. No credit card required for signup.
2

Add environment variables

Add PostHog credentials to your .env.local file:
.env.local
# PostHog Analytics (Optional)
NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_api_key_here
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Important: Both variables must be present for PostHog to initialize. If either is missing, PostHog will remain disabled.
Restart your development server after adding environment variables.
3

Verify integration

Test that PostHog is tracking events:
  1. Start your dev server: pnpm dev
  2. Visit http://localhost:3000
  3. Open browser console and look for PostHog debug messages (in development mode)
  4. Check PostHog dashboard → Activity to see live pageviews
In development mode, PostHog automatically enables debug logging. Check your browser console for [PostHog] messages.

How It Works

Architecture

Sabo’s PostHog integration consists of three main parts:
  1. Client-side provider (src/components/posthog-provider.tsx)
    • Initializes PostHog in the browser
    • Wraps the entire app to provide PostHog context
    • Enables debug mode in development
  2. Server-side client (src/lib/posthog/server.ts)
    • Tracks events from API routes and server components
    • Uses PostHog Node.js SDK
    • Flushes events immediately for real-time tracking
  3. Exports module (src/lib/posthog/index.ts)
    • Centralizes imports for both client and server utilities
    • Re-exports usePostHog hook and getPostHogClient function

Initialization

PostHog initializes automatically when the app loads, but only if environment variables are set.
src/components/posthog-provider.tsx
"use client";

import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";

if (typeof window !== "undefined") {
  const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
  const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;

  if (posthogKey && posthogHost) {
    posthog.init(posthogKey, {
      api_host: posthogHost,
      loaded: (posthog) => {
        if (process.env.NODE_ENV === "development") {
          posthog.debug();  // Enable debug logging in dev
        }
      },
    });
  }
}

export function PHProvider({ children }: { children: React.ReactNode }) {
  return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
The PHProvider wraps your entire app in src/app/layout.tsx:
src/app/layout.tsx
import { PHProvider } from "@/components/posthog-provider";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <PHProvider>
          <ThemeProvider>
            {children}
          </ThemeProvider>
        </PHProvider>
      </body>
    </html>
  );
}
PHProvider wraps ThemeProvider to ensure PostHog context is available throughout the entire application, including all client components.

Tracking Events

Automatic Pageview Tracking

PostHog automatically tracks pageviews for all routes in your app. No additional code required. What’s tracked:
  • Route changes (including Next.js App Router navigation)
  • URL parameters and query strings
  • Referrer information
  • Browser and device details
Viewing pageviews:
  1. Go to PostHog Dashboard → Activity
  2. Filter by event type: $pageview
  3. See real-time page visits with full URL paths

Custom Event Tracking (Client-Side)

Track user interactions, button clicks, form submissions, and other custom events using the usePostHog() hook.
1

Import the hook

import { usePostHog } from "@/lib/posthog";
2

Call the hook in your component

export function MyComponent() {
  const posthog = usePostHog();
  
  // Component logic...
}
3

Track events

const handleClick = () => {
  // Track event with properties
  posthog?.capture("button_clicked", {
    button_name: "upgrade_to_pro",
    location: "pricing_page",
    plan: "pro",
  });
};

Complete Example

src/components/marketing/pricing.tsx
"use client";

import { usePostHog } from "@/lib/posthog";
import { Button } from "@/components/ui/button";

export function PricingCard({ plan }) {
  const posthog = usePostHog();
  
  const handleUpgrade = () => {
    // Track subscription initiation
    posthog?.capture("subscription_started", {
      plan_id: plan.id,
      plan_name: plan.name,
      price: plan.monthlyPrice,
      billing_cycle: "monthly",
    });
    
    // Proceed with checkout...
  };
  
  return (
    <Button onClick={handleUpgrade}>
      Upgrade to {plan.name}
    </Button>
  );
}
Use optional chaining (posthog?.capture) to safely handle cases where PostHog is not initialized (e.g., missing environment variables).

Common Events to Track

Here are some recommended events to track in a SaaS application:
// User actions
posthog?.capture("user_signed_up", { method: "email" });
posthog?.capture("user_logged_in", { method: "google_oauth" });
posthog?.capture("user_logged_out");

// Subscription events
posthog?.capture("subscription_started", { plan: "pro", cycle: "yearly" });
posthog?.capture("subscription_cancelled", { reason: "too_expensive" });
posthog?.capture("subscription_upgraded", { from: "free", to: "pro" });

// Feature usage
posthog?.capture("feature_used", { feature_name: "dashboard_export" });
posthog?.capture("search_performed", { query: "analytics", results_count: 12 });
posthog?.capture("file_uploaded", { file_type: "pdf", file_size: 2048 });

// Engagement
posthog?.capture("form_submitted", { form_name: "contact", success: true });
posthog?.capture("video_watched", { video_id: "onboarding", duration: 180 });
posthog?.capture("invite_sent", { invite_count: 3 });

Server-Side Event Tracking

Track events from API routes, server actions, or server components using the server-side PostHog client.
src/app/api/your-route/route.ts
import { getPostHogClient } from "@/lib/posthog";

export async function POST(request: Request) {
  const posthog = getPostHogClient();
  
  // Track server-side event
  posthog?.capture({
    distinctId: userId,  // User identifier
    event: "api_call_made",
    properties: {
      endpoint: "/api/data",
      method: "POST",
      response_time: 142,
    },
  });
  
  // Your API logic...
  
  return Response.json({ success: true });
}
The server client (src/lib/posthog/server.ts) is configured to flush events immediately:
src/lib/posthog/server.ts
import { PostHog } from "posthog-node";

let posthogClient: PostHog | null = null;

export function getPostHogClient(): PostHog | null {
  const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
  const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;

  if (!posthogKey || !posthogHost) {
    return null;  // PostHog disabled
  }

  if (!posthogClient) {
    posthogClient = new PostHog(posthogKey, {
      host: posthogHost,
      flushAt: 1,           // Flush after each event
      flushInterval: 0,     // Don't batch events
    });
  }
  
  posthogClient.debug(true);  // Enable debug logging
  return posthogClient;
}
Server-side tracking requires a distinctId (usually the user ID) to associate events with specific users. Make sure to provide this when capturing server events.

User Identification

Identifying Users

Call posthog.identify() in whichever component completes your authentication flow (for example, the /sign-in page) so future events are tied to the logged-in user:
"use client";

import { usePostHog } from "@/lib/posthog";

export function SignInForm() {
  const posthog = usePostHog();
  
  const handleSignIn = async (email: string) => {
    const user = await signIn(email); // Your existing auth logic
    
    posthog?.identify(user.id, {
      email: user.email,
      name: user.full_name,
      plan: "free",
      created_at: user.created_at,
    });
    
    posthog?.capture("user_signed_in", { method: "email" });
  };
}

Setting User Properties

Update user properties as they change:
// Update subscription status
posthog?.people.set({
  plan: "pro",
  mrr: 12,
  billing_cycle: "monthly",
});

// Track trait changes
posthog?.capture("subscription_upgraded", {
  from_plan: "free",
  to_plan: "pro",
});

Resetting User Identity

Clear user identification on logout:
const handleLogout = async () => {
  await signOut();
  
  // Reset PostHog identity
  posthog?.reset();
};

Feature Flags

PostHog feature flags allow you to roll out features gradually, run A/B tests, and toggle features without deploying code.

Using Feature Flags (Client-Side)

"use client";

import { usePostHog } from "@/lib/posthog";
import { useEffect, useState } from "react";

export function NewFeature() {
  const posthog = usePostHog();
  const [showNewUI, setShowNewUI] = useState(false);
  
  useEffect(() => {
    // Check feature flag value
    const isEnabled = posthog?.isFeatureEnabled("new-dashboard-ui");
    setShowNewUI(isEnabled ?? false);
  }, [posthog]);
  
  return showNewUI ? <NewDashboard /> : <OldDashboard />;
}

Feature Flag with Variants

const variant = posthog?.getFeatureFlag("pricing-experiment");

// Show different pricing based on variant
if (variant === "high-price") {
  return <PricingCard price={29} />;
} else if (variant === "low-price") {
  return <PricingCard price={19} />;
} else {
  return <PricingCard price={24} />;  // Default
}

Creating Feature Flags

  1. Go to PostHog Dashboard → Feature Flags
  2. Click “New feature flag”
  3. Configure:
    • Key: new-dashboard-ui (use in code)
    • Rollout: Percentage (e.g., 50% of users)
    • Filters: Target specific users, groups, or properties
  4. Save and the flag is immediately available
Feature flags are cached locally, so changes may take a few minutes to propagate to all users. You can force a refresh by calling posthog?.reloadFeatureFlags().
Want to convert those flags into statistically sound experiments? PostHog’s A/B testing guide explains how to define variants, success metrics, and rollouts using the same flag keys shown above.

Configuration Options

PostHog Initialization Options

You can customize PostHog behavior by modifying src/components/posthog-provider.tsx:
src/components/posthog-provider.tsx
posthog.init(posthogKey, {
  api_host: posthogHost,
  
  // Session recording (default: disabled)
  disable_session_recording: false,
  
  // Autocapture clicks and form submissions (default: true)
  autocapture: true,
  
  // Capture pageviews automatically (default: true)
  capture_pageview: true,
  
  // Capture pageleave events (default: true)
  capture_pageleave: true,
  
  // Respect Do Not Track browser setting (default: false)
  respect_dnt: true,
  
  // Debug mode (automatically enabled in development)
  loaded: (posthog) => {
    if (process.env.NODE_ENV === "development") {
      posthog.debug();
    }
  },
});

Privacy Options

Configure PostHog to respect user privacy:
// Disable session recording for sensitive pages
useEffect(() => {
  if (window.location.pathname.includes("/settings/account")) {
    posthog?.stopSessionRecording();
  }
}, [posthog]);

// Opt user out of tracking
posthog?.opt_out_capturing();

// Opt user back in
posthog?.opt_in_capturing();

// Check if user has opted out
const hasOptedOut = posthog?.has_opted_out_capturing();

Best Practices

If you need a formal naming taxonomy for events and properties, refer to PostHog’s event taxonomy guide. It includes naming tables, spreadsheet templates, and enforcement strategies that complement the recommendations below.
Use consistent, descriptive names for events:Good:
  • subscription_started
  • user_signed_up
  • file_uploaded
Bad:
  • click
  • event1
  • test
Guidelines:
  • Use snake_case for event names
  • Use past tense verbs (clicked, not click)
  • Be specific (checkout_completed vs purchase)
  • Group related events with prefixes (subscription_*, user_*)
Include relevant context with every event:
posthog?.capture("file_uploaded", {
  file_type: "pdf",           // What kind
  file_size: 2048,            // How large
  upload_method: "drag_drop", // How uploaded
  location: "dashboard",      // Where
  timestamp: Date.now(),      // When
});
Guidelines:
  • Keep property names consistent across events
  • Include user properties (plan, role, etc.)
  • Add context (page location, user state)
  • Use appropriate data types (numbers for counts, booleans for flags)
PostHog batches events by default, but you can optimize further:
// Debounce high-frequency events
const debouncedTrack = debounce((searchTerm) => {
  posthog?.capture("search_performed", { query: searchTerm });
}, 500);

// Track only on significant actions
const handleScroll = () => {
  if (scrollPercent > 75 && !tracked) {
    posthog?.capture("article_read_75_percent");
    setTracked(true);
  }
};
Tips:
  • Don’t track every keystroke or mouse movement
  • Batch similar events (e.g., track scroll milestones: 25%, 50%, 75%, 100%)
  • Use feature flags to disable analytics in staging environments
Verify events are tracked correctly:In Development:
  • Check browser console for [PostHog] debug messages
  • Visit PostHog Dashboard → Activity (live view)
  • Use PostHog’s “Test mode” to see events in real-time
Before Production:
  • Test with different user states (logged in/out, different plans)
  • Verify user identification works after sign-in
  • Check that properties contain expected values
  • Test feature flags with different rollout percentages
// Add logging in development
if (process.env.NODE_ENV === "development") {
  console.log("Tracking event:", eventName, properties);
}
posthog?.capture(eventName, properties);

Production Deployment

1

Update environment variables

Set PostHog credentials in your production environment (Vercel, Netlify, etc.):
NEXT_PUBLIC_POSTHOG_KEY=phc_your_production_key
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Use separate PostHog projects for development and production to keep analytics data isolated.
2

Disable debug mode

Ensure debug mode is only enabled in development (already configured in posthog-provider.tsx):
loaded: (posthog) => {
  if (process.env.NODE_ENV === "development") {
    posthog.debug();  // Only in dev
  }
}
3

Configure session recording

Decide whether to enable session recording in PostHog Dashboard:
  1. Go to Settings → Project Settings → Recordings
  2. Enable/disable session recording
  3. Configure privacy settings:
    • Mask sensitive inputs (passwords, credit cards)
    • Block specific elements by CSS selector
    • Disable recording on certain pages
Session recording captures user interactions. Ensure you comply with GDPR/privacy regulations and disclose this in your privacy policy.
4

Set up alerts

Configure PostHog to notify you of important events:
  1. Go to Alerts → Create alert
  2. Choose trigger (e.g., “Subscription cancelled” event)
  3. Set threshold and frequency
  4. Connect to Slack, email, or webhooks
Test alerts in PostHog to ensure notifications are working before going live.

Troubleshooting

Symptoms: No events in PostHog dashboard, no debug messages in console.Causes:
  • Missing environment variables
  • Incorrect API key or host
  • Variables not prefixed with NEXT_PUBLIC_
Fix:
  1. Verify both NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST are set in .env.local
  2. Restart dev server after adding variables
  3. Check browser console for PostHog errors
  4. Confirm API key format: phc_...
  5. Use correct host (US: https://app.posthog.com, EU: https://eu.posthog.com)
Symptoms: PostHog initializes but events don’t show up.Causes:
  • Wrong project API key
  • Ad blockers blocking PostHog
  • Network issues
  • PostHog ingestion delay
Fix:
  1. Check you’re viewing the correct project in PostHog dashboard
  2. Disable ad blockers (uBlock, Privacy Badger often block analytics)
  3. Check browser network tab for failed requests to PostHog
  4. Wait 1-2 minutes for events to appear (PostHog has slight delay)
  5. Verify API key in PostHog → Project Settings → Project API Key
Symptoms: React error when calling usePostHog().Causes:
  • Missing "use client" directive
  • Component is server component
  • PostHog not imported correctly
Fix:
  1. Add "use client" at top of file using usePostHog()
  2. Import from @/lib/posthog, not posthog-js directly
  3. Ensure PHProvider wraps your component tree in layout.tsx
"use client";  // Add this!

import { usePostHog } from "@/lib/posthog";  // Correct import

export function MyComponent() {
  const posthog = usePostHog();
  // ...
}
Symptoms: getPostHogClient() returns null on server.Causes:
  • Environment variables not set in production
  • Variables missing NEXT_PUBLIC_ prefix
Fix:
  1. Verify environment variables are set in hosting platform (Vercel, Netlify)
  2. Both client and server use NEXT_PUBLIC_* variables (this is intentional for consistency)
  3. Redeploy after adding environment variables
  4. Check server logs for PostHog initialization messages
Symptoms: isFeatureEnabled() always returns false.Causes:
  • Feature flag not created in PostHog
  • User not identified (posthog.identify() not called)
  • Flag filters don’t match current user
  • Feature flags not loaded yet
Fix:
  1. Create feature flag in PostHog Dashboard → Feature Flags
  2. Call posthog.identify(userId) after authentication
  3. Check flag filters/rollout percentage
  4. Wait for flags to load or call posthog.reloadFeatureFlags()
useEffect(() => {
  // Wait for feature flags to load
  posthog?.onFeatureFlags(() => {
    const isEnabled = posthog.isFeatureEnabled("flag-name");
    setShowFeature(isEnabled);
  });
}, [posthog]);

Privacy & GDPR Compliance

PostHog provides tools to help you comply with privacy regulations: PostHog sets cookies (ph_*) to track users. Implement cookie consent before initializing:
src/components/posthog-provider.tsx
if (typeof window !== "undefined") {
  // Check for user consent before initializing
  const hasConsent = getCookieConsent(); // Your consent logic
  
  if (hasConsent && posthogKey && posthogHost) {
    posthog.init(posthogKey, {
      api_host: posthogHost,
      // ...
    });
  }
}

Data Deletion Requests

Handle GDPR deletion requests via PostHog API:
// In your data deletion API route
await fetch(`https://app.posthog.com/api/persons/${userId}`, {
  method: 'DELETE',
  headers: {
    'Authorization': `Bearer ${process.env.POSTHOG_PERSONAL_API_KEY}`
  }
});

Anonymization

Anonymize sensitive data before tracking:
// Don't track PII directly
posthog?.capture("form_submitted", {
  email_domain: email.split("@")[1],  // Only domain
  user_id_hash: hashUserId(userId),   // Hash instead of raw ID
});
Review your privacy policy to disclose PostHog usage. Sabo includes placeholder legal pages in src/content/legal/ that mention PostHog—update these with your specific implementation.