VexellDocs · v1.5

Reference

Analytics

Vexell ships a privacy-first analytics scaffold: vendor-agnostic, consent-gated, GDPR/CCPA-ready out of the box. Nothing fires before the user accepts cookies.

Consent first by default. The CookieConsent banner mounts globally. If the user hasn't accepted, track() is a no-op (history is still kept locally for the dev panel).

Track an event

Pick your edition. The interface is symmetric — only the call site differs.

tsx
// 1. From any client component:
'use client'
import { useAnalytics } from '@/lib/analytics'

export function CTAButton() {
  const analytics = useAnalytics()
  return (
    <button
      onClick={() => analytics.track('cta_clicked', {
        label: 'hero',
        location: 'home',
      })}
    >
      Start free
    </button>
  )
}

// 2. Typed event catalog at lib/analytics/events.ts:
import type { AnalyticsEvent } from '@/lib/analytics/events'
// AnalyticsEvent unions Marketing + App + Security event names.
// Misspelled event names fail at compile time.

// 3. Inspect from devtools:
//    window.vexellAnalytics.consented
//    window.vexellAnalytics.history

Cross-tab consent sync

Both editions persist consent to localStorage and dispatch a vexell:consent-changed CustomEvent. Listening to both gives you cross-tab + same-tab coverage.

javascript
// CookieConsent persists to localStorage AND dispatches a CustomEvent
// so cross-tab AND same-tab listeners stay in sync.
//
// localStorage events fire across tabs; the custom event covers same-tab.
// AnalyticsProvider listens to both and load/unloads its vendor.

window.dispatchEvent(new CustomEvent('vexell:consent-changed', {
  detail: { analytics: true, marketing: true, preferences: true }
}))

localStorage.setItem('vexell.consent.v1', JSON.stringify({
  analytics: true, marketing: true, preferences: true,
  timestamp: Date.now(),
}))

PostHog adapter sample

Drop in any vendor. The provider exposes a load(consent) / track(name, payload) / unload() interface. Below is a working PostHog adapter; GA4, Plausible, and Segment follow the same shape.

typescript
// lib/analytics/adapters/posthog.ts (Next.js — same shape works for HTML)
import posthog from 'posthog-js'

export const posthogAdapter = {
  load(consent) {
    if (!consent.analytics) return
    posthog.init(process.env.NEXT_PUBLIC_ANALYTICS_KEY, {
      api_host: process.env.NEXT_PUBLIC_ANALYTICS_HOST,
      capture_pageview: false,         // we'll fire it ourselves
      autocapture: false,              // privacy-first default
      disable_session_recording: true, // turn on only with explicit consent
    })
  },
  track(name, payload) {
    posthog.capture(name, payload)
  },
  identify(userId, traits) {
    posthog.identify(userId, traits)
  },
  unload() {
    posthog.reset()
  },
}

Read next