Analytics
The project uses a dual-write strategy: every user interaction writes to both Supabase (for fast counters) and PostHog (for rich analytics, sessions, and dashboards).
Why dual-write?
| Backend | Purpose | Used by |
|---|---|---|
| Supabase | Simple event counters (views, downloads, shares) | Embed HUD badges, /api/analytics GET |
| PostHog | Rich analytics: funnels, session recordings, HogQL queries | /analytics dashboard, error tracking |
Both backends are optional — the app degrades gracefully if either is not configured.
How events are fired
The useAnalytics hook (lib/use-analytics.ts) manages all analytics:
useAnalytics(gpxUrl, pageUrl)
→ On mount: fetch Supabase counters + fire view event
→ trackDownload(): dual-write download event
→ trackShare(channel): dual-write share event
→ trackOpenRouteStart(): dual-write navigation eventView deduplication
Views are deduplicated per session per GPX URL:
- A
btt_viewed_{gpxUrl}flag is stored in sessionStorage - If the flag exists, the view event is skipped
- A React ref (
viewFired) prevents duplicate fires from re-renders
Session ID
A stable session ID is generated and stored in sessionStorage:
{timestamp_base36}-{random_base36}Falls back to a timestamp-based ID if sessionStorage is unavailable (e.g., cross-origin iframes).
Event delivery
Events are sent via navigator.sendBeacon (preferred, survives page close) with fetch as fallback. All analytics calls are fire-and-forget — failures never break the UI.
Internal user filtering
The /api/analytics/ip-check endpoint detects admin/internal IPs and returns { internal: true }. On the client (instrumentation-client.ts), if internal:
posthog.register({ is_internal: true });This tags all subsequent PostHog events with is_internal: true. The analytics dashboard filters these out using PostHog’s “Filter out internal and test users” feature.
PostHog setup
PostHog is initialized in instrumentation-client.ts (Next.js 15.3+ pattern):
- API host:
/ingest(proxied via next.config.ts rewrites to avoid ad blockers) - Session recording: enabled
- Error tracking: enabled (
capture_exceptions: true) - Persistence:
localStorage+cookienormally,memorywhen in cross-origin iframes