VibeWeek
Home/Grow/Set Up PostHog Product Analytics

Set Up PostHog Product Analytics

⬅️ Growth Overview

Product Analytics Setup for Your New SaaS

Goal: Ship a working PostHog instance that tracks the right events, identifies users correctly, powers the funnels and cohorts every other 6-grow playbook depends on, and survives the move from 100 to 10,000 customers without rework. Avoid the typical product-analytics tax — over-tracked event noise, ID confusion, and dashboards nobody updates.

Process: Follow this chat pattern with your AI coding tool such as Claude or v0.app. Pay attention to the notes in [brackets] and replace the bracketed text with your own content.

Timeframe: 1 day to v1 instrumented and shipping events. Week 2 of launch: dashboards built. Quarterly review of event taxonomy locked into the calendar from launch onward.


Why PostHog (and Why You Need This Layer)

Vercel Analytics and Google Search Console (per 5.1 Analytics Integration) cover the web layer — page views, search performance, marketing attribution. PostHog covers the product layer — what users do inside the app, who sticks around, which features matter, where they get stuck.

These are different jobs. You want both. The web layer answers "did our marketing work?" The product layer answers "did the people we acquired actually use the product?" — which is the upstream signal for every retention, conversion, and roadmap decision.

PostHog specifically (vs. Mixpanel, Amplitude, Heap):

  • Open source + cloud + self-hosted. Most flexible; you own your data if you ever need to migrate.
  • All-in-one: events + feature flags + session replay + experiments + heatmaps + surveys + LLM observability. The integration cost of stitching these together separately is real.
  • Generous free tier (1M events/month, 5k session replays). Survives launch and the first 100 customers without payment.
  • API + SQL access — you can run arbitrary queries against your event data, not just the prebuilt funnels.

This guide pairs with Activation Funnel Diagnosis (which uses the events you wire here), Feature Flags (PostHog has feature flags built in), Reduce Churn (the behavioral signals come from PostHog events), and LLM Quality Monitoring (PostHog has dedicated LLM observability now).


1. Decide on Self-Hosted vs Cloud

Two real choices in 2026, with a genuine tradeoff.

I'm building [your product] at [your-domain.com]. My stack is [Next.js / chosen]. I'm at [N] paying customers.

Help me pick between PostHog Cloud and self-hosted.

**PostHog Cloud (US or EU)**:
- Pros: zero ops; ships in 5 minutes; the team upgrades the platform; SOC 2 + HIPAA-eligible plans.
- Cons: data lives on PostHog infra; pricing scales with events past 1M/mo (~$0.00005 per event after).
- Best for: most indie SaaS. The free tier covers the first 6-12 months for typical apps; the Cloud-managed updates remove a real maintenance burden.

**Self-hosted PostHog (Docker / Kubernetes)**:
- Pros: full control; data residency requirements satisfied; flat infrastructure cost at scale.
- Cons: real ops burden — you manage upgrades, ClickHouse scaling, backups; ~10-20 hours/month at scale; harder to debug than Cloud.
- Best for: enterprise customers with hard data-residency requirements, or massive scale (50M+ events/month) where Cloud pricing exceeds the ops cost of self-hosting.

For my specific case:
- Recommend ONE path with rationale
- If Cloud, recommend US or EU based on my customer base
- If self-hosted, the minimum viable infrastructure shape (managed Postgres + ClickHouse, 4-8 vCPU primary, etc.)
- The migration path between them — both directions are supported but non-trivial

Default if no strong reason: PostHog Cloud (US for US-customer-heavy products, EU for EU-customer-heavy or compliance-sensitive). Defer self-hosting until past the free tier.

The single most-overlooked decision: EU vs US Cloud. They are separate instances; you cannot move data between them without a full re-instrumentation. Pick deliberately based on where your customers are and your data-residency posture.


2. Install the SDK and Verify Events Land

The first 30 minutes is wiring the SDK and confirming events arrive in the dashboard. Most teams over-engineer this step.

Install PostHog in my Next.js App Router app at [your-domain.com] using PostHog Cloud [US/EU].

Step 1: Install the package:

