Back to .md Directory

📋 Analytics Implementation Task Guide

Provides a step-by-step guide to integrate PostHog analytics into a Next.js project, including client and server-side tracking, user identification, and feature flags.

May 2, 2026
0 downloads
0 views
ai
View source

What this file does

Provides a step-by-step guide to integrate PostHog analytics into a Next.js project, including client and server-side tracking, user identification, and feature flags.

When to use it

  • Adding PostHog analytics to a Next.js app
  • Tracking client usage after sharing code
  • Implementing typed event tracking with TypeScript
  • Setting up server-side analytics in API routes

Assumes this stack

Next.jsTypeScriptPostHogReactNode.js

📋 Analytics Implementation Task Guide

Overview

This document explains exactly how the analytics system was implemented so you can replicate it in any project.


🎯 Goal

Implement PostHog analytics to track client usage even after sharing code. All events flow back to YOUR analytics instance.


📦 Step 1: Install Dependencies

npm install posthog-js

What it does: Installs PostHog JavaScript SDK for client-side tracking.


🏗️ Step 2: Create Core Analytics Service

File: lib/analytics.ts

This is the heart of the system - a singleton service that wraps PostHog.

Key Components:

A. Type-Safe Event Definitions

export type AnalyticsEvent = {
  'research_initiated': {
    query: string
    model: string
    criteriaCount: number
    userId?: string
  }
  'research_completed': {
    query: string
    model: string
    duration: number
    success: boolean
    resultsCount: number
    userId?: string
  }
  // ... add all your events here
}

Why: TypeScript ensures you never misspell event names or properties.

B. Analytics Class

class Analytics {
  private initialized = false
  private userId: string | null = null

  // Initialize PostHog
  initialize(apiKey: string, options?: { host?: string }) {
    if (this.initialized) return
    if (typeof window === 'undefined') return // Server-side check

    posthog.init(apiKey, {
      api_host: options?.host || 'https://app.posthog.com',
      capture_pageview: true,
      autocapture: true,
      session_recording: {
        maskAllInputs: true,
        maskTextSelector: '*',
      },
    })
    this.initialized = true
  }

  // Track events
  track<K extends keyof AnalyticsEvent>(
    event: K,
    properties: AnalyticsEvent[K]
  ) {
    if (!this.initialized) return
    
    posthog.capture(event, {
      ...properties,
      timestamp: new Date().toISOString(),
      environment: process.env.NODE_ENV,
      userId: this.userId || properties.userId || 'anonymous',
    })
  }

  // Identify users
  identify(userId: string, traits?: Record<string, any>) {
    if (!this.initialized) return
    this.userId = userId
    posthog.identify(userId, traits)
  }

  // Timer utility
  startTimer(operationName: string): () => number {
    const startTime = performance.now()
    return () => Math.round(performance.now() - startTime)
  }

  // Feature flags
  isFeatureEnabled(key: string): boolean {
    if (!this.initialized) return false
    return posthog.isFeatureEnabled(key) ?? false
  }
}

// Export singleton
export const analytics = new Analytics()

C. Server-Side Tracking Function

export async function trackServerEvent<K extends keyof AnalyticsEvent>(
  event: K,
  properties: AnalyticsEvent[K],
  apiKey?: string
) {
  if (!apiKey) return

  await fetch('https://app.posthog.com/capture/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      api_key: apiKey,
      event,
      properties: {
        ...properties,
        timestamp: new Date().toISOString(),
        environment: process.env.NODE_ENV,
      },
      timestamp: new Date().toISOString(),
    }),
  })
}

Why: API routes run server-side and need direct HTTP calls to PostHog.


🔌 Step 3: Create React Provider

File: components/analytics-provider-wrapper.tsx

Initializes analytics when the app loads and identifies users automatically.

"use client"

import { useEffect } from 'react'
import { useSession } from 'next-auth/react'
import { analytics } from '@/lib/analytics'

export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
  const { data: session, status } = useSession()

  // Initialize on mount
  useEffect(() => {
    const apiKey = process.env.NEXT_PUBLIC_POSTHOG_KEY
    const host = process.env.NEXT_PUBLIC_POSTHOG_HOST

    if (apiKey) {
      analytics.initialize(apiKey, { host })
    }
  }, [])

  // Identify user when logged in
  useEffect(() => {
    if (status === 'authenticated' && session?.user) {
      analytics.identify(session.user.email || 'unknown', {
        email: session.user.email,
        name: session.user.name,
      })
    } else if (status === 'unauthenticated') {
      analytics.reset()
    }
  }, [session, status])

  return <>{children}</>
}

