Analytics

beginner*

Privacy-friendly analytics with PostHog. Includes event tracking, user identification, and feature flag integration.

analyticsposthogtrackingeventsprivacy
Tested on201619TS5.9
$ bunx sinew add developer-experience/analytics
Interactive demo coming soon

1The Problem

Tracking user behavior requires:

  • Privacy-respecting data collection
  • Custom event tracking
  • User identification
  • Server-side tracking support

2The Solution

Use PostHog for product analytics with event tracking, user identification, and feature flag integration. Includes both client-side and server-side tracking utilities.

3Files

lib/analytics/client.ts

lib/analytics/client.tsTypeScript
import posthog from "posthog-js";

// Module-level guard so we don't re-init on Fast Refresh or repeated mounts.
let hasInitialized = false;

// Initialize PostHog (call once in your app)
export function initAnalytics() {
  if (typeof window === "undefined") return;
  if (hasInitialized) return;
  hasInitialized = true;

  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    // US cloud. Use https://eu.i.posthog.com for the EU region.
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
    capture_pageview: false, // We'll capture manually
    capture_pageleave: true,
    persistence: "localStorage+cookie",
  });
}

// Identify user (call after login)
export function identifyUser(userId: string, properties?: Record<string, unknown>) {
  posthog.identify(userId, properties);
}

// Reset user (call after logout)
export function resetUser() {
  posthog.reset();
}

// Track page view
export function trackPageView(url?: string) {
  posthog.capture("$pageview", {
    $current_url: url || window.location.href,
  });
}

// Track custom event
export function trackEvent(event: string, properties?: Record<string, unknown>) {
  posthog.capture(event, properties);
}

lib/analytics/events.ts

lib/analytics/events.tsTypeScript
import { trackEvent } from "./client";

// Type-safe event tracking
export const Events = {
  // User events
  signUp: (method: "email" | "google" | "github") => trackEvent("user_signed_up", { method }),

  login: (method: "email" | "google" | "github") => trackEvent("user_logged_in", { method }),

  // Conversion events
  trialStarted: (plan: string) => trackEvent("trial_started", { plan }),

  subscriptionStarted: (plan: string, interval: "monthly" | "yearly") =>
    trackEvent("subscription_started", { plan, interval }),

  // E-commerce events
  purchaseCompleted: (orderId: string, total: number, items: number) =>
    trackEvent("purchase_completed", { orderId, total, items }),

  // Engagement events
  searchPerformed: (query: string, resultsCount: number) =>
    trackEvent("search_performed", { query, resultsCount }),
};

components/analytics-provider.tsx

components/analytics-provider.tsxTSX
"use client";

import { useEffect } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { initAnalytics, trackPageView } from "@/lib/analytics/client";

export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  // Initialize PostHog on mount
  useEffect(() => {
    initAnalytics();
  }, []);

  // Track page views on route change
  useEffect(() => {
    if (pathname) {
      const url = searchParams.toString() ? `${pathname}?${searchParams.toString()}` : pathname;
      trackPageView(url);
    }
  }, [pathname, searchParams]);

  return <>{children}</>;
}

lib/analytics/server.ts

lib/analytics/server.tsTypeScript
import { PostHog } from "posthog-node";

let posthogServer: PostHog | null = null;

export function getServerPostHog(): PostHog {
  if (!posthogServer) {
    posthogServer = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
      // US cloud. Use https://eu.i.posthog.com for the EU region.
      host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
      flushAt: 1,
      flushInterval: 0,
    });
  }
  return posthogServer;
}

// Track server-side event
export async function trackServerEvent(
  distinctId: string,
  event: string,
  properties?: Record<string, unknown>
) {
  const posthog = getServerPostHog();
  posthog.capture({ distinctId, event, properties });
  await posthog.flush();
}

4Dependencies

$ bun add posthog-js posthog-node

5Configuration

Environment Variables

| Variable | Description | Required | | -------------------------- | ----------------------- | -------- | | NEXT_PUBLIC_POSTHOG_KEY | PostHog project API key | Yes | | NEXT_PUBLIC_POSTHOG_HOST | PostHog host (US or EU) | No |

6Usage

Add Provider to Layout

import { Suspense } from "react";
import { AnalyticsProvider } from "@/components/analytics-provider";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Suspense fallback={null}>
          <AnalyticsProvider>{children}</AnalyticsProvider>
        </Suspense>
      </body>
    </html>
  );
}
TSX

Track Events

import { Events } from "@/lib/analytics/events";

// On sign up
Events.signUp("google");

// On purchase
Events.purchaseCompleted("order_123", 99.99, 3);
TypeScript

7Alternatives

  • PostHog - Open-source with feature flags
  • Plausible - Privacy-focused, no cookies
  • Umami - Open-source, self-hostable
  • Mixpanel - Advanced funnel analysis

Related patterns