All docs/Analytics

docs/architecture/posthog-analytics.md

PostHog Analytics Architecture

This document outlines the analytics architecture using PostHog across HappyClient's applications, covering two distinct tracking domains with separate projects.

Overview

HappyClient uses two separate PostHog projects to track different audiences:

ProjectAudiencePurpose
A: HappyClient UsersOur usersTrack journey from website to activation
B: Video Flow End-UsersOur users' usersTrack video testimonial flow completion

This separation ensures:

  • Clear data ownership boundaries
  • Different privacy considerations per audience
  • Focused analytics for each use case
  • Independent dashboard and funnel management

Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│                     PostHog Project A: HappyClient Users                │
│                    (website + onboarding + library)                     │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────┐   ┌──────────────────┐   ┌──────────────────┐    │
│  │     Website      │   │      Hello       │   │     Library      │    │
│  │ thehappyclient   │   │     (hello.*)    │   │   (library.*)    │    │
│  │     .com         │   │                  │   │                  │    │
│  └────────┬─────────┘   └────────┬─────────┘   └────────┬─────────┘    │
│           │                      │                      │              │
│           │    cookie_domain: ".thehappyclient.com"     │              │
│           └──────────────────────┼──────────────────────┘              │
│                                  │                                      │
│                    ┌─────────────▼─────────────┐                       │
│                    │   Unified distinct_id     │                       │
│                    │   Cross-subdomain cookie  │                       │
│                    └─────────────┬─────────────┘                       │
│                                  │                                      │
│                    ┌─────────────▼─────────────┐                       │
│                    │   posthog.identify()      │                       │
│                    │   on Clerk login/signup   │                       │
│                    └───────────────────────────┘                       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│               PostHog Project B: Video Flow End-Users                   │
│                       (branded-video-flow)                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │                    Branded Video Flow                             │  │
│  │                   flow.thehappyclient.com                         │  │
│  │                                                                   │  │
│  │   Events: flow_page_visited, video_recorded, submit_success, etc │  │
│  └──────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │                    Library (Server-side)                          │  │
│  │               Queries Project B for analytics dashboards          │  │
│  │                                                                   │  │
│  │   Uses: POSTHOG_FLOW_ANALYTICS_API_KEY, _PROJECT_ID (server-only)│  │
│  └──────────────────────────────────────────────────────────────────┘  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Project A: HappyClient User Analytics

Purpose

Track HappyClient's own users across their complete journey:

  1. First website visit (anonymous)
  2. CTA click → Onboarding
  3. Onboarding steps completion
  4. Signup/login
  5. Product activation and usage

Applications

AppSubdomainCodebaseStatus
Websitethehappyclient.comseparateTo be implemented
Onboardinghello.thehappyclient.comapps/helloNeeds cross-domain config
Librarylibrary.thehappyclient.comapps/libraryTo be implemented

Environment Variables

# All apps use the same credentials
NEXT_PUBLIC_POSTHOG_KEY=phc_<happyclient_users_project>
NEXT_PUBLIC_POSTHOG_HOST=https://eu.posthog.com

Initialization Pattern

All apps must use identical initialization for cross-subdomain tracking:

// Shared initialization config
import posthog from "posthog-js";

export function initPostHog(key: string, host: string) {
  if (typeof window === "undefined") return null;
  
  if (!posthog.__loaded) {
    posthog.init(key, {
      api_host: host,
      persistence: "localStorage+cookie",
      cookie_domain: ".thehappyclient.com",  // Leading dot required!
      capture_pageview: true,
      capture_pageleave: true,
      // Optional: Session replay
      disable_session_recording: false,
      session_recording: {
        maskAllInputs: true,
        maskInputFn: (text, element) => {
          // Mask sensitive fields
          if (element?.type === "password") return "***";
          return text;
        },
      },
    });
  }
  
  return posthog;
}

Identity Resolution

PostHog identity is resolved when users authenticate via Clerk:

