Consent Management
Trackkit ships with a lightweight consent layer designed to gate analytics reliably while staying out of your way. It works in browsers, supports SSR hydration, and plays nicely with your own CMP or banner.
- Default behavior: nothing is sent until allowed by policy.
- When consent is denied: events are dropped (optionally allowing only “essential” events).
- When consent is pending: events are queued and flushed on grant.
If you haven’t seen it yet, skim Configuration → Consent for env-based config. This page focuses on behavior and usage.
TL;DR Quick Start
import { init, grantConsent, denyConsent, onConsentChange, getConsent } from 'trackkit';
const analytics = createAnalytics({
provider: 'umami',
site: 'your-site-id',
consent: {
initialStatus: 'pending', // 'pending' | 'granted' | 'denied'
requireExplicit: true, // if true, we never auto-promote to granted
allowEssentialOnDenied: false,
policyVersion: '2024-01-15',
},
});
// Show UI if pending
if (analytics.getConsent()?.status === 'pending') showBanner();
// Button handlers
acceptBtn.onclick = () => analytics.grantConsent();
rejectBtn.onclick = () => analytics.denyConsent();
// React to changes
const off = analytics.onConsentChange((status, prev) => {
if (status === 'granted') hideBanner();
});Using the singleton API? Replace
analytics.grantConsent()withgrantConsent()and skipcreateAnalytics.
Using a CMP? Just wire your CMP callbacks to
grantConsent()/denyConsent().
Consent Model
States
- pending No events sent. Events are queued in memory and flushed if consent is later granted.
- granted All events flow immediately (subject to DNT, domain filters, etc.).
- denied Non-essential events are dropped (not queued). If
allowEssentialOnDeniedistrue, essential events (likeidentify) still send; otherwise they’re blocked.
Transitions
pending → grantedwhen you callgrantConsent()(or implicit grant; see below).pending → deniedwhen you calldenyConsent().- Any state →
pendingwhen you callresetConsent()or whenpolicyVersionchanges.
Essential vs analytics categories
Trackkit splits events into essential and analytics categories. Internally, only identify and minimal provider bootstrap signals are essential. All track / pageview events are analytics unless marked otherwise by your own wrapper.
When allowEssentialOnDenied = true, essential events can still be sent after denial (useful for strictly-necessary product telemetry). Otherwise, all events are blocked on denial.
Configuration
Keep this minimal at first, then adjust as your policy demands:
const analytics = createAnalytics({
consent: {
/**
* Initial status at startup. If stored consent exists (and matches policyVersion),
* the stored value overrides this.
* @default 'pending'
*/
initialStatus: 'pending', // 'pending' | 'granted' | 'denied'
/**
* If true, Trackkit will NOT auto-promote from pending to granted on first user action.
* @default true
*/
requireExplicit: true,
/**
* If denied, allow essential events (e.g. identify) to pass.
* @default false
*/
allowEssentialOnDenied: false,
/**
* Version stamp for your privacy policy. When this changes, stored consent is invalidated
* and state resets to `initial`.
*/
policyVersion: '2024-01-15',
/**
* Disable localStorage persistence; consent lives only in-memory (resets on reload).
* @default false
*/
disablePersistence: false,
/**
* Storage key if you need to customize it.
* @default '__trackkit_consent__'
*/
storageKey: '__trackkit_consent__',
}
});Implicit consent (opt-out models): If you set requireExplicit: false and keep initialStatus: 'pending', the first analytics event during a genuine user interaction can auto-promote to granted. (This promotion never happens if you keep requireExplicit: true.)
For environment-based config of consent (e.g.,
VITE_TRACKKIT_CONSENT), see API → Configuration.
Runtime API
import {
getConsent, // -> { status: 'pending'|'granted'|'denied', updatedAt, policyVersion, ... }
grantConsent,
denyConsent,
resetConsent, // resets to 'pending' and clears stored value
onConsentChange // (status, previousStatus) => void; returns unsubscribe fn
} from 'trackkit';getConsent()— inspect current status and counters (queued & dropped).grantConsent()— sets status to granted, flushes any queued events.denyConsent()— sets status to denied, clears queued analytics events; essential behavior depends onallowEssentialOnDenied.resetConsent()— clears stored consent (if any), sets pending.onConsentChange()— subscribe to changes (useful to lazily load other tools after grant).
Typical Patterns
1. Explicit banner (GDPR-style)
const analytics = ({ consent: { initial: 'pending', requireExplicit: true } });
if (getConsent()?.status === 'pending') showBanner();
accept.onclick = () => grantConsent();
reject.onclick = () => denyConsent();2. Implicit grant on first interaction (opt-out)
const analytics = ({
consent: { initial: 'pending', requireExplicit: false },
});
// First analytics event after user interacts can auto-promote to 'granted'.
// (Never auto-promotes if requireExplicit is true.)3. Policy version bumps (force re-consent)
const analytics = ({
consent: { policyVersion: '2024-10-01', initial: 'pending' },
});
// If stored policyVersion differs, consent resets to 'pending'.4. Load third-party tools only after grant
analytics.onConsentChange((status) => {
if (status === 'granted') {
import('./pixels/facebook').then(m => m.init());
import('./hotjar').then(m => m.init());
}
});5. SSR
- On the server, consent is effectively pending; events go into the SSR queue.
- On the client, Trackkit hydrates SSR events and gates them through consent before sending.
- If consent remains pending or denied on the client, SSR events won’t leak.
How Consent Interacts with Other Policies
- Do Not Track (DNT): If
doNotTrackis enabled (default), DNT can block sends even if consent is granted. You can disable DNT enforcement viacreateAnalytics({ doNotTrack: false })if your policy allows. - Domain/URL Filters:
domainsandexcludestill apply after consent is granted. - Localhost: If
trackLocalhostisfalse, nothing sends on localhost even when consent is granted (unless you override).
Diagnostics & Debugging
Enable debug logs:
createAnalytics({ debug: true })to trace consent decisions and queue behavior.At runtime:
tsconsole.log(getConsent()); // status, queued count, dropped countStorage unavailable? If localStorage is disabled/restricted, Trackkit falls back to memory-only behavior if
disablePersistenceis true; otherwise consent may not persist across reloads.
Common symptoms & checks:
- “Events aren’t sending” →
getConsent().statusmust begranted. Pending queues; denied drops. - “My identify still sends after denial” → Set
allowEssentialOnDenied: false. - “Users weren’t re-asked after policy update” → Ensure
policyVersionchanged and persistence is enabled.
Minimal CMP Integration
Hook your CMP’s callbacks straight into your analytics instance:
import { createAnalytics } from 'trackkit';
const analytics = createAnalytics({
provider: 'ga4',
site: 'G-XXXXXXXXXX',
consent: { initialStatus: 'pending', requireExplicit: true },
});
cmp.on('consent:granted', () => analytics.grantConsent());
cmp.on('consent:denied', () => analytics.denyConsent());
cmp.on('consent:reset', () => analytics.resetConsent());Using the singleton helpers instead? Import
grantConsent/denyConsent/resetConsentfrom'trackkit'and call them directly.
If your CMP provides granular categories, gate calls accordingly and keep Trackkit’s consent strict (e.g., requireExplicit: true, initialStatus: 'pending'). Trackkit’s own categories are minimal (essential vs analytics); you can still decide at your UI layer which calls to make.
Testing Consent Behavior
Unit/integration tests: simulate each initial state and verify queue/flush/drop:
initial: 'pending'→ queue, thengrantConsent()→ flushinitial: 'denied'with/withoutallowEssentialOnDenied→ drop/allow essentialpolicyVersionbump → stored consent invalidated → back topending
Use small
await Promise.resolve()(or a shortsetTimeout(0)) to let async flushes complete.
Best Practices
- Default to explicit consent unless your legal basis differs (
requireExplicit: true,initial: 'pending'). - Version your policy and rotate
policyVersionwhen language meaningfully changes. - Don’t rely on auto-promotion if you need an auditable explicit signal.
- Load extras after grant (pixels, replayers) via
onConsentChange. - Keep the UI obvious and accessible; make “reject” as easy as “accept”.
Where to go next
- Configuration reference: environment-driven consent config, defaults, and examples.
- Queue Management: deeper dive into pending/flush semantics and SSR hydration.
- Provider Guides: see how each adapter behaves under consent (e.g., identify being essential).
If anything in this doc conflicts with your legal requirements, defer to your counsel and tune the options accordingly.