Claude Skill

api-analytics-posthog-analytics

PostHog event tracking, user identification, group analytics for B2B, GDPR consent patterns. Use when implementing product analytics, tracking user behavior, setting up funnels, or configuring privacy-compliant tracking.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-analytics-posthog-analytics_skills_api-analytics-posthog-analytics-3a51ef5.zip · 19 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-analytics-posthog-analytics/skills/api-analytics-posthog-analytics
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

PostHog Analytics Patterns

Quick Guide: Use PostHog for product analytics with structured event naming (category:object_action), server-side tracking for reliability, and proper user identification integrated with your authentication flow. Client-side for UI interactions, server-side for business events. Always call reset() on logout, never store PII in event properties, and use captureImmediate() or await shutdown() in serverless environments.

Detailed Resources:


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST call posthog.identify() ONLY when a user signs up or logs in - never on every page load)

(You MUST include the user's database ID as distinct_id in ALL server-side events)

(You MUST call posthog.reset() when a user logs out to unlink future events)

(You MUST use the category:object_action naming convention for all custom events)

(You MUST NEVER include PII (email, name, phone) in event properties - use user IDs only)

</critical_requirements>


Auto-detection: PostHog, posthog-js, posthog-node, usePostHog, PostHogProvider, capture, identify, group analytics, product analytics, event tracking, funnel analysis

When to use:

  • Tracking user behavior and product analytics
  • Setting up conversion funnels and retention analysis
  • Implementing group analytics for B2B multi-tenant apps
  • Understanding feature adoption and user journeys
  • A/B testing analysis (in conjunction with feature flags)

When NOT to use:

  • Feature flag implementation (separate concern)
  • Error tracking and logging (use dedicated error tracking tools)
  • Infrastructure monitoring (use observability tools)

Key patterns covered:

  • Event naming conventions (category:object_action)
  • Property naming patterns (object_adjective, is_/has_ booleans)
  • User identification with authentication flow integration
  • Client-side tracking with React hooks
  • Server-side tracking with posthog-node
  • Group analytics for B2B organizations
  • Privacy and GDPR consent patterns
  • TypeScript patterns for type-safe events




<red_flags>

RED FLAGS

High Priority Issues:

  • Using email as distinct_id -- PII should not be the identifier
  • Missing posthog.reset() on logout -- users get mixed together
  • No await shutdown() in serverless -- events are lost
  • PII in event properties -- GDPR violation risk
  • Calling identify() on every render -- performance degradation

Common Mistakes:

  • Importing posthog directly instead of using usePostHog hook in React
  • Not setting up reverse proxy (api_host: "/ingest") -- events blocked by ad blockers
  • Different event names for same action on frontend vs backend
  • Not using person_profiles: "identified_only" -- 4x higher costs on anonymous events
  • Using capture() instead of captureImmediate() in serverless -- events may not complete

Gotchas & Edge Cases:

  • distinct_id is required for ALL server-side events (unlike client-side which auto-generates one)
  • group() must include group ID with every event (not persisted like identify())
  • Maximum 5 group types per project
  • cookieless_mode: "always" disables identify() entirely -- privacy trade-off
  • PostHog web SDK is client-side only -- will not work in server components
  • Session IDs must be manually passed to server-side events for session linking

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST call posthog.identify() ONLY when a user signs up or logs in - never on every page load)