Key Points:

  • Runs only on client ("use client")
  • Initializes once on app load
  • Auto-identifies authenticated users
  • Resets on logout

🎨 Step 4: Integrate Provider in Layout

File: app/layout.tsx

Wrap your entire app with the analytics provider.

import { AnalyticsProvider } from '@/components/analytics-provider-wrapper'

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

Why: Ensures analytics is initialized before any component renders.


🪝 Step 5: Create React Hook (Optional)

File: lib/hooks/useAnalytics.ts

Makes it easier to track events in components.

import { useCallback } from 'react'
import { analytics, type AnalyticsEvent } from '@/lib/analytics'

export function useAnalytics() {
  const track = useCallback(<K extends keyof AnalyticsEvent>(
    event: K,
    properties: AnalyticsEvent[K]
  ) => {
    analytics.track(event, properties)
  }, [])

  const startTimer = useCallback((operationName: string) => {
    return analytics.startTimer(operationName)
  }, [])

  return { track, startTimer, identify: analytics.identify.bind(analytics) }
}

Usage in components:

const { track, startTimer } = useAnalytics()

const handleClick = () => {
  track('button_clicked', { buttonName: 'submit' })
}

🔧 Step 6: Add Tracking to API Routes

Example: app/api/research/route.ts

Import the tracking function:

import { trackServerEvent } from '@/lib/analytics'

Track on success:

export const POST = async (req: Request) => {
  try {
    const body = await req.json()
    const { city, model, criteria } = body

    // Track event
    await trackServerEvent('research_initiated', {
      query: city,
      model: model || 'default',
      criteriaCount: criteria?.length || 0,
    }, process.env.NEXT_PUBLIC_POSTHOG_KEY)

    // ... your logic here ...

    return NextResponse.json({ success: true })
  } catch (error) {
    // Track errors
    await trackServerEvent('api_error', {
      endpoint: '/api/research',
      errorMessage: error instanceof Error ? error.message : 'Unknown error',
      statusCode: 500,
    }, process.env.NEXT_PUBLIC_POSTHOG_KEY)

    return NextResponse.json({ error: 'Failed' }, { status: 500 })
  }
}

Pattern:

  1. Import trackServerEvent
  2. Track success events with relevant data
  3. Track errors in catch blocks
  4. Always pass process.env.NEXT_PUBLIC_POSTHOG_KEY

🎯 Step 7: Add Tracking to Components

Example: components/research-panel.tsx

Import analytics:

import { analytics } from '@/lib/analytics'

Track user interactions:

const handleSearch = async () => {
  // Track search initiated
  analytics.track('research_initiated', {
    query: cityInput,
    model: model,
    criteriaCount: criteria.length,
  })

  // Time the operation
  const endTimer = analytics.startTimer('search_operation')

  try {
    const response = await fetch('/api/research', { /* ... */ })
    const duration = endTimer()

    // Track success
    analytics.track('research_completed', {
      query: cityInput,
      model: model,
      duration,
      success: true,
      resultsCount: data.results?.length || 0,
    })
  } catch (error) {
    // Track failure
    analytics.track('research_failed', {
      query: cityInput,
      model: model,
      error: error.message,
    })
  }
}

Best Practices:

  • Track at the START of operations
  • Track at the END (success/failure)
  • Use timers for duration tracking
  • Include context (query, model, etc.)

🌍 Step 8: Environment Configuration

File: .env.local (create this, never commit)

NEXT_PUBLIC_POSTHOG_KEY=phc_your_actual_key_here
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com

File: .env.example (commit this)

# Analytics Configuration (PostHog)
# Sign up at https://posthog.com or self-host
# Get your API key and paste below
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com

Why NEXT_PUBLIC_?

  • Prefix makes it available in browser
  • Next.js bakes it into the build
  • Client code can access it

🧪 Step 9: Create Test Utilities

File: lib/analytics-test.ts