// After Clerk login/signup
import { useUser } from "@clerk/nextjs";
import { usePostHog } from "posthog-js/react";

export function useIdentifyUser() {
  const { user, isLoaded } = useUser();
  const posthog = usePostHog();

  useEffect(() => {
    if (!isLoaded || !user || !posthog) return;

    posthog.identify(user.id, {
      email: user.emailAddresses[0]?.emailAddress,
      name: user.fullName,
      org_id: user.organizationMemberships?.[0]?.organization.id,
      created_at: user.createdAt,
    });
  }, [user, isLoaded, posthog]);
}

Identity flow:

  1. User visits website → Anonymous distinct_id created
  2. User navigates to hello.* → Same distinct_id (cross-subdomain cookie)
  3. User completes onboarding → Same distinct_id
  4. User signs up via Clerk → posthog.identify(clerkUserId) merges IDs
  5. User uses library → Full timeline from website visit available

Event Naming Convention

Events follow the pattern: {app}_{action}

// Website events
posthog.capture("website_page_view", { page: "/pricing" });
posthog.capture("website_cta_click", { cta: "start_free_trial" });
posthog.capture("website_demo_requested");

// Onboarding events
posthog.capture("onboarding_started");
posthog.capture("onboarding_step_completed", { step: "upload_video", step_index: 2 });
posthog.capture("onboarding_finished");

// Library events
posthog.capture("library_login");
posthog.capture("library_project_created", { project_id: "..." });
posthog.capture("library_flow_published", { flow_id: "..." });

Default Event Properties

Register default properties on initialization:

// In each app's PostHog provider
posthog.register({
  app: "library",  // or "onboarding" or "website"
  env: process.env.NODE_ENV === "production" ? "production" : "development",
  version: process.env.NEXT_PUBLIC_APP_VERSION || "unknown",
});

Library App Implementation

The library app needs a new client-side PostHog provider:

// apps/library/components/providers/PostHogProvider.tsx
"use client";

import { useEffect, useMemo } from "react";
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
import { useUser } from "@clerk/nextjs";
import { getLibraryPublicEnvironment } from "@/lib/env";

export function PostHogProvider({ children }: { children: React.ReactNode }) {
  const { user, isLoaded } = useUser();

  const client = useMemo(() => {
    if (typeof window === "undefined") return null;
    
    const env = getLibraryPublicEnvironment();
    const key = env.NEXT_PUBLIC_POSTHOG_KEY;
    const host = env.NEXT_PUBLIC_POSTHOG_HOST;
    
    if (!key || !host) return null;
    
    if (!posthog.__loaded) {
      posthog.init(key, {
        api_host: host,
        persistence: "localStorage+cookie",
        cookie_domain: ".thehappyclient.com",
        capture_pageview: true,
        capture_pageleave: true,
      });
      
      // Register default properties
      posthog.register({
        app: "library",
        env: env.ENVIRONMENT === "cloud" ? "production" : "development",
      });
    }
    
    return posthog;
  }, []);

  // Identify user when Clerk auth is ready
  useEffect(() => {
    if (!isLoaded || !user || !client) return;
    
    client.identify(user.id, {
      email: user.emailAddresses[0]?.emailAddress,
      name: user.fullName,
      org_id: user.organizationMemberships?.[0]?.organization.id,
    });
  }, [user, isLoaded, client]);

  if (!client) return <>{children}</>;

  return <PHProvider client={client}>{children}</PHProvider>;
}

Hello App Updates

Update existing PostHog config for cross-subdomain cookies:

// apps/hello/app/_providers/client-providers.tsx
// Update the posthog.init call to include:
posthog.init(key, {
  api_host: host,
  persistence: "localStorage+cookie",
  cookie_domain: ".thehappyclient.com",  // ADD THIS
  capture_pageview: true,
  capture_pageleave: true,
});

// Register default properties
posthog.register({
  app: "onboarding",
  env: "production",
});

