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.
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-analytics-posthog-analytics/skills/api-analytics-posthog-analytics
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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 callreset()on logout, never store PII in event properties, and usecaptureImmediate()orawait shutdown()in serverless environments.
Detailed Resources:
- examples/core.md - Event naming, user identification, property conventions
- examples/client-tracking.md - React hooks, provider setup, component tracking
- examples/server-tracking.md - posthog-node, serverless patterns, auth events
- examples/group-analytics.md - B2B organization tracking
- examples/privacy-gdpr.md - GDPR consent, cookieless mode, PII filtering
- 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
<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
posthogdirectly instead of usingusePostHoghook 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 ofcaptureImmediate()in serverless -- events may not complete
Gotchas & Edge Cases:
distinct_idis required for ALL server-side events (unlike client-side which auto-generates one)group()must include group ID with every event (not persisted likeidentify())- Maximum 5 group types per project
cookieless_mode: "always"disablesidentify()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.
Reviews (0)
No reviews yet.
No comments yet.