Set up a provider-agnostic product analytics system. Use PostHog as the default provider, but architect it so providers can be swapped without changing application code.
Architecture
Create a multi-layer analytics system:
- Provider Layer (lib/analytics/providers.ts): Abstract dispatcher that sends events to any enabled provider (PostHog, Statsig, Mixpanel, etc.). Include enable/disable checks so providers fail gracefully.
- Events Layer (lib/analytics/events.ts): Type-safe analytics API with methods like analytics.userSignedUp(), analytics.featureUsed(). This layer calls the provider dispatcher, never directly calling PostHog/etc.
- Provider Implementation (lib/analytics/posthog.tsx): Provider-specific wrapper component and initialization.
- User Identification (lib/analytics/use-identify.ts): Hook that fetches user profile data and identifies users across all enabled providers.
- Feature Flags (lib/analytics/feature-flags.ts, lib/analytics/use-feature-flag.ts): Provider-specific feature flag utilities if supported.
Key Requirements
- Centralized exports: lib/analytics/index.ts exports everything
- Type safety: Define entity types and event properties with TypeScript
- Error isolation: If one provider fails, app continues working
- Environment safety: Auto-disable in test environments (check PLAYWRIGHT_TEST and NODE_ENV)
- User identification: Auto-fetch relevant user properties from database and send to analytics
- Manual pageview tracking: Capture page views on navigation, not automatically
Event Categories to Track
Define events for your product's key user journeys:
- Authentication flow: Sign up start/complete, sign in, sign out
- Core actions: CRUD operations for your main entities
- Engagement: Interactions users have with content (likes, shares, saves, etc.)
- Discovery: Search, filtering, navigation patterns
- Feature adoption: Key feature usage, onboarding steps, settings changes
- Conversion: Payment events, upgrade actions, goal completions
Group events by user journey and lifecycle stage for easier analysis.
PostHog Example Setup
Provider (lib/analytics/posthog.tsx):
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://app.posthog.com",
person_profiles: "identified_only",
capture_pageview: false,
session_recording: { maskAllInputs: true }
});
CSP Configuration (in proxy.ts or middleware.ts):
const cspHeader = [ "script-src 'self' 'unsafe-inline' https://*.posthog.com",
"connect-src 'self' https://*.posthog.com",
"style-src 'self' 'unsafe-inline' https://*.posthog.com",
"img-src 'self' data: https: blob:",
"font-src 'self' data: https://*.posthog.com",
].join("; ");
Environment Variables:
Please add these to .env.example
NEXT_PUBLIC_POSTHOG_KEY=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
Testing
Mock the analytics module in tests:
vi.mock("@/lib/analytics", () => ({
analytics: { contentCreated: vi.fn() }
}));
The architecture should allow switching from PostHog to Mixpanel/Amplitude/etc. by only changing the provider layer, not the events layer or application code.