export const quickVerify = () => {
  const results = {
    posthogLoaded: typeof window !== 'undefined' && !!(window as any).posthog,
    posthogInitialized: !!(window as any).posthog?.__loaded,
    apiKeyPresent: !!process.env.NEXT_PUBLIC_POSTHOG_KEY,
    trackingWorks: false,
  }

  try {
    analytics.track('test_event', { test: true } as any)
    results.trackingWorks = true
  } catch {
    results.trackingWorks = false
  }

  console.table(results)
  return results
}

Usage: Run in browser console to verify setup.


📊 Step 10: Enhance Analytics Dashboard

File: components/analytics-logs.tsx

Show real-time analytics info to users (optional).

"use client"

import { useEffect, useState } from 'react'
import posthog from 'posthog-js'

export function AnalyticsLogs() {
  const [logs, setLogs] = useState<any[]>([])

  useEffect(() => {
    // Check if PostHog is initialized
    const isInitialized = typeof window !== 'undefined' && posthog.__loaded

    if (isInitialized) {
      setLogs([{ 
        message: 'Analytics tracking active',
        type: 'INFO' 
      }])
    } else {
      setLogs([{ 
        message: 'Configure PostHog in .env.local',
        type: 'WARNING' 
      }])
    }
  }, [])

  return (
    <div>
      {/* Display logs */}
    </div>
  )
}

🔒 Step 11: Security & Deployment

For Client Deployment:

  1. Keep analytics configured - Don't remove PostHog code
  2. Use YOUR API key - Bake it into the build
  3. Deploy normally - npm run build && npm start
  4. Monitor immediately - Events flow to YOUR dashboard

Environment Variable Strategy:

# Local Development (.env.local - gitignored)
NEXT_PUBLIC_POSTHOG_KEY=phc_your_dev_key

# Production (Vercel/Netlify/etc)
NEXT_PUBLIC_POSTHOG_KEY=phc_your_prod_key

# Client's Environment (they never see this)
# Key is baked into build at deploy time

📋 Complete Event List

Add these to your AnalyticsEvent type:

export type AnalyticsEvent = {
  // Research
  'research_initiated': { query: string; model: string; criteriaCount: number }
  'research_completed': { query: string; duration: number; success: boolean }
  'research_failed': { query: string; error: string }
  
  // Model
  'model_changed': { previousModel: string; newModel: string }
  'model_settings_updated': { model: string; settings: any }
  
  // Criteria
  'custom_criteria_added': { criteriaName: string; criteriaType: string }
  'custom_criteria_removed': { criteriaName: string }
  
  // Auth
  'user_signed_in': { userId: string; method: string }
  'user_signed_up': { userId: string }
  'user_signed_out': { userId: string }
  
  // Errors
  'api_error': { endpoint: string; errorMessage: string; statusCode?: number }
  'client_error': { component: string; errorMessage: string }
  
  // Performance
  'api_response_time': { endpoint: string; duration: number; success: boolean }
  'page_load': { page: string; loadTime: number }
}

🎯 Usage Patterns

Pattern 1: Track Simple Event

analytics.track('button_clicked', {
  buttonName: 'submit',
  userId: currentUser.id,
})

Pattern 2: Track with Duration

const endTimer = analytics.startTimer('operation')
await doSomething()
const duration = endTimer()

analytics.track('operation_completed', {
  operation: 'data_fetch',
  duration,
  success: true,
})

Pattern 3: Track Error

try {
  await riskyOperation()
} catch (error) {
  analytics.track('operation_failed', {
    operation: 'data_fetch',
    error: error.message,
  })
}

Pattern 4: Server-Side Tracking

// In API route
await trackServerEvent('api_called', {
  endpoint: '/api/endpoint',
  method: 'POST',
}, process.env.NEXT_PUBLIC_POSTHOG_KEY)

🚀 Quick Setup Checklist

When implementing in a new project:

  • Install posthog-js
  • Create lib/analytics.ts with Analytics class
  • Create components/analytics-provider-wrapper.tsx
  • Add provider to app/layout.tsx
  • Create lib/hooks/useAnalytics.ts (optional)
  • Add tracking to API routes
  • Add tracking to components
  • Create .env.local with PostHog key
  • Create .env.example template
  • Test with quickVerify() function
  • Deploy and verify events in PostHog

🔍 Debugging