\`\`\`bash
npm install posthog-js
\`\`\`

Step 2: Initialize on the client. Create `lib/posthog.ts` and wire from a client provider component:

\`\`\`tsx
// app/providers.tsx
"use client";

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

export function PostHogProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
      api_host: "/ingest",
      ui_host: "https://us.posthog.com",
      capture_pageview: false, // we'll handle pageviews manually for App Router
      capture_pageleave: true,
      person_profiles: "identified_only", // don't bloat profiles with anonymous traffic
    });
  }, []);

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

Step 3: Wrap the app in `app/layout.tsx`:

\`\`\`tsx
import { PostHogProvider } from "./providers";
// ...
<PostHogProvider>{children}</PostHogProvider>
\`\`\`

Step 4: Configure the proxy. Browser-side requests go through `/ingest` on my own domain — bypasses ad-blockers and consolidates to a first-party origin. Add to `next.config.js`:

\`\`\`js
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*" },
    { source: "/ingest/decide", destination: "https://us.i.posthog.com/decide" },
  ];
}
\`\`\`

Step 5: Add manual pageview capture for App Router (the SDK can't auto-detect Next.js App Router routes reliably):

\`\`\`tsx
"use client";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { usePostHog } from "posthog-js/react";

export function PostHogPageview() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const posthog = usePostHog();

  useEffect(() => {
    if (pathname && posthog) {
      const url = window.origin + pathname + (searchParams ? `?${searchParams}` : "");
      posthog.capture("$pageview", { $current_url: url });
    }
  }, [pathname, searchParams, posthog]);

  return null;
}
\`\`\`

Step 6: Add `NEXT_PUBLIC_POSTHOG_KEY` to my Vercel project env vars.

Step 7: Verify. Open my app. Open the PostHog dashboard's Live Events view. Click around the app. Events should appear in real time.

For each step, output the actual code to paste. Don't skip the proxy step (item 4) — without it, ~30% of events are blocked by ad-blockers in 2026.

Also output: the test-of-instrumentation checklist I run after deploying — pageview fires, identify fires after login, custom events fire, no console errors.

The reverse-proxy step is the move 90% of teams skip. PostHog's documentation has covered it for years; it's the difference between "we capture 70% of traffic" and "we capture 95% of traffic."


3. Define Your Event Taxonomy

The single decision that determines whether PostHog pays off in 6 months: the event taxonomy. Loose taxonomy = noise; tight taxonomy = signal. The teams that ship "track everything" land at 50,000 unique event names within a year and cannot answer any specific question.

Build my event taxonomy for [your product]. The product helps [audience] do [outcome].

Naming convention: `domain_action` in snake_case. NEVER:
- Mixed cases or spaces
- Adjectives in names ("clicked_amazing_button")
- Page-specific names ("dashboard_button_clicked" — better: "report_generated")
- Past-tense verbs alone ("clicked", "viewed" — needs a noun: "report_generated")

The 4-tier event hierarchy I want:

**Tier 1: Lifecycle events** (5-10 events) — the events that define a user's journey
- `signup_complete` — account created, source attribution attached
- `onboarded` — user finished the onboarding flow
- `activated` — user hit the activation event (the specific action that predicts retention; from [Activation Funnel](activation-funnel-chat.md))
- `subscription_started` — first paid event
- `subscription_canceled` — cancellation
- `account_deleted` — final deletion (per [Data Trust](data-trust-chat.md))

These should be ~6 events that I never delete. They define the funnels every other team metric depends on.

**Tier 2: Core product events** (15-25 events) — the meaningful actions inside the product
- For an AI content tool: `report_generated`, `template_saved`, `template_used`, `export_downloaded`
- For a code tool: `agent_run_started`, `agent_run_completed`, `pr_opened`, `code_committed`
- One event per meaningful customer-perceived action; no more

**Tier 3: Engagement events** (10-20 events) — secondary actions that signal engagement
- `team_member_invited`, `integration_connected`, `webhook_added`, `documentation_viewed`

**Tier 4: System events** (don't overdo this) — errors, performance, edge cases
- `error_thrown`, `feature_flag_evaluated`
- Use sparingly — most operational events belong in Sentry / your error tracker, not PostHog

For each event, define:
- Exact event name
- Required properties (user_id, plan, etc.)
- Optional properties (specific to the event)
- Description (1 sentence; what triggers this)

Output a single events.md spec doc that lives in the repo and gets updated when events change. Anyone adding an event must update this doc as part of the PR.

Anti-patterns I should avoid:
- "track every button click" — generates noise that buries signal
- Naming events after pages instead of actions — pages change; actions don't
- Multiple names for the same conceptual action ("user_signed_up" vs "signup_complete" vs "registration_done")
- Putting business logic in event names — ("upgraded_to_pro_5_users") instead of properties

Output: the events.md doc with my taxonomy, ~30-50 named events total across the four tiers.

The 30–50-event ceiling is intentional. Teams with 200+ events have invariably duplicated, mis-named, and over-fragmented their taxonomy. Tight taxonomy + properties for variation > loose taxonomy with everything as a separate event.


4. Identify Users Correctly

The most-debugged part of every product analytics setup is identification. Get it right early or pay forever.

Wire user identification correctly. The principle: anonymous users have a `distinct_id` (PostHog's auto-generated UUID); identified users get tied to my internal `user_id` via `posthog.identify()`.

For my product:

**On signup completion** (the moment a user first becomes "known" to me):

\`\`\`tsx
posthog.identify(
  user.id, // my internal user ID, always
  {
    email: user.email,
    plan: "free",
    signup_source: utm_source ?? "direct",
    created_at: user.createdAt,
  }
);
posthog.capture("signup_complete", { source: utm_source });
\`\`\`

**On every subsequent login** (re-confirms identification, refreshes properties):

\`\`\`tsx
posthog.identify(user.id, {
  plan: user.plan,
  team_id: user.teamId, // if multi-user
  is_admin: user.isAdmin,
});
\`\`\`

**On logout**:

\`\`\`tsx
posthog.reset(); // returns to anonymous distinct_id
\`\`\`

**For multi-user products** (teams, orgs):

PostHog supports "groups" — assign a user to an org/team and aggregate org-level metrics:

\`\`\`tsx
posthog.group("organization", user.organizationId, {
  name: user.organization.name,
  plan: user.organization.plan,
  seat_count: user.organization.seatCount,
});
\`\`\`

Now I can build cohorts at the org level (not just user level).

**For server-side events** (Stripe webhooks, scheduled jobs):

Use the server-side library. Same `identify` + `capture` API, but runs from your backend:

\`\`\`bash
npm install posthog-node
\`\`\`

\`\`\`ts
import { PostHog } from "posthog-node";
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
  host: "https://us.i.posthog.com",
});

await posthog.capture({
  distinctId: user.id,
  event: "subscription_started",
  properties: { plan: "pro", mrr: 29 },
});

await posthog.shutdown(); // important — ensures events are flushed before serverless function exits
\`\`\`

Critical edge cases:

1. **Anonymous → identified merging**: when a user signs up after browsing anonymously, PostHog automatically merges the anonymous distinct_id into the identified user. Verify this works by:
   - Open your site in incognito (anonymous)
   - Click around (generates anonymous events)
   - Sign up
   - Verify the anonymous events now show under your identified user profile, not a separate user

2. **Server vs client events for the same user**: both use the same `distinct_id` (your user_id). Mixing identifiers across server and client breaks the user profile.

3. **`posthog.reset()` on logout** — without this, the next person on a shared device inherits the previous user's identity.

Output: the identification code for signup, login, logout, server-side, and the test plan to verify merging works.

The most common analytics bug: events fire correctly but split across multiple "users" in the dashboard because identification is inconsistent. The 5-minute test (anonymous → signup → verify merging) catches this immediately if run.


5. Build the First Five Dashboards

Most teams spend hours configuring dashboards that nobody reads. Build five and stop.

Build my five PostHog dashboards. These are the only ones the team should look at on a recurring basis.

**Dashboard 1: North Star Metric**
- Single primary metric for the business — typically MRR, paid signups per week, or weekly active users
- 90-day trend, with annotations for product launches and pricing changes
- Compared against goal line (if set)
- Refresh: weekly review

**Dashboard 2: Activation Funnel**
- Per [Activation Funnel](activation-funnel-chat.md): signup → first action → activation → retained
- Conversion rate at each step
- Time-to-activation distribution (median, 75th percentile)
- Cohort comparison: this week's signups vs last week's
- Refresh: weekly

**Dashboard 3: Feature Adoption**
- Top 10 product events from Tier 2 (Section 3) ranked by daily active users
- New vs returning users per feature
- Helps me see which features are actually used vs which are bolted-on dust
- Refresh: monthly

**Dashboard 4: Retention**
- Cohort retention curve: of users who signed up week N, what % were active in week N+1, N+2, N+4, N+12?
- Compared cohorts: this quarter vs last quarter
- Sub-cohort: by plan (free vs paid retention typically diverges sharply)
- Refresh: monthly

**Dashboard 5: Quality / Health**
- Error rate trend (events tagged as `error_thrown`)
- Median latency for critical user actions (e.g., generation_completed timing)
- Per-feature usage anomalies (sudden 50% drop in `report_generated` is a red flag)
- Refresh: daily monitoring; weekly review

For each dashboard:
- Output the PostHog Insight queries (PostHog's UI does this; capture the underlying definitions)
- Set the refresh cadence
- Decide who owns it (founder for all 5 in early stage; later: each gets an owner)

Critical anti-patterns:
- 50-tile dashboards — nobody reads them. Five focused dashboards beat one massive one.
- "Vanity dashboards" with raw counts and no comparisons — meaningless without trend or cohort context
- Dashboards that update slowly or stale data — fix the data pipeline before adding more dashboards

Output: the dashboard specs + a calendar block for weekly + monthly review.

The single biggest dashboard improvement: always show comparison. "1,247 signups this week" is meaningless. "1,247 signups this week, 1,098 last week, +13.6%" is a number you can act on. Every dashboard tile should have a comparison built in.


6. Wire Feature Flags Through PostHog

Per Feature Flags, flags live in PostHog (rather than a separate flag service) once you've adopted PostHog as your analytics layer. Single tool, integrated event data, easier A/B test analysis.

Wire PostHog feature flags into my app.

Setup:

\`\`\`tsx
// On any page or component
import { useFeatureFlagEnabled } from "posthog-js/react";

function MyComponent() {
  const showNewUI = useFeatureFlagEnabled("new-onboarding-flow");

  if (showNewUI) return <NewUI />;
  return <OldUI />;
}
\`\`\`

For server-side flag evaluation (per the [Feature Flags](feature-flags-chat.md) playbook, server-side checks are required for security-critical and entitlement gates):

\`\`\`ts
import { PostHog } from "posthog-node";
const ph = new PostHog(process.env.POSTHOG_API_KEY!);
const isEnabled = await ph.isFeatureEnabled("pro-features", userId);
\`\`\`

Critical patterns:

1. **Bootstrap flags** to avoid layout shift on first render. Pass server-evaluated flag values to the client at SSR time so the UI doesn't flicker between variants.

2. **Auto-capture flag exposure**. PostHog automatically logs `$feature_flag_called` whenever a flag is evaluated. This is what makes A/B test analysis possible — every conversion can be associated with the variant the user saw.

3. **Default-safe behavior**. If PostHog is unreachable, every flag check should return the default-off value. Don't let an analytics outage break the product.

4. **Stale flag cleanup**. Per [Feature Flags](feature-flags-chat.md) Section 7's hygiene checklist, flags older than 90 days should be reviewed monthly. PostHog has a "stale flags" report — use it.

A/B test pattern using flags:

\`\`\`tsx
const variant = posthog.getFeatureFlag("pricing-test");
posthog.capture("pricing_page_viewed", { variant });
// ... later
posthog.capture("subscription_started", { variant, plan, mrr });
\`\`\`

In PostHog, this lets me build a funnel: pricing_page_viewed → subscription_started, broken down by variant. Statistical-significance calculation is built in.

Output: the React hook usage, the server-side evaluation code, the bootstrap pattern, and a sample A/B test setup with the corresponding analysis query.

The bootstrap pattern is non-obvious but matters. Without it, users see the default UI for ~50ms, then flicker to the variant — terrible UX, and skews results because users in the flicker have a different experience than users without.


7. Set Up LLM Observability (for AI Products)

PostHog added dedicated LLM observability in 2024-2025. For AI products, this layer is the difference between guessing about model quality and knowing.

Wire PostHog LLM observability for [my AI product].

What it captures:
- Every LLM call: prompt, response, model, tokens (input + output), latency, cost
- Per-user / per-session aggregations: total spend, tokens, latency
- Quality scoring (manual or LLM-as-judge per [LLM Quality Monitoring](llm-quality-monitoring-chat.md))
- Failure rates, error types

Implementation:

PostHog provides wrappers for major LLM providers. For Anthropic:

\`\`\`bash
npm install posthog-node @posthog/ai
\`\`\`

\`\`\`ts
import { PostHog } from "posthog-node";
import Anthropic from "@anthropic-ai/sdk";
import { withTracing } from "@posthog/ai";

const posthog = new PostHog(process.env.POSTHOG_API_KEY!);
const anthropic = new Anthropic();

const tracedAnthropic = withTracing(anthropic, posthog, {
  posthogDistinctId: userId,
  posthogProperties: { feature: "report_generation", plan: user.plan },
});

const response = await tracedAnthropic.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: prompt }],
});

await posthog.shutdown();
\`\`\`

Every call automatically generates `$ai_generation` events with all metadata. PostHog's LLM observability dashboard groups these.

What this unlocks:

1. **Per-user spend tracking** — know exactly which users are driving most token spend (often correlates with most engagement; sometimes with abuse)
2. **Per-feature cost** — which feature is the most expensive to operate? Optimize there.
3. **Latency dashboards** — p50, p95, p99 per feature. Sub-2s latency is table stakes; p99 over 10s is a UX problem.
4. **Failure tracking** — rate of model errors, content-policy refusals, timeouts.
5. **Pair with [LLM Quality Monitoring](llm-quality-monitoring-chat.md)**: PostHog stores the events, the eval suite scores quality, you build dashboards on the joined data.

Output: the wrapper code for [my chosen LLM provider], the standard properties to attach (feature, plan, version), and the LLM-observability dashboard layout.

For AI SaaS specifically, this layer is non-optional past 100 customers. Without it, your unit economics are guesses, and any "the model got worse" customer report has no data to diagnose against.


8. Make It Survive Quarterly Audits

Product analytics decays. Events get added without standards, dashboards go stale, the taxonomy drifts. The discipline that prevents rot:

Build the quarterly product analytics audit.

Once per quarter (90 minutes):

1. **Event audit**:
   - Count of unique event names. If grew >20% without a corresponding product expansion, drift is happening.
   - Top 10 events by volume — do they match my Tier 1+2 from Section 3?
   - Events fired in the last 90 days that aren't in events.md — these are unauthorized adds. Bring them into the doc or remove them.

2. **Property audit**:
   - For each top-10 event: are required properties always present? Run a query for events missing required properties.
   - Property values that are inconsistent (e.g., plan = "Pro" vs "pro" vs "PRO") — fix the source code, then back-fill or accept the noise threshold.

3. **Dashboard audit**:
   - Date last viewed (PostHog tracks this). Dashboards not viewed in 30+ days should be archived or deleted.
   - Slow dashboards (>5s to load) — usually a signal that the underlying query is poorly structured. Refactor.

4. **User identification audit**:
   - "Anonymous" users with substantial event history — these are users where identification failed. Investigate the failure point (often a missed `identify()` call after social login).
   - User profiles with multiple emails — usually merging issues.

5. **Cost / volume forecast**:
   - Events / month trajectory: if I'm trending toward 5M+ events/month on PostHog Cloud, plan upgrade or self-host conversation NOW (not when the bill arrives).

For each issue, output:
- A specific fix or a created ticket
- The owner (founder for now; later: data lead / eng lead / PM)

Don't skip the quarterly audit. The decay is invisible until 9 months in, when you realize half your dashboards are wrong and you can't tell which.

Common Failure Modes

"Our PostHog has 500 unique events." Tax of "track everything" with no taxonomy. Section 3 is the cleanup playbook. Brutal but necessary; do it before going past 1,000 customers.

"Dashboards show different numbers than Stripe." Identification problem; events are split across multiple distinct_ids. Run the merging test in Section 4.

"Our events are blocked by ad-blockers." Reverse proxy missing per Section 2 step 4. Add it; expect a 20-30% lift in capture.

"Server-side events from cron jobs don't show up." Missing await posthog.shutdown() at the end of serverless functions. Without it, events are buffered and never sent before the function exits.

"Feature flag changes aren't picked up." SDK caches flag evaluations for 30 seconds by default. For real-time flag toggles, set the cache shorter, or use server-side evaluation for security-critical flags.

"PostHog bill jumped 10×." Look at the events / month trend. Usually one or two events firing far too often (e.g., on every keystroke instead of on debounced submit). Find the culprit, throttle.

"We can't query our own data." Use PostHog SQL access — the Insight queries don't cover everything. The built-in SQL editor lets you run arbitrary queries against your event store.


Related Reading

⬅️ Growth Overview