Skip to Content
AnalyticsOverview

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?

BackendPurposeUsed by
SupabaseSimple event counters (views, downloads, shares)Embed HUD badges, /api/analytics GET
PostHogRich 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 event

View 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+cookie normally, memory when in cross-origin iframes