Analytics not working?

  1. Check initialization:

    // In browser console
    console.log(window.posthog)
    console.log(window.posthog.__loaded)
    
  2. Check environment variable:

    console.log(process.env.NEXT_PUBLIC_POSTHOG_KEY)
    
  3. Check network requests:

    • Open DevTools → Network
    • Look for app.posthog.com/capture/
    • Should see 200 OK responses
  4. Run verification:

    import { quickVerify } from '@/lib/analytics-test'
    quickVerify()
    

📚 PostHog Setup

Option 1: Cloud (5 minutes)

  1. Go to https://posthog.com/signup
  2. Create account
  3. Create new project
  4. Copy API key (starts with phc_)
  5. Paste in .env.local

Option 2: Self-Hosted (30 minutes)

# Clone PostHog
git clone https://github.com/PostHog/posthog.git
cd posthog

# Start with Docker
docker-compose up -d

# Access at http://localhost:8000
# Create project and get API key

Self-Hosted Benefits:

  • Complete data ownership
  • No external dependencies
  • Maximum privacy compliance
  • Unlimited events

🎯 Key Concepts

Why This Works for Client Tracking:

  1. API key in build: NEXT_PUBLIC_ variables are baked into JavaScript bundle
  2. Your instance: Events always go to YOUR PostHog (your API key)
  3. Client deploys: They deploy the built code with your key embedded
  4. You monitor: All their events flow to your dashboard

Architecture:

Client's Deployment
    ↓
Your Built Code (with your PostHog key)
    ↓
User Actions → Browser → PostHog.capture()
    ↓
YOUR PostHog Instance (Cloud or Self-Hosted)
    ↓
Your Analytics Dashboard

🔄 Replication Steps

To replicate in any project:

  1. Copy Files:

    • lib/analytics.ts
    • components/analytics-provider-wrapper.tsx
    • lib/hooks/useAnalytics.ts
    • lib/analytics-test.ts
  2. Install Package:

    npm install posthog-js
    
  3. Integrate:

    • Wrap app with <AnalyticsProvider>
    • Add tracking to API routes
    • Add tracking to components
  4. Configure:

    • Get PostHog API key
    • Add to .env.local
    • Test with quickVerify()
  5. Deploy:

    • Build with npm run build
    • Deploy to production
    • Monitor events in PostHog

✅ Success Criteria

You've successfully implemented analytics when:

  • quickVerify() shows all green checks
  • ✅ Events appear in PostHog dashboard within 60 seconds
  • ✅ Session replays are recording
  • ✅ No console errors related to PostHog
  • ✅ Network tab shows successful capture requests
  • ✅ User identification works after login

🎓 Advanced Features

Feature Flags:

if (analytics.isFeatureEnabled('new-ui')) {
  // Show new UI
}

A/B Testing:

const variant = analytics.getFeatureFlag('pricing-test')
// Returns 'control' or 'test'

Cohorts:

Create user groups in PostHog dashboard based on behavior.

Custom Dashboards:

Build dashboards in PostHog with:

  • Total events by type
  • User engagement metrics
  • Performance trends
  • Error rates

📝 Final Notes

Remember:

  • This implementation is production-ready
  • It scales to millions of events
  • Self-hosting gives you complete control
  • Client deployments automatically track to YOUR instance
  • No additional client configuration needed

Cost:

  • PostHog Cloud: Free tier → 1M events/month
  • Self-Hosted: Only infrastructure costs (AWS, DigitalOcean, etc.)

Privacy:

  • GDPR compliant
  • Automatic PII masking
  • Data retention controls
  • User opt-out support

🎉 Done!

You now have a complete blueprint for implementing analytics in any Next.js project. Just follow this guide step-by-step and you'll have enterprise-grade tracking in under an hour.

Key Takeaway: Once implemented, you maintain complete visibility into how clients use your code, even after deployment!

What's inside

11 numbered steps, 3 code files, 1 event type definition, 1 test utility, 1 checklist

Change this for your project

  • Replace NEXT_PUBLIC_POSTHOG_KEY=phc_your_actual_key_here with your own PostHog API key
  • Replace NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com with your self-hosted URL if applicable
  • Replace 'research_initiated' and other event names in AnalyticsEvent type with your own events

Where it goes

Keep it in your repository where the agent or team that needs it will read it.

Worth borrowing

  • Using a singleton analytics service with TypeScript generics for type-safe event tracking
  • Separating client-side and server-side tracking functions for isomorphic apps
  • Wrapping analytics initialization in a React provider that auto-identifies authenticated users

Related Documents