(You MUST include the user's database ID as distinct_id in ALL server-side events)

(You MUST call posthog.reset() when a user logs out to unlink future events)

(You MUST use the category:object_action naming convention for all custom events)

(You MUST NEVER include PII (email, name, phone) in event properties - use user IDs only)

Failure to follow these rules will cause analytics data quality issues, privacy violations, or lost events.

</critical_reminders>

Files (skills)
  • examples
    • client-tracking.md 3.5 KB
      # PostHog Analytics - Client-Side Tracking
      
      > Client-side tracking patterns with React hooks and PostHog provider.
      >
      > **Return to:** [SKILL.md](../SKILL.md) | **Prerequisites:** [core.md](core.md)
      >
      > **Related:** [server-tracking.md](server-tracking.md) | [privacy-gdpr.md](privacy-gdpr.md)
      
      ---
      
      ## Provider Setup
      
      ```typescript
      // providers/posthog-provider.tsx
      "use client";
      
      import { useEffect } from "react";
      import posthog from "posthog-js";
      import { PostHogProvider as PHProvider, usePostHog } from "posthog-js/react";
      
      const POSTHOG_KEY = process.env.POSTHOG_KEY!;
      const POSTHOG_HOST =
        process.env.POSTHOG_HOST ?? "https://us.i.posthog.com";
      
      // Initialize PostHog
      if (typeof window !== "undefined" && POSTHOG_KEY) {
        posthog.init(POSTHOG_KEY, {
          api_host: POSTHOG_HOST,
          defaults: "2026-01-30", // Use versioned config defaults for stability
          person_profiles: "identified_only", // Only create profiles for identified users
          capture_pageview: false, // Disable automatic pageviews (we handle manually)
          capture_pageleave: true, // Track when users leave pages
        });
      }
      
      // Manual pageview tracking - use your router's pathname hook
      function PostHogPageView() {
        const pathname = useCurrentPathname(); // Your router's pathname hook
        const posthog = usePostHog();
      
        useEffect(() => {
          if (pathname && posthog) {
            const url = window.origin + pathname;
            posthog.capture("$pageview", { $current_url: url });
          }
        }, [pathname, posthog]);
      
        return null;
      }
      
      interface PostHogProviderProps {
        children: React.ReactNode;
      }
      
      export function PostHogProvider({ children }: PostHogProviderProps) {
        return (
          <PHProvider client={posthog}>
            <PostHogPageView />
            {children}
          </PHProvider>
        );
      }
      ```
      
      **Why good:** `person_profiles: "identified_only"` reduces costs (anonymous events 4x cheaper), manual pageview capture gives control over URL tracking, `defaults` date pins config behavior for stability
      
      ---
      
      ## Event Tracking Hook
      
      ```typescript
      // hooks/use-analytics.ts
      "use client";
      
      import { useCallback } from "react";
      import { usePostHog } from "posthog-js/react";
      
      import type { PostHogEvent } from "../lib/analytics/constants";
      
      interface EventProperties {
        [key: string]: string | number | boolean | null | undefined;
      }
      
      export function useAnalytics() {
        const posthog = usePostHog();
      
        const track = useCallback(
          (event: PostHogEvent | string, properties?: EventProperties) => {
            posthog?.capture(event, properties);
          },
          [posthog],
        );
      
        const trackFeatureUsed = useCallback(
          (featureName: string, properties?: EventProperties) => {
            track("feature:used", {
              feature_name: featureName,
              ...properties,
            });
          },
          [track],
        );
      
        return { track, trackFeatureUsed };
      }
      ```
      
      **Why good:** Centralized tracking with type hints, convenience methods for common patterns, null-safe via optional chaining
      
      ---
      
      ## Component Usage
      
      ```typescript
      // Good Example - Tracking in components
      "use client";
      
      import { useAnalytics } from "../hooks/use-analytics";
      import { POSTHOG_EVENTS } from "../lib/analytics/constants";
      
      export function CreateProjectButton() {
        const { track } = useAnalytics();
      
        const handleClick = () => {
          track(POSTHOG_EVENTS.FEATURE_USED, {
            feature_name: "create_project",
            source: "dashboard_header",
          });
      
          // ... create project logic
        };
      
        return (
          <button onClick={handleClick} type="button">
            Create Project
          </button>
        );
      }
      ```
      
      **Why good:** Uses constant for event name, includes context (source), clean separation of tracking from business logic
      
    • core.md 8.8 KB
      # PostHog Analytics - Core Examples
      
      > Essential patterns for PostHog analytics: naming conventions, user identification, funnel design, and type safety.
      >
      > **Return to:** [SKILL.md](../SKILL.md)
      
      **Extended Examples:**
      
      - [client-tracking.md](client-tracking.md) - React Hooks, Provider Setup
      - [server-tracking.md](server-tracking.md) - posthog-node, Serverless Patterns
      - [group-analytics.md](group-analytics.md) - B2B Organization Tracking
      - [privacy-gdpr.md](privacy-gdpr.md) - GDPR Consent, Cookieless Mode
      
      ---
      
      ## Pattern 1: Event Naming Conventions
      
      ### Naming Rules
      
      ```typescript
      // Good Example - Structured event names
      // Format: category:object_action
      // - category: Context (signup_flow, settings, dashboard)
      // - object: Component/location (password_button, pricing_page)
      // - action: Present-tense verb (click, submit, view)
      
      // Signup flow events
      "signup_flow:email_form_submit";
      "signup_flow:google_oauth_click";
      "signup_flow:verification_email_sent";
      
      // Dashboard events
      "dashboard:project_create";
      "dashboard:invite_member_click";
      "dashboard:export_data_download";
      
      // Settings events
      "settings:password_change_submit";
      "settings:notification_toggle";
      "settings:billing_plan_upgrade";
      
      // Alternative format: object_verb (simpler, still good)
      "project_created";
      "user_signed_up";
      "invite_sent";
      ```
      
      **Why good:** Category prefix groups related events in PostHog UI, present-tense verbs are consistent, snake_case is lowercase and readable, structured names enable wildcard queries like `signup_flow:*`
      
      ```typescript
      // Bad Example - Inconsistent naming
      "UserSignedUp"; // BAD: PascalCase
      "user-signed-up"; // BAD: kebab-case (use snake_case)
      "clicked_button"; // BAD: Past tense
      "button click"; // BAD: Spaces
      "signup"; // BAD: Too vague
      "signUpFormSubmittedByUserOnPage"; // BAD: Too verbose
      ```
      
      **Why bad:** Inconsistent casing makes queries impossible, past tense mixes with present, vague names don't tell you what happened, verbose names are hard to type
      
      ### Property Naming
      
      ```typescript
      // Good Example - Structured property names
      const eventProperties = {
        // Object_adjective pattern
        project_id: "proj_abc123",
        plan_name: "pro",
        item_count: 5,
      
        // Boolean: is_ or has_ prefix
        is_first_purchase: true,
        has_completed_onboarding: false,
        is_annual_billing: true,
      
        // Dates: _date or _timestamp suffix
        trial_end_date: "2025-01-15",
        last_login_timestamp: 1704067200,
      
        // Enums: use the actual value
        payment_method: "stripe",
        source: "google_ads",
      };
      ```
      
      **Why good:** Consistent patterns enable filtering and grouping, boolean prefixes make type obvious, date suffixes clarify format expectations
      
      ### Funnel-Ready Event Design
      
      Design events with funnel analysis in mind -- use consistent category prefixes for each step:
      
      ```typescript
      // Good Example - Events designed for funnel analysis
      // Signup funnel: Visit -> Start -> Submit -> Verify -> Complete
      
      // Step 1: User visits signup page
      track("signup_flow:page_view", {
        source: utmSource,
        referrer: document.referrer,
      });
      
      // Step 2: User starts signup form
      track("signup_flow:form_started", {
        method: "email", // or "google", "github"
      });
      
      // Step 3: User submits form
      track("signup_flow:form_submitted", {
        method: "email",
        has_referral_code: Boolean(referralCode),
      });
      
      // Step 4: User verifies email (server-side)
      posthogServer.capture({
        distinctId: userId,
        event: "signup_flow:email_verified",
        properties: {
          verification_time_seconds: verificationTimeSeconds,
        },
      });
      
      // Step 5: User completes onboarding (server-side)
      posthogServer.capture({
        distinctId: userId,
        event: "signup_flow:onboarding_completed",
        properties: {
          steps_completed: completedSteps.length,
          total_steps: ONBOARDING_STEPS_COUNT,
        },
      });
      ```
      
      **Why good:** Consistent `signup_flow:` prefix groups funnel events, each step has unique action, properties enable breakdown analysis (by method, source)
      
      **Funnel design tips:**
      
      - Use consistent prefix for all funnel steps (e.g., `signup_flow:`, `checkout:`, `onboarding:`)
      - Include properties that enable segmentation (source, method, plan)
      - Track both client-side (UI interactions) and server-side (business events)
      - Server-side events are more reliable for critical conversion steps
      
      ---
      
      ## Pattern 2: User Identification with Authentication
      
      ### Constants
      
      ```typescript
      // lib/analytics/constants.ts
      export const POSTHOG_EVENTS = {
        // Auth events (server-side)
        USER_SIGNED_UP: "user_signed_up",
        USER_LOGGED_IN: "user_logged_in",
        USER_LOGGED_OUT: "user_logged_out",
        PASSWORD_RESET_REQUESTED: "password_reset_requested",
      
        // Subscription events (server-side)
        SUBSCRIPTION_CREATED: "subscription_created",
        SUBSCRIPTION_CANCELLED: "subscription_cancelled",
        PLAN_UPGRADED: "plan_upgraded",
      
        // UI events (client-side)
        SIGNUP_FORM_SUBMIT: "signup_flow:form_submit",
        ONBOARDING_STEP_COMPLETED: "onboarding:step_completed",
        FEATURE_USED: "feature:used",
      } as const;
      
      export type PostHogEvent = (typeof POSTHOG_EVENTS)[keyof typeof POSTHOG_EVENTS];
      ```
      
      ### Client-Side Identification
      
      ```typescript
      // Good Example - Identify on auth state change
      "use client";
      
      import { useEffect } from "react";
      import { usePostHog } from "posthog-js/react";
      
      // Use your auth solution's session hook
      interface SessionUser {
        id: string;
        plan?: string;
        createdAt: string;
        emailVerified?: boolean;
      }
      
      export function useAnalyticsIdentify(user: SessionUser | null) {
        const posthog = usePostHog();
      
        useEffect(() => {
          if (!posthog || !user) return;
      
          // Only identify if not already identified
          if (!posthog._isIdentified()) {
            posthog.identify(user.id, {
              // Safe properties only - no PII in properties
              plan: user.plan ?? "free",
              created_at: user.createdAt,
              is_verified: user.emailVerified ?? false,
            });
          }
        }, [posthog, user]);
      }
      ```
      
      **Why good:** `_isIdentified()` prevents duplicate calls, uses database user ID (not email) as distinct_id, only safe properties stored (no PII), runs once on session change
      
      ```typescript
      // Bad Example - Over-identifying
      "use client";
      
      import { usePostHog } from "posthog-js/react";
      
      export function BadIdentify() {
        const posthog = usePostHog();
      
        // BAD: Runs on every render
        posthog?.identify("user@example.com", {
          // BAD: Email as ID
          email: "user@example.com", // BAD: PII in properties
          name: "John Doe", // BAD: PII in properties
          phone: "+1234567890", // BAD: PII in properties
        });
      }
      ```
      
      **Why bad:** Runs on every render (performance issue), email as distinct_id is PII, storing PII in properties violates privacy regulations
      
      ### Logout Reset
      
      ```typescript
      // Good Example - Reset on logout
      "use client";
      
      import { usePostHog } from "posthog-js/react";
      
      export function useLogout(signOut: () => Promise<void>) {
        const posthog = usePostHog();
      
        const handleLogout = async () => {
          // Track logout event before reset
          posthog?.capture("user_logged_out");
      
          // Reset PostHog to unlink future events
          posthog?.reset();
      
          // Then sign out via your auth solution
          await signOut();
        };
      
        return { logout: handleLogout };
      }
      ```
      
      **Why good:** Captures logout event before reset, `reset()` unlinks future events from this user, prevents shared computer user mixing
      
      ---
      
      ## Pattern 3: Type-Safe Event Tracking
      
      Use TypeScript to enforce event name and property consistency at compile time:
      
      ```typescript
      // lib/analytics/types.ts
      
      // Event-specific property types
      interface UserSignedUpProperties {
        plan: "free" | "pro" | "enterprise";
        source?: string;
      }
      
      interface ProjectCreatedProperties {
        project_id: string;
        is_first_project: boolean;
      }
      
      interface FeatureUsedProperties {
        feature_name: string;
        source?: string;
      }
      
      // Map event names to their required properties
      export interface EventPropertyMap {
        user_signed_up: UserSignedUpProperties;
        project_created: ProjectCreatedProperties;
        "feature:used": FeatureUsedProperties;
        // Add more as your event catalog grows
      }
      ```
      
      ```typescript
      // Type-safe track function with overloads
      export function useTypedAnalytics() {
        const posthog = usePostHog();
      
        function track<E extends keyof EventPropertyMap>(
          event: E,
          properties: EventPropertyMap[E],
        ): void;
        function track(event: string, properties?: Record<string, unknown>): void;
        function track(event: string, properties?: Record<string, unknown>): void {
          posthog?.capture(event, properties);
        }
      
        return { track };
      }
      
      // Usage - TypeScript catches errors at compile time
      const { track } = useTypedAnalytics();
      track("user_signed_up", { plan: "pro" }); // OK
      track("user_signed_up", { source: "google_ads" }); // Error: missing "plan"
      ```
      
      **Why good:** Compile-time validation catches typos and missing properties, IDE autocomplete for event names, type definitions serve as living documentation
      
      **When to use:** Teams with multiple developers, products with many events, codebases where analytics accuracy is critical
      
    • funnel-analysis.md 2.7 KB
      # PostHog Analytics - Funnel Analysis Setup
      
      > Designing events for conversion funnel analysis in PostHog.
      >
      > **Return to:** [SKILL.md](../SKILL.md) | **Prerequisites:** [core.md](core.md)
      >
      > **Related:** [server-tracking.md](server-tracking.md) | [type-safety.md](type-safety.md)
      
      ---
      
      ## Funnel-Ready Events
      
      ```typescript
      // ✅ Good Example - Events designed for funnel analysis
      // Signup funnel: Visit -> Start -> Verify -> Complete
      
      // Step 1: User visits signup page
      track("signup_flow:page_view", {
        source: utmSource,
        referrer: document.referrer,
      });
      
      // Step 2: User starts signup form
      track("signup_flow:form_started", {
        method: "email", // or "google", "github"
      });
      
      // Step 3: User submits form
      track("signup_flow:form_submitted", {
        method: "email",
        has_referral_code: Boolean(referralCode),
      });
      
      // Step 4: User verifies email (server-side)
      posthogServer.capture({
        distinctId: userId,
        event: "signup_flow:email_verified",
        properties: {
          verification_time_seconds: verificationTimeSeconds,
        },
      });
      
      // Step 5: User completes onboarding (server-side)
      posthogServer.capture({
        distinctId: userId,
        event: "signup_flow:onboarding_completed",
        properties: {
          steps_completed: completedSteps.length,
          total_steps: ONBOARDING_STEPS_COUNT,
        },
      });
      ```
      
      **Why good:** Consistent `signup_flow:` prefix groups funnel events, each step has unique action, properties enable breakdown analysis (by method, source)
      
      ---
      
      ## PostHog Funnel Configuration
      
      ```markdown
      In PostHog Funnels:
      
      1. Create new funnel
      2. Add steps in order:
         - signup_flow:page_view
         - signup_flow:form_started
         - signup_flow:form_submitted
         - signup_flow:email_verified
         - signup_flow:onboarding_completed
      3. Set conversion window (e.g., 7 days)
      4. Break down by: method, source, plan
      
      Key metrics:
      
      - Overall conversion rate
      - Drop-off at each step
      - Time to convert
      - Breakdown by acquisition source
      ```
      
      ---
      
      ## Funnel Design Guidelines
      
      **Event naming for funnels:**
      
      1. Use consistent prefix for all funnel steps (e.g., `signup_flow:`, `checkout:`, `onboarding:`)
      2. Each step should have a unique action verb
      3. Include properties that enable segmentation
      
      **Common funnel breakdowns:**
      
      | Property            | Purpose                    |
      | ------------------- | -------------------------- |
      | `source`            | Attribution analysis       |
      | `method`            | Compare auth methods       |
      | `plan`              | Compare conversion by tier |
      | `has_referral_code` | Measure referral impact    |
      
      **Funnel tips:**
      
      - Track both client-side (UI interactions) and server-side (business events)
      - Server-side events are more reliable for critical conversions
      - Include timestamps in properties for time-to-convert analysis
      
    • group-analytics.md 2.8 KB
      # PostHog Analytics - Group Analytics for B2B
      
      > Group analytics patterns for B2B SaaS with team/organization accounts.
      >
      > **Return to:** [SKILL.md](../SKILL.md) | **Prerequisites:** [core.md](core.md), [server-tracking.md](server-tracking.md)
      >
      > **Related:** [privacy-gdpr.md](privacy-gdpr.md) | [core.md](core.md)
      
      ---
      
      ## Group Identification (Client-Side)
      
      ```typescript
      // Good Example - Identify organization group
      "use client";
      
      import { useEffect } from "react";
      import { usePostHog } from "posthog-js/react";
      
      // Use your auth solution's hook to get the active organization
      interface ActiveOrg {
        id: string;
        name: string;
        plan?: string;
        memberCount: number;
        createdAt: string;
      }
      
      export function useOrganizationAnalytics(activeOrg: ActiveOrg | null) {
        const posthog = usePostHog();
      
        useEffect(() => {
          if (!posthog || !activeOrg) return;
      
          // Identify the organization group
          posthog.group("company", activeOrg.id, {
            name: activeOrg.name,
            plan: activeOrg.plan ?? "free",
            member_count: activeOrg.memberCount,
            created_at: activeOrg.createdAt,
          });
        }, [posthog, activeOrg]);
      }
      ```
      
      **Why good:** Uses database org ID as group key, sets useful org properties, runs when org context changes
      
      ---
      
      ## Server-Side Group Events
      
      ```typescript
      // Good Example - Group event on server
      import { posthogServer } from "../lib/analytics/posthog-server";
      
      interface InviteEventData {
        userId: string;
        organizationId: string;
        inviteeEmail: string; // Don't include in properties!
        role: string;
      }
      
      export async function trackMemberInvited(data: InviteEventData) {
        posthogServer.capture({
          distinctId: data.userId,
          event: "organization:member_invited",
          properties: {
            role: data.role,
            // Note: inviteeEmail NOT included (PII)
          },
          groups: {
            company: data.organizationId,
          },
        });
      
        // Update organization properties
        posthogServer.groupIdentify({
          groupType: "company",
          groupKey: data.organizationId,
          properties: {
            last_invite_sent_at: new Date().toISOString(),
          },
        });
      
        await posthogServer.shutdown();
      }
      ```
      
      **Why good:** Event associated with both user AND organization, groupIdentify updates org properties, PII (email) excluded from properties
      
      ---
      
      ## Querying Group Metrics
      
      ```markdown
      In PostHog:
      
      - Trends: "Unique companies" aggregation
      - Funnels: "Aggregating by Unique organizations"
      - Retention: Organization-level retention curves
      - Metrics: "Daily Active Companies" instead of DAU
      ```
      
      ---
      
      ## When to Use Groups
      
      **Use groups for:**
      
      - B2B SaaS with team/organization accounts
      - Marketplaces tracking buyer and seller companies
      - Enterprise features needing org-level rollout
      
      **Limitations:**
      
      - Maximum 5 group types per project
      - One group per type per event (can't have Company A AND Company B)
      
    • privacy-gdpr.md 4.5 KB
      # PostHog Analytics - Privacy and GDPR Consent
      
      > Privacy-compliant analytics patterns for GDPR, cookieless mode, and consent management.
      >
      > **Return to:** [SKILL.md](../SKILL.md) | **Prerequisites:** [core.md](core.md)
      >
      > **Related:** [group-analytics.md](group-analytics.md) | [core.md](core.md)
      
      ---
      
      ## Cookieless Mode Configuration
      
      ```typescript
      // Good Example - GDPR-compliant initialization
      import posthog from "posthog-js";
      
      const POSTHOG_KEY = process.env.POSTHOG_KEY!;
      
      // Initialize with consent-aware settings
      posthog.init(POSTHOG_KEY, {
        api_host: "/ingest", // Use reverse proxy to avoid ad blockers
        person_profiles: "identified_only",
        capture_pageview: false,
      
        // GDPR: Don't set cookies until consent
        persistence: "localStorage+cookie",
      
        // Option 1: Full cookieless (no consent banner needed)
        // cookieless_mode: "always",
      
        // Option 2: Cookieless until consent (show banner)
        cookieless_mode: "on_reject",
      });
      ```
      
      **Why good:** `cookieless_mode: "on_reject"` respects user choice, reverse proxy increases delivery rate, `person_profiles: "identified_only"` reduces cost and data
      
      ---
      
      ## Consent Banner Integration
      
      ```typescript
      // Good Example - Consent management
      "use client";
      
      import { useState, useEffect } from "react";
      import { usePostHog } from "posthog-js/react";
      
      const CONSENT_STORAGE_KEY = "analytics_consent";
      
      type ConsentStatus = "granted" | "denied" | "pending";
      
      export function useCookieConsent() {
        const posthog = usePostHog();
        const [consent, setConsent] = useState<ConsentStatus>("pending");
      
        useEffect(() => {
          const stored = localStorage.getItem(
            CONSENT_STORAGE_KEY,
          ) as ConsentStatus | null;
          if (stored) {
            setConsent(stored);
            if (stored === "granted") {
              posthog?.opt_in_capturing();
            }
          }
        }, [posthog]);
      
        const acceptCookies = () => {
          setConsent("granted");
          localStorage.setItem(CONSENT_STORAGE_KEY, "granted");
          posthog?.opt_in_capturing();
        };
      
        const rejectCookies = () => {
          setConsent("denied");
          localStorage.setItem(CONSENT_STORAGE_KEY, "denied");
          posthog?.opt_out_capturing();
        };
      
        return { consent, acceptCookies, rejectCookies };
      }
      ```
      
      **Why good:** Persists consent choice, `opt_in_capturing()` and `opt_out_capturing()` are PostHog's official consent methods, handles pending state for first-time visitors
      
      ---
      
      ## Filtering Events with `before_send` Hook
      
      Use the `before_send` hook to filter, amend, or redact events before they're sent:
      
      ```typescript
      // Good Example - Redact PII and filter internal traffic
      import posthog from "posthog-js";
      
      const POSTHOG_KEY = process.env.POSTHOG_KEY!;
      const INTERNAL_DOMAINS = ["@mycompany.com", "@test.com"];
      
      posthog.init(POSTHOG_KEY, {
        api_host: "/ingest",
        person_profiles: "identified_only",
      
        // Filter or modify events before sending
        before_send: (event) => {
          // Skip internal user traffic
          const email = event.properties?.$email as string | undefined;
          if (email && INTERNAL_DOMAINS.some((d) => email.endsWith(d))) {
            return null; // Don't send this event
          }
      
          // Redact sensitive URL parameters
          if (event.properties?.$current_url) {
            const url = new URL(event.properties.$current_url as string);
            url.searchParams.delete("token");
            url.searchParams.delete("secret");
            event.properties.$current_url = url.toString();
          }
      
          return event;
        },
      });
      ```
      
      **Why good:** Filters internal/test traffic at source, redacts sensitive URL params, reduces noise and costs, ensures PII doesn't reach PostHog servers
      
      **Warning:** Modifying or sampling events is advanced functionality. Core PostHog features may require 100% of unmodified events. Only modify your own custom events if possible.
      
      ---
      
      ## What NOT to Track
      
      ```typescript
      // BAD - NEVER include PII in event properties
      const badProperties = {
        email: "user@example.com", // PII
        name: "John Doe", // PII
        phone: "+1234567890", // PII
        ip_address: "192.168.1.1", // PII
        address: "123 Main St", // PII
        credit_card: "4111...", // PII + Payment data
        password: "secret", // Sensitive
        ssn: "123-45-6789", // PII
      };
      
      // Good - Safe properties to include
      const goodProperties = {
        user_id: "user_abc123", // Pseudonymized ID
        plan: "pro", // Account metadata
        feature_name: "export", // Product data
        is_enterprise: true, // Boolean flags
        source: "google_ads", // Attribution
        page_path: "/dashboard", // Navigation (no PII in URL)
      };
      ```
      
      **Key rule:** Never store data that can identify a specific individual. Use pseudonymized IDs (database UUIDs) instead of emails or names.
      
    • server-tracking.md 4.2 KB
      # PostHog Analytics - Server-Side Tracking
      
      > Server-side tracking patterns with posthog-node for backend events.
      >
      > **Return to:** [SKILL.md](../SKILL.md) | **Prerequisites:** [core.md](core.md)
      >
      > **Related:** [group-analytics.md](group-analytics.md) | [privacy-gdpr.md](privacy-gdpr.md)
      
      ---
      
      ## PostHog Client Setup
      
      ```typescript
      // lib/analytics/posthog-server.ts
      import { PostHog } from "posthog-node";
      
      const POSTHOG_KEY = process.env.POSTHOG_API_KEY!;
      const POSTHOG_HOST = process.env.POSTHOG_HOST ?? "https://us.i.posthog.com";
      
      // Serverless-optimized settings
      const FLUSH_AT = 1; // Flush immediately in serverless
      const FLUSH_INTERVAL_MS = 0; // Don't wait
      
      export const posthogServer = new PostHog(POSTHOG_KEY, {
        host: POSTHOG_HOST,
        flushAt: FLUSH_AT,
        flushInterval: FLUSH_INTERVAL_MS,
      });
      ```
      
      **Why good:** `flushAt: 1` and `flushInterval: 0` ensure events are sent before serverless function terminates, prevents lost events in short-lived functions
      
      ---
      
      ## Serverless Best Practices
      
      For serverless environments, use `captureImmediate()` instead of `capture()`:
      
      ```typescript
      // Preferred for serverless - guarantees HTTP request completes
      await posthogServer.captureImmediate({
        distinctId: user.id,
        event: "subscription_created",
        properties: {
          plan: "pro",
          is_annual: true,
        },
      });
      ```
      
      **Why `captureImmediate` over `capture`:** Even with `flushAt: 1`, `capture()` is still async. Serverless environments can freeze or terminate before the request completes. `captureImmediate()` guarantees the HTTP request finishes before your function continues.
      
      **Always call `shutdown()` at the end:**
      
      ```typescript
      // Ensures all queued events are sent before function terminates
      await posthogServer.shutdown();
      ```
      
      ---
      
      ## Server-Side Event Tracking
      
      ```typescript
      // Good Example - Server-side tracking in an API route handler
      import { posthogServer } from "../lib/analytics/posthog-server";
      import { POSTHOG_EVENTS } from "../lib/analytics/constants";
      
      // Inside your route handler:
      async function handleCreateProject(user: AuthUser, body: CreateProjectBody) {
        const project = await createProject(body);
      
        // Track server-side event
        posthogServer.capture({
          distinctId: user.id, // REQUIRED: User's database ID
          event: "project_created",
          properties: {
            project_id: project.id,
            project_name: project.name, // OK if not PII
            plan: user.plan,
            is_first_project: user.projectCount === 0,
          },
        });
      
        // Ensure event is sent before response
        await posthogServer.shutdown();
      
        return { project };
      }
      ```
      
      **Why good:** `distinctId` uses database user ID, `shutdown()` ensures delivery before function ends, business event captured reliably on server
      
      ```typescript
      // Bad Example - Missing required fields
      posthogServer.capture({
        // BAD: Missing distinctId - event will fail
        event: "project_created",
        properties: {
          email: user.email, // BAD: PII in properties
        },
      });
      // BAD: No shutdown() - event may be lost in serverless
      ```
      
      **Why bad:** Missing distinctId causes event failure, PII in properties violates privacy, no shutdown() risks losing events in serverless
      
      ---
      
      ## Tracking Auth Events
      
      ```typescript
      // lib/auth-events.ts
      import { posthogServer } from "../lib/analytics/posthog-server";
      import { POSTHOG_EVENTS } from "../lib/analytics/constants";
      
      interface AuthEventUser {
        id: string;
        plan?: string;
        createdAt?: Date;
      }
      
      export async function trackUserSignedUp(user: AuthEventUser) {
        posthogServer.capture({
          distinctId: user.id,
          event: POSTHOG_EVENTS.USER_SIGNED_UP,
          properties: {
            plan: user.plan ?? "free",
            signup_timestamp: new Date().toISOString(),
          },
        });
      
        // Set user properties
        posthogServer.identify({
          distinctId: user.id,
          properties: {
            plan: user.plan ?? "free",
            created_at: user.createdAt?.toISOString(),
          },
        });
      
        await posthogServer.shutdown();
      }
      
      export async function trackUserLoggedIn(user: AuthEventUser) {
        posthogServer.capture({
          distinctId: user.id,
          event: POSTHOG_EVENTS.USER_LOGGED_IN,
          properties: {
            login_timestamp: new Date().toISOString(),
          },
        });
      
        await posthogServer.shutdown();
      }
      ```
      
      **Why good:** Centralized auth event tracking, `identify()` sets user properties once, `shutdown()` ensures delivery
      
    • type-safety.md 3.8 KB
      # PostHog Analytics - TypeScript Type-Safe Events
      
      > Type-safe event tracking patterns with TypeScript compile-time validation.
      >
      > **Return to:** [SKILL.md](../SKILL.md) | **Prerequisites:** [core.md](core.md)
      >
      > **Related:** [server-tracking.md](server-tracking.md) | [funnel-analysis.md](funnel-analysis.md)
      
      ---
      
      ## Official Types Package
      
      For type safety with `window.posthog`, install the official types package:
      
      ```bash
      npm install @posthog/types
      ```
      
      This provides TypeScript definitions for the global PostHog object when using the script tag method.
      
      ---
      
      ## Event Type Definitions
      
      ```typescript
      // lib/analytics/types.ts
      
      // Event name union type
      export type AnalyticsEvent =
        | "user_signed_up"
        | "user_logged_in"
        | "user_logged_out"
        | "project_created"
        | "project_deleted"
        | "subscription_created"
        | "subscription_cancelled"
        | "feature:used"
        | "onboarding:step_completed"
        | "signup_flow:form_submitted";
      
      // Base properties all events should have
      interface BaseEventProperties {
        timestamp?: string;
      }
      
      // Event-specific property types
      interface UserSignedUpProperties extends BaseEventProperties {
        plan: "free" | "pro" | "enterprise";
        source?: string;
      }
      
      interface ProjectCreatedProperties extends BaseEventProperties {
        project_id: string;
        is_first_project: boolean;
      }
      
      interface FeatureUsedProperties extends BaseEventProperties {
        feature_name: string;
        source?: string;
      }
      
      interface OnboardingStepProperties extends BaseEventProperties {
        step_number: number;
        step_name: string;
      }
      
      // Event property map
      export interface EventPropertyMap {
        user_signed_up: UserSignedUpProperties;
        project_created: ProjectCreatedProperties;
        "feature:used": FeatureUsedProperties;
        "onboarding:step_completed": OnboardingStepProperties;
        // Add more as needed...
      }
      
      // Named exports
      export type {
        AnalyticsEvent,
        EventPropertyMap,
        UserSignedUpProperties,
        ProjectCreatedProperties,
      };
      ```
      
      ---
      
      ## Type-Safe Track Function
      
      ```typescript
      // ✅ Good Example - Type-safe tracking
      import type { AnalyticsEvent, EventPropertyMap } from "@/lib/analytics/types";
      
      export function useTypedAnalytics() {
        const posthog = usePostHog();
      
        // Overloaded track function for type safety
        function track<E extends keyof EventPropertyMap>(
          event: E,
          properties: EventPropertyMap[E],
        ): void;
        function track(
          event: AnalyticsEvent,
          properties?: Record<string, unknown>,
        ): void;
        function track(event: string, properties?: Record<string, unknown>): void {
          posthog?.capture(event, properties);
        }
      
        return { track };
      }
      
      // Usage - TypeScript catches errors at compile time
      const { track } = useTypedAnalytics();
      
      // ✅ Correct - TypeScript validates properties
      track("user_signed_up", {
        plan: "pro",
        source: "google_ads",
      });
      
      // ❌ Error - Missing required property "plan"
      track("user_signed_up", {
        source: "google_ads",
      });
      
      // ❌ Error - Invalid plan value
      track("user_signed_up", {
        plan: "invalid_plan", // Type error!
      });
      ```
      
      **Why good:** Compile-time validation catches typos and missing properties, IDE autocomplete for event names and properties, type definitions serve as documentation
      
      ---
      
      ## Benefits of Type-Safe Events
      
      | Benefit                 | Description                                       |
      | ----------------------- | ------------------------------------------------- |
      | Compile-time validation | Catch typos and missing properties before runtime |
      | IDE autocomplete        | Get suggestions for event names and properties    |
      | Self-documenting        | Type definitions serve as living documentation    |
      | Refactor-safe           | Rename events/properties with confidence          |
      | Team alignment          | Shared types ensure consistent tracking           |
      
      **When to use:**
      
      - Teams with multiple developers tracking events
      - Products with many events and properties
      - Codebases where analytics accuracy is critical
      
  • reference.md 6.3 KB
    # PostHog Analytics - Reference Guide
    
    > Decision frameworks, anti-patterns, and red flags for PostHog analytics.
    >
    > **Return to:** [SKILL.md](SKILL.md) for core concepts.
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Client-Side vs Server-Side Tracking
    
    ```
    Is the event a business action (signup, purchase, subscription)?
    ├── YES → Server-side (posthog-node)
    │   └── More reliable, not blocked by ad blockers
    └── NO → Is it a UI interaction (click, scroll, form input)?
        ├── YES → Client-side (posthog-js/react)
        └── NO → Consider if you need to track it at all
    ```
    
    ### Anonymous vs Identified Events
    
    ```
    Do you need to associate events with a user profile?
    ├── YES → Identified events
    │   └── Use posthog.identify() with user database ID
    └── NO → Anonymous events (4x cheaper)
        └── Use person_profiles: "identified_only"
    ```
    
    ### When to Use Groups
    
    ```
    Is this a B2B product with multi-user accounts?
    ├── YES → Use group analytics
    │   ├── company: Organization/team accounts
    │   ├── project: Multi-project workspaces
    │   └── Max 5 group types per project
    └── NO → Standard user-level analytics
    ```
    
    </decision_framework>
    
    ---
    
    <integration>
    
    ## Integration Points
    
    **Where analytics hooks into your app:**
    
    - **Authentication**: Call `identify()` on login/signup, `reset()` on logout
    - **Server routes**: Server-side event tracking with posthog-node for business events
    - **Feature flags**: PostHog's feature flag system uses the same instance (separate concern from analytics)
    
    **Conflicts with:**
    
    - Other product analytics platforms -- choose one primary analytics tool to avoid event duplication
    
    </integration>
    
    ---
    
    <anti_patterns>
    
    ## Anti-Patterns
    
    ### Identifying on Every Page Load
    
    ```typescript
    // ANTI-PATTERN: Identify on every render
    function App() {
      const posthog = usePostHog();
    
      // BAD: Runs on every render!
      posthog?.identify(userId);
    }
    ```
    
    **Why it's wrong:** Creates unnecessary API calls, degrades performance, may cause rate limiting.
    
    **What to do instead:** Check `_isIdentified()` first, or only identify on auth state changes.
    
    ---
    
    ### PII in Event Properties
    
    ```typescript
    // ANTI-PATTERN: Storing PII
    posthog.capture("user_action", {
      email: user.email, // BAD
      name: user.name, // BAD
      phone: user.phone, // BAD
    });
    ```
    
    **Why it's wrong:** Violates GDPR, creates liability, can't be easily deleted.
    
    **What to do instead:** Use user ID only, set safe properties on person profile.
    
    ---
    
    ### Missing Server-Side Shutdown
    
    ```typescript
    // ANTI-PATTERN: No shutdown in serverless
    export async function handler() {
      posthogServer.capture({
        distinctId: userId,
        event: "important_event",
      });
    
      return response; // Event may be lost!
    }
    ```
    
    **Why it's wrong:** Serverless functions terminate before batch flush. Even with `flushAt: 1`, `capture()` is async and may not complete.
    
    **What to do instead:** Use `captureImmediate()` or always `await posthogServer.shutdown()` before returning.
    
    ---
    
    ### Vague Event Names
    
    ```typescript
    // ANTI-PATTERN: Unhelpful event names
    posthog.capture("click");
    posthog.capture("submit");
    posthog.capture("action");
    ```
    
    **Why it's wrong:** Can't distinguish between different clicks/submits, analysis impossible.
    
    **What to do instead:** Use `category:object_action` format.
    
    </anti_patterns>
    
    ---
    
    ## Event Taxonomy Reference
    
    ### Naming Convention
    
    | Pattern                  | Example                   | Use Case                      |
    | ------------------------ | ------------------------- | ----------------------------- |
    | `category:object_action` | `signup_flow:form_submit` | Grouped events for funnels    |
    | `object_action`          | `project_created`         | Simple business events        |
    | `$pageview`              | `$pageview`               | Page views (PostHog standard) |
    
    ### Property Conventions
    
    | Pattern            | Example                    | Description       |
    | ------------------ | -------------------------- | ----------------- |
    | `object_adjective` | `project_id`, `plan_name`  | Entity references |
    | `is_*`             | `is_first_purchase`        | Boolean flags     |
    | `has_*`            | `has_completed_onboarding` | Boolean state     |
    | `*_date`           | `trial_end_date`           | Date strings      |
    | `*_timestamp`      | `last_login_timestamp`     | Unix timestamps   |
    | `*_count`          | `item_count`               | Numeric counts    |
    
    ### Standard PostHog Events
    
    | Event                  | Description                          |
    | ---------------------- | ------------------------------------ |
    | `$pageview`            | Page view (capture manually in SPAs) |
    | `$pageleave`           | User leaves page                     |
    | `$autocapture`         | Automatic click/form tracking        |
    | `$feature_flag_called` | Feature flag evaluated               |
    
    ---
    
    ## Troubleshooting
    
    ### Events Not Appearing
    
    1. **Check initialization**: Ensure `posthog.init()` called with valid key
    2. **Check distinct_id**: Server-side events require explicit `distinctId`
    3. **Check shutdown**: Serverless needs `await posthogServer.shutdown()` or use `captureImmediate()`
    4. **Check filters**: Verify not filtering by environment/user
    
    ### User Profiles Not Linked
    
    1. **Check identify timing**: Only identify on auth changes
    2. **Check distinct_id consistency**: Use same ID client and server
    3. **Check reset on logout**: Missing `reset()` causes profile mixing
    
    ### High Costs
    
    1. **Enable `person_profiles: "identified_only"`**: 4x cheaper anonymous events
    2. **Disable autocapture**: If not using automatic click tracking
    3. **Reduce pageview tracking**: Only track meaningful pages
    
    ---
    
    ## Sources
    
    - [PostHog Event Tracking Guide](https://posthog.com/tutorials/event-tracking-guide)
    - [PostHog Best Practices](https://posthog.com/docs/product-analytics/best-practices)
    - [PostHog User Identification](https://posthog.com/docs/getting-started/identify-users)
    - [PostHog Group Analytics](https://posthog.com/docs/product-analytics/group-analytics)
    - [PostHog GDPR Compliance](https://posthog.com/docs/privacy/gdpr-compliance)
    - [PostHog Node.js SDK](https://posthog.com/docs/libraries/node)
    - [PostHog React SDK](https://posthog.com/docs/libraries/react)
    - [PostHog JS Configuration](https://posthog.com/docs/libraries/js/config)
    
  • SKILL.md 10.1 KB
    ---
    name: api-analytics-posthog-analytics
    description: PostHog event tracking, user identification, group analytics for B2B, GDPR consent patterns. Use when implementing product analytics, tracking user behavior, setting up funnels, or configuring privacy-compliant tracking.
    ---
    
    # PostHog Analytics Patterns
    
    > **Quick Guide:** Use PostHog for product analytics with structured event naming (`category:object_action`), server-side tracking for reliability, and proper user identification integrated with your authentication flow. Client-side for UI interactions, server-side for business events. Always call `reset()` on logout, never store PII in event properties, and use `captureImmediate()` or `await shutdown()` in serverless environments.
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Event naming, user identification, property conventions
    - [examples/client-tracking.md](examples/client-tracking.md) - React hooks, provider setup, component tracking
    - [examples/server-tracking.md](examples/server-tracking.md) - posthog-node, serverless patterns, auth events
    - [examples/group-analytics.md](examples/group-analytics.md) - B2B organization tracking
    - [examples/privacy-gdpr.md](examples/privacy-gdpr.md) - GDPR consent, cookieless mode, PII filtering
    - [reference.md](reference.md) - Decision frameworks, anti-patterns, event taxonomy
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST call `posthog.identify()` ONLY when a user signs up or logs in - never on every page load)**
    
    **(You MUST include the user's database ID as `distinct_id` in ALL server-side events)**
    
    **(You MUST call `posthog.reset()` when a user logs out to unlink future events)**
    
    **(You MUST use the `category:object_action` naming convention for all custom events)**
    
    **(You MUST NEVER include PII (email, name, phone) in event properties - use user IDs only)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** PostHog, posthog-js, posthog-node, usePostHog, PostHogProvider, capture, identify, group analytics, product analytics, event tracking, funnel analysis
    
    **When to use:**
    
    - Tracking user behavior and product analytics
    - Setting up conversion funnels and retention analysis
    - Implementing group analytics for B2B multi-tenant apps
    - Understanding feature adoption and user journeys
    - A/B testing analysis (in conjunction with feature flags)
    
    **When NOT to use:**
    
    - Feature flag implementation (separate concern)
    - Error tracking and logging (use dedicated error tracking tools)
    - Infrastructure monitoring (use observability tools)
    
    **Key patterns covered:**
    
    - Event naming conventions (`category:object_action`)
    - Property naming patterns (`object_adjective`, `is_`/`has_` booleans)
    - User identification with authentication flow integration
    - Client-side tracking with React hooks
    - Server-side tracking with posthog-node
    - Group analytics for B2B organizations
    - Privacy and GDPR consent patterns
    - TypeScript patterns for type-safe events
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    PostHog analytics follows a **structured taxonomy** approach: consistent naming conventions, meaningful properties, and strategic placement (client vs server). Track what matters for product decisions, not everything.
    
    **Core principles:**
    
    1. **Server-side for business events** - User signups, purchases, subscriptions (reliable, not blocked)
    2. **Client-side for UI interactions** - Button clicks, page views, form interactions
    3. **Identify once per session** - Not on every page load
    4. **Structured naming** - Makes querying and analysis possible at scale
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Event Naming Conventions
    
    Use the **`category:object_action`** framework for consistent, queryable event names.
    
    ```typescript
    // category: Context (signup_flow, settings, dashboard)
    // object: Component/location (password_button, pricing_page)
    // action: Present-tense verb (click, submit, view)
    
    "signup_flow:email_form_submit";
    "dashboard:project_create";
    "settings:billing_plan_upgrade";
    
    // Simpler alternative: object_verb
    "project_created";
    "user_signed_up";
    ```
    
    **Why good:** Category prefix groups related events in PostHog UI, enables wildcard queries like `signup_flow:*`, consistent naming makes analysis possible at scale.
    
    **Property naming rules:**
    
    - `object_adjective`: `project_id`, `plan_name`, `item_count`
    - `is_` / `has_` for booleans: `is_first_purchase`, `has_completed_onboarding`
    - `_date` / `_timestamp` suffix: `trial_end_date`, `last_login_timestamp`
    
    See [examples/core.md](examples/core.md) for complete naming examples.
    
    ---
    
    ### Pattern 2: User Identification with Authentication
    
    Call `identify()` only on auth state change (not every render). Use database user ID as `distinct_id`. Call `reset()` on logout.
    
    ```typescript
    // Check _isIdentified() to prevent duplicate calls
    useEffect(() => {
      if (session?.user && !posthog._isIdentified()) {
        posthog.identify(session.user.id, {
          plan: session.user.plan ?? "free",
          created_at: session.user.createdAt,
          is_verified: session.user.emailVerified ?? false,
        });
      }
    }, [session?.user]);
    ```
    
    ```typescript
    // Always reset on logout
    posthog?.capture("user_logged_out");
    posthog?.reset(); // Unlink future events from this user
    ```
    
    See [examples/core.md](examples/core.md) for full identification hook and logout handler.
    
    ---
    
    ### Pattern 3: Server-Side Tracking
    
    Track business events reliably from your backend with posthog-node.
    
    ```typescript
    // Serverless: use captureImmediate (guarantees HTTP completion)
    await posthogServer.captureImmediate({
      distinctId: user.id,
      event: "subscription_created",
      properties: { plan: "pro", is_annual: true },
    });
    
    // Always call shutdown before returning in serverless
    await posthogServer.shutdown();
    ```
    
    **Key rules:**
    
    1. Always include `distinctId` (user's database ID)
    2. Use `captureImmediate()` for serverless (guarantees HTTP completion)
    3. Always call `shutdown()` before returning in serverless
    4. Configure `flushAt: 1` and `flushInterval: 0` for serverless
    
    See [examples/server-tracking.md](examples/server-tracking.md) for complete server setup and route examples.
    
    ---
    
    ### Pattern 4: Group Analytics (B2B)
    
    Associate events with organizations using PostHog groups for B2B metrics.
    
    ```typescript
    // Client-side: identify organization
    posthog.group("company", org.id, {
      name: org.name,
      plan: org.plan ?? "free",
      member_count: org.memberCount,
    });
    
    // Server-side: include groups in event
    posthogServer.capture({
      distinctId: user.id,
      event: "organization:member_invited",
      properties: { role: data.role },
      groups: { company: data.organizationId },
    });
    ```
    
    **Limitations:** Maximum 5 group types per project. One group per type per event.
    
    See [examples/group-analytics.md](examples/group-analytics.md) for complete group patterns.
    
    ---
    
    ### Pattern 5: Privacy and GDPR Consent
    
    PostHog supports cookieless tracking and consent management.
    
    ```typescript
    // Cookieless mode: "always" (no consent needed) or "on_reject" (with banner)
    posthog.init(POSTHOG_KEY, {
      cookieless_mode: "on_reject",
      person_profiles: "identified_only",
    });
    
    // Consent methods
    posthog.opt_in_capturing(); // User accepts
    posthog.opt_out_capturing(); // User rejects
    ```
    
    **Key rule:** Never store PII (email, name, phone, IP, address) in event properties. Use pseudonymized IDs only.
    
    See [examples/privacy-gdpr.md](examples/privacy-gdpr.md) for consent banner integration and `before_send` filtering.
    
    </patterns>
    
    ---
    
    <performance>
    
    ## Performance Optimization
    
    **Web Apps (default batching):** Use default settings -- PostHog batches efficiently out of the box.
    
    **Serverless (immediate delivery):**
    
    ```typescript
    const posthogServer = new PostHog(POSTHOG_KEY, {
      flushAt: 1, // Flush after 1 event
      flushInterval: 0, // No interval batching
    });
    // Use captureImmediate() or capture() + await shutdown()
    ```
    
    **Reducing Costs:**
    
    ```typescript
    posthog.init(POSTHOG_KEY, {
      person_profiles: "identified_only", // Anonymous events 4x cheaper
      autocapture: false, // Disable for high-traffic sites
    });
    ```
    
    </performance>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Using email as `distinct_id` -- PII should not be the identifier
    - Missing `posthog.reset()` on logout -- users get mixed together
    - No `await shutdown()` in serverless -- events are lost
    - PII in event properties -- GDPR violation risk
    - Calling `identify()` on every render -- performance degradation
    
    **Common Mistakes:**
    
    - Importing `posthog` directly instead of using `usePostHog` hook in React
    - Not setting up reverse proxy (`api_host: "/ingest"`) -- events blocked by ad blockers
    - Different event names for same action on frontend vs backend
    - Not using `person_profiles: "identified_only"` -- 4x higher costs on anonymous events
    - Using `capture()` instead of `captureImmediate()` in serverless -- events may not complete
    
    **Gotchas & Edge Cases:**
    
    - `distinct_id` is required for ALL server-side events (unlike client-side which auto-generates one)
    - `group()` must include group ID with every event (not persisted like `identify()`)
    - Maximum 5 group types per project
    - `cookieless_mode: "always"` disables `identify()` entirely -- privacy trade-off
    - PostHog web SDK is client-side only -- will not work in server components
    - Session IDs must be manually passed to server-side events for session linking
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST call `posthog.identify()` ONLY when a user signs up or logs in - never on every page load)**
    
    **(You MUST include the user's database ID as `distinct_id` in ALL server-side events)**
    
    **(You MUST call `posthog.reset()` when a user logs out to unlink future events)**
    
    **(You MUST use the `category:object_action` naming convention for all custom events)**
    
    **(You MUST NEVER include PII (email, name, phone) in event properties - use user IDs only)**
    
    **Failure to follow these rules will cause analytics data quality issues, privacy violations, or lost events.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related