Project B: Video Flow End-User Analytics

Purpose

Track end-users (customers' customers) as they complete video testimonial flows. This is a separate audience from HappyClient users.

PostHog Project

Client-Side Tracking

Implemented in apps/branded-video-flow via the @repo/app-video-flow package.

Key files:

  • packages/app-video-flow/src/web/analytics/posthog.ts — Type-safe event definitions
  • packages/app-video-flow/src/web/components/providers/PostHogProvider.tsx — React provider
  • apps/branded-video-flow/app/_providers/client-providers.tsx — App initialization

Event Types

All events are type-safe via PostHogEventMap:

// From packages/app-video-flow/src/web/analytics/posthog.ts
export type PostHogEventMap = {
  flow_page_visited: PostHogEvents.FlowPageVisited;
  flow_start: PostHogEvents.FlowStart;
  step_change: PostHogEvents.FlowStepChange;
  session_created: PostHogEvents.SessionCreated;
  reward_selected: PostHogEvents.RewardSelected;
  video_recorded: PostHogEvents.VideoRecorded;
  video_skipped: PostHogEvents.VideoSkipped;
  upload_start: PostHogEvents.UploadStart;
  upload_success: PostHogEvents.UploadSuccess;
  upload_error: PostHogEvents.UploadError;
  form_submitted: PostHogEvents.FormSubmitted;
  submit_start: PostHogEvents.SubmitStart;
  submit_success: PostHogEvents.SubmitSuccess;
  submit_error: PostHogEvents.SubmitError;
  // ... video playback events
};

Server-Side Querying

The library app queries Project B's data for analytics dashboards:

Environment variables (server-only):

# apps/library/.env
# Note: POSTHOG_FLOW_ANALYTICS_* prefix clarifies these are for QUERYING Project B,
# not for client-side tracking (which uses NEXT_PUBLIC_POSTHOG_*)
POSTHOG_FLOW_ANALYTICS_API_KEY=phk_...       # Personal API Key with query:read scope
POSTHOG_FLOW_ANALYTICS_PROJECT_ID=74547
POSTHOG_FLOW_ANALYTICS_HOST=https://eu.posthog.com

Key files:

  • apps/library/lib/services/posthog-client.ts — HogQL query execution
  • apps/library/lib/services/posthog-analytics.ts — Analytics service
  • apps/library/lib/services/funnel-calculator.ts — Funnel metrics calculation

Example query:

// From apps/library/lib/services/posthog-client.ts
export async function queryPostHogEvents(
  flowId: string,
  excludeAdmins: boolean = false
): Promise<PostHogEvent[]> {
  const client = getPostHogClient();
  if (!client.isConfigured()) {
    throw new Error("PostHog not configured");
  }

  const query = new HogQLQueryBuilder()
    .select(["event", "timestamp", "person_id", "properties"])
    .from("events")
    .where(`properties.flow_id = '${flowId}'`)
    .where(`timestamp >= now() - INTERVAL 30 DAY`)
    .orderBy("timestamp", "ASC")
    .build();

  const response = await client.executeQuery(query);
  return PostHogEventParser.parseEvents(response.results);
}

Flow Analytics Dashboard

The library displays flow analytics by querying Project B:

// apps/library/components/admin/FlowAnalyticsDashboard.tsx
import { getFlowAnalyticsData } from "@/lib/services/posthog-analytics";

export async function FlowAnalyticsDashboard({ flowId }: { flowId: string }) {
  const analytics = await getFlowAnalyticsData(flowId);
  
  if (!analytics.isPostHogConfigured) {
    return <PostHogNotConfiguredMessage />;
  }
  
  return (
    <div>
      <FunnelChart data={analytics.funnel} />
      <VisitorsTrend data={analytics.dailyVisitors} />
    </div>
  );
}

Dashboards & Investigable Questions (Project A)

Two PostHog dashboards in the app project (ID 117783) visualize the custom events instrumented across the hello and library apps. Together they cover the full user lifecycle from onboarding entry to post-publish activation.

Dashboard 1: Onboarding Funnel & Engagement

URL: https://eu.posthog.com/project/117783/dashboard/502991 Tags: onboarding, analytics

InsightTypeWhat It Answers
Onboarding FunnelFunnelWhere do users drop off during onboarding? Shows conversion from Started → Objective → Company → Exit Path → Published. Identifies the highest-drop-off step.
Onboarding VolumeBold NumberHow many users are starting onboarding? Total onboarding_started count over the last 30 days — the top-of-funnel input metric.
Publish ConversionBold NumberHow many flows actually get published? Total onboarding_published count — the bottom-of-funnel output metric. Compare with volume for overall conversion rate.
Objective DistributionPieWhat are users trying to achieve? Breakdown of onboarding_objective_selected by objective_id — shows which use cases (testimonials, case studies, etc.) drive adoption.
AI Feature UsageBarAre users engaging with AI features? Breakdown of onboarding_ai_action by type (generate_questions, regenerate, translate, generate_image, generate_more). Measures AI feature-market fit.
Auth Wall FunnelFunnelHow many users does the auth gate lose? Publish Initiated → Auth Shown → Published. Measures authentication friction for unauthenticated users.
Publish OutcomesLineHow reliable is publishing? Side-by-side onboarding_published vs onboarding_publish_failed over time. Spikes in failures signal infrastructure issues.
Language AdoptionLineHow popular is multi-language support? onboarding_language_added count broken down by language. Shows which languages are most requested and whether multi-language is a growth driver.

Key questions this dashboard answers:

  1. What is our onboarding conversion rate? — Compare Onboarding Volume to Publish Conversion.
  2. Where is the biggest drop-off? — Read the Onboarding Funnel step-by-step conversion rates.
  3. Is the auth gate killing conversion? — Check the Auth Wall Funnel for the Initiated → Published conversion rate.
  4. Which objectives should we prioritize? — Look at Objective Distribution to see what users actually want.
  5. Is AI worth the investment? — AI Feature Usage shows whether users engage with AI features and which ones they prefer.
  6. Are publishes reliable? — Publish Outcomes line chart shows failure trends over time.
  7. Should we invest more in i18n? — Language Adoption reveals demand for non-English flows.

Dashboard 2: Post-Publish Activation

URL: https://eu.posthog.com/project/117783/dashboard/502992 Tags: library, analytics

InsightTypeWhat It Answers
Activation FunnelFunnelDo published flows actually get used? Published → URL Copied → First Video Received. Measures the full activation loop.
Flows PublishedBold NumberHow many flows went live this period? Total onboarding_published count from the library perspective — input to activation.
Share ActionsLineAre users sharing their flows? library_flow_url_copied + library_try_yourself_clicked over time. Share actions are the first signal of distribution intent.
First Video ReceivedBold NumberHow many flows reached the activation milestone? Count of library_first_video_received — the 0→1 video transition that means a flow is truly activated.
Video ViewsLineAre users engaging with received videos? library_video_viewed per day — measures whether users actually watch the video responses they receive.
CSV ExportsLineAre users extracting data? library_csv_exported per day — a power-user engagement signal indicating deeper product adoption.

Key questions this dashboard answers:

  1. What percentage of published flows get shared? — Activation Funnel: Published → URL Copied conversion.
  2. What percentage of shared flows receive a video response? — Activation Funnel: URL Copied → First Video Received conversion.
  3. Is the "aha moment" happening? — First Video Received count. This is the key activation metric — a flow that never receives a video is a dead flow.
  4. Are users coming back to view responses? — Video Views trend. Sustained views indicate retention and engagement.
  5. Are power users emerging? — CSV Exports trend. Export behavior signals users who are deeply engaged and may be candidates for upsell.
  6. Is distribution growing? — Share Actions trend over time. Rising share actions mean organic growth potential.

Combining Both Dashboards

The two dashboards form a complete picture of the user lifecycle:

Onboarding Funnel & Engagement              Post-Publish Activation
┌─────────────────────────────┐             ┌─────────────────────────────┐
│                             │             │                             │
│  Start → Objective →        │             │  Published → URL Copied →   │
│  Company → Exit Path →      │  ────────►  │  First Video Received       │
│  Published                  │  (handoff)  │                             │
│                             │             │  + Video Views, CSV Exports │
│  + AI Usage, Auth Funnel,   │             │                             │
│    Objectives, Languages    │             │                             │
└─────────────────────────────┘             └─────────────────────────────┘

Cross-dashboard questions:

QuestionWhere to Look
What is our end-to-end conversion from start to activation?Onboarding Volume ÷ First Video Received
Are users who use AI features more likely to activate?Filter Onboarding Funnel by users who fired onboarding_ai_action, compare activation rates
Which objectives lead to highest activation?Cross-reference Objective Distribution with Activation Funnel (requires PostHog cohort analysis)
Is the auth wall impacting activation, not just publish?Compare Auth Wall Funnel drop-off with Activation Funnel rates for auth vs pre-auth users

What These Dashboards Don't Cover

GapWhyAlternative
Per-step time spentNo duration tracking — use PostHog session replaySession replay + $pageview timestamps
Return visits / multi-session onboardingonboarding_draft_resumed exists but no dashboard insight yetCreate ad-hoc insight from onboarding_draft_resumed
Asset upload patternsonboarding_asset_uploaded is tracked but not dashboardedCreate ad-hoc insight breaking down by asset_type and source
Per-organization activationEvents carry organization_id but dashboards show aggregateFilter any insight by organization_id property
Website → Onboarding attributionWebsite tracking not yet implemented (Project A)Pending website PostHog integration
Video flow completion ratesBelongs to Project B (end-user analytics)See Project B dashboards

Environment Variable Summary

Project A: HappyClient User Analytics

VariableAppsPurpose
NEXT_PUBLIC_POSTHOG_KEYwebsite, hello, libraryClient-side tracking key
NEXT_PUBLIC_POSTHOG_HOSTwebsite, hello, libraryPostHog API host

Project B: Video Flow Analytics

VariableAppsPurpose
NEXT_PUBLIC_POSTHOG_KEYbranded-video-flowClient-side tracking key
NEXT_PUBLIC_POSTHOG_HOSTbranded-video-flowPostHog API host
POSTHOG_FLOW_ANALYTICS_API_KEYlibrary (server)Personal API key for querying
POSTHOG_FLOW_ANALYTICS_PROJECT_IDlibrary (server)Project ID for queries
POSTHOG_FLOW_ANALYTICS_HOSTlibrary (server)API host for queries

Best Practices

1. Never Mix Projects

Each app should only send events to its designated project. Never configure library to send client events to Project B.

2. Use Type-Safe Events

For Project B, use the typed captureEvent function:

import { captureEvent } from "@repo/app-video-flow/web";

captureEvent("flow_start", {
  flow_id: flowId,
  session_id: sessionId,
});

3. Handle Missing Configuration Gracefully

if (!posthogClient) {
  console.warn("PostHog not configured");
  return <>{children}</>;
}

4. Don't Track on Kiosk/Preview Routes

The BVF PostHogProvider automatically disables tracking on kiosk routes and when disableTracking={true}.

5. Respect User Privacy

  • Mask sensitive inputs in session recordings
  • Don't capture PII in event properties
  • Respect browser DNT settings where appropriate

Related Documents

  • prds/posthog-unified-analytics-prd.md — Requirements and acceptance criteria
  • docs/architecture/posthog-event-tracking.md — Event instrumentation details, user journey mapping, and type-safe event module
  • docs/posthog-flow-analytics-overview.md — Video flow analytics details
  • docs/posthog-customer-analytics-workflow.md — Customer analytics workflows