Skip to main content
Shipr uses PostHog for product analytics, routed through a Next.js reverse proxy to avoid ad-blockers.

Quick Start

1

Get PostHog API Key

Sign up at posthog.com and create a new project. Copy your project API key from the settings page.
2

Add Environment Variables

Add the following to your .env.local file:
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
3

Verify Installation

The analytics components are already integrated in src/app/layout.tsx. No additional setup is required.

Architecture

Three components handle analytics in the root layout:
ComponentFilePurpose
PostHogProvidersrc/components/posthog-provider.tsxInitializes the PostHog client and wraps the app
PostHogPageviewsrc/components/posthog-pageview.tsxCaptures $pageview on route changes
PostHogIdentifysrc/components/posthog-identify.tsxLinks Clerk user identity to PostHog person

PostHog Provider

The provider initializes the PostHog client on the client side:
src/components/posthog-provider.tsx
"use client";

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

if (
  typeof window !== "undefined" &&
  process.env.NEXT_PUBLIC_POSTHOG_KEY &&
  process.env.NEXT_PUBLIC_POSTHOG_HOST
) {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
    api_host:
      process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
    ui_host: "https://us.posthog.com",
    person_profiles: "identified_only",
    capture_pageview: false,
    capture_pageleave: true,
    debug: false,
  });
}

export function PostHogProvider({ children }: { children: ReactNode }) {
  return <PHProvider client={posthog}>{children}</PHProvider>;
}
capture_pageview is set to false because pageviews are captured manually by PostHogPageview to work correctly with Next.js App Router navigation.

Reverse Proxy Setup

PostHog requests are proxied through Next.js rewrites to bypass ad-blockers. This is configured in next.config.ts:
next.config.ts
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*",
    },
  ];
}
This routes:
  • /ingest/static/* to https://us-assets.i.posthog.com/static/*
  • /ingest/* to https://us.i.posthog.com/*

User Identification

The PostHogIdentify component automatically identifies users when they sign in with Clerk:
src/components/posthog-identify.tsx
"use client";

import { useEffect, useRef } from "react";
import { useUser, useAuth } from "@clerk/nextjs";
import posthog from "posthog-js";

export function PostHogIdentify() {
  const { user, isLoaded, isSignedIn } = useUser();
  const { has } = useAuth();
  const identifiedRef = useRef(false);

  useEffect(() => {
    if (!isLoaded) return;

    if (isSignedIn && user && !identifiedRef.current) {
      // Identify user in PostHog
      const plan = has?.({ plan: "pro" }) ? "pro" : "free";
      posthog.identify(user.id, {
        email: user.primaryEmailAddress?.emailAddress,
        name: user.fullName,
        first_name: user.firstName,
        last_name: user.lastName,
        username: user.username,
        created_at: user.createdAt,
        plan,
      });
      identifiedRef.current = true;
    } else if (!isSignedIn && identifiedRef.current) {
      // Reset PostHog on logout
      posthog.reset();
      identifiedRef.current = false;
    }
  }, [isLoaded, isSignedIn, user, has]);

  return null;
}
On sign-out, it calls posthog.reset() to clear the person profile.

Tracked Events

Shipr tracks the following events out of the box:
EventLocationDescription
$pageviewposthog-pageview.tsxEvery route change
cta_clickedHero, CTA sectionsUser clicked a call-to-action button
pricing_plan_clickedPricing sectionUser selected a pricing plan
upgrade_button_clickedDashboardUser clicked upgrade
faq_expandedFAQ sectionUser opened a FAQ item
navigation_clickedHeaderNav link clicked (includes device type)
mobile_menu_toggledHeaderMobile menu opened/closed
theme_toggledTheme toggleTheme changed (includes previous/new theme)

Capturing Custom Events

Capture custom events anywhere in your app:
import posthog from "posthog-js";

function MyComponent() {
  const handleClick = () => {
    posthog.capture("custom_event_name", {
      property_key: "property_value",
      user_action: "button_click",
    });
  };

  return <button onClick={handleClick}>Track This</button>;
}
Keep event names in snake_case and include relevant context as properties.

Environment Variables Reference

VariableRequiredDescription
NEXT_PUBLIC_POSTHOG_KEYYesYour PostHog project API key (starts with phc_)
NEXT_PUBLIC_POSTHOG_HOSTYesPostHog API host (usually https://us.i.posthog.com)

Build docs developers (and LLMs) love