# Analytics Tracker
Typed event tracking for Next.js 14 — PostHog wrapper with auto page view hook, user identification, a self-hosted Supabase fallback, and six pre-written SQL queries for DAU, WAU, funnel, revenue, and retention cohorts.
## What's included
**PostHog wrapper**
- `initAnalytics(config)` — initialises PostHog with lazy import; disables autocapture and built-in pageview tracking; accepts `posthogKey`, `apiHost`, `debug`, `disabled`
- `track(event, properties?)` — captures a typed `AnalyticsEvent` with an auto-added `timestamp`; logs to console in development; no-ops silently if PostHog isn't loaded
- `identifyUser(userId, traits?)` — calls `posthog.identify`; pass after login/session restore
- `resetAnalytics()` — calls `posthog.reset`; call on logout to disassociate the device
- `trackPageView(url?)` — manual page view capture; defaults to `window.location.pathname`
- `setUserProperty(key, value)` — sets a PostHog person property via `people.set`
**React hooks**
- `usePageTracking()` — auto-fires `trackPageView` on `usePathname` changes; deduplicates consecutive fires; drop into a layout component
- `useTrackOnce(event, props?)` — fires a single event on mount; useful for impression tracking on detail pages
**Self-hosted Supabase fallback**
- `trackToSupabase(event, userId, properties?)` — POSTs to `/api/analytics`; includes session ID (from `sessionStorage`), URL, and referrer; use when PostHog is unavailable or for private data
- `ANALYTICS_API_ROUTE` — paste-ready content for `app/api/analytics/route.ts`; inserts into `analytics_events` via service role
**SQL queries**
- `ANALYTICS_QUERIES.dailyActiveUsers` — DAU for the last 30 days
- `ANALYTICS_QUERIES.weeklyActiveUsers` — WAU for the last 90 days
- `ANALYTICS_QUERIES.topEvents` — top 15 events by count in the last 7 days
- `ANALYTICS_QUERIES.conversionFunnel` — page view → detail view → purchase started → purchase completed counts for the last 30 days
- `ANALYTICS_QUERIES.revenueByEvent` — revenue and purchase count per `block_id` from `purchase_completed` events
- `ANALYTICS_QUERIES.retentionCohort` — weekly retention cohort table by signup week
**Types**
- `AnalyticsEvent` — union of 21 typed event names
- `EventProperties` — `Record<string, string | number | boolean | null | undefined>`
## Setup
### 1. Install dependencies
```bash
npm install posthog-js
```
### 2. Environment variables
```
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxxxxxxxxxxxxxxxxx # PostHog project API key
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com # or your self-hosted host
# Self-hosted fallback only:
NEXT_PUBLIC_SUPABASE_URL=your Supabase project URL
SUPABASE_SERVICE_ROLE_KEY=service role key (server-only)
```
### 3. Initialise in your providers
```tsx
// app/providers.tsx
'use client'
import { useEffect } from 'react'
import { initAnalytics, usePageTracking } from '@/blocks/analytics'
function PageTracker() { usePageTracking(); return null }
export function Providers({ children }: { children: React.ReactNode }) {
useEffect(() => {
initAnalytics({ posthogKey: process.env.NEXT_PUBLIC_POSTHOG_KEY! })
}, [])
return <>{children}<PageTracker /></>
}
```
### 4. Self-hosted fallback (optional)
Copy `ANALYTICS_API_ROUTE` to `app/api/analytics/route.ts`, then run this in Supabase:
```sql
CREATE TABLE analytics_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
session_id TEXT NOT NULL,
event TEXT NOT NULL,
properties JSONB NOT NULL DEFAULT '{}',
url TEXT,
referrer TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX analytics_event_type_idx ON analytics_events(event, created_at DESC);
CREATE INDEX analytics_user_idx ON analytics_events(user_id, created_at DESC);
```
## Usage examples
```ts
// Track events anywhere — server or client
import { track } from '@/blocks/analytics'
// After a successful purchase
track('purchase_completed', { block_id: 'auth', amount: 19, currency: 'USD' })
// After a failed purchase
track('purchase_failed', { block_id: 'auth', reason: 'card_declined' })
```
```ts
// Identify user after login (call once per session)
import { identifyUser, resetAnalytics } from '@/blocks/analytics'
// On sign-in:
identifyUser(session.user.id, {
email: session.user.email,
role: session.user.role,
plan: 'pro',
createdAt: session.user.createdAt,
})
// On sign-out:
resetAnalytics()
```
```tsx
// Track a block detail view once on mount
'use client'
import { useTrackOnce } from '@/blocks/analytics'
export function BlockDetailPage({ block }) {
useTrackOnce('block_detail_viewed', { block_id: block.id, price: block.price })
return <div>{/* content */}</div>
}
```
## Notes
- `initAnalytics` uses a dynamic `import('posthog-js')` — PostHog is not loaded until `initAnalytics` is called, so events fired before the async import resolves are silently dropped; call `initAnalytics` as early as possible (top of `providers.tsx`)
- `track` is a no-op on the server — the `_posthog` module variable is always `null` in RSC/API routes; use `trackToSupabase` for server-side event capture
- `ANALYTICS_QUERIES` are plain SQL strings — run them via `db.rpc`, a Supabase SQL editor, or a migration; they reference the `analytics_events` table and the `properties` JSONB column directly, so they only work with the self-hosted fallback, not with PostHog
- `conversionFunnel` counts events across all users for the period, not per-user funnel steps — a user who viewed a detail page 10 times counts as 10; for true per-user funnel analysis, use PostHog's built-in funnel charts