Learn how to implement cookie consent and integrate it with Zenovay analytics for privacy compliance.
Do You Need a Cookie Banner?
Zenovay Cookie Usage
| Mode | Client-side storage | Consent needed |
|---|---|---|
| Cookieless mode (pre-selected for new websites) | None. The server derives an anonymous ID that changes every day | Nothing is stored on the device; check your own obligations |
| Cookie mode | Two first-party cookies (zenovay_visitor_id, zenovay_session_id) plus a few localStorage keys | Usually, because an identifier is stored on the device |
Cookieless until consent (data-cookieless="until-consent") | None until the visitor accepts, then the same as cookie mode | Built for a banner: cookies are set only after consent |
The raw IP address is not stored in your analytics. The table above is only about what the tracking script writes to the visitor's browser. Cookieless mode prevents the script from writing any cookie or localStorage entry at all.
When Consent is Required
Consent typically required when:
- Using persistent cookies
- Tracking across sessions
- Identifying returning visitors
- Collecting personal data
Consent may not be required when:
- Using cookieless mode (no cookies)
- Strictly necessary functionality
- Anonymous aggregate statistics
Cookieless Mode (No Cookies)
You can run the Zenovay tracker in cookieless mode, which writes nothing to the visitor's browser. There are two ways to enable it.
Option 1: script attribute. Add data-cookieless="true" to your script tag:
<script
defer
data-tracking-code="YOUR_TRACKING_CODE"
data-cookieless="true"
src="https://api.zenovay.com/z.js"
></script>
Option 2: website setting. Open your website's dashboard, go to its Settings, and turn on the cookieless toggle (it appears in both the General and Advanced tabs). The General tab also regenerates the snippet above with data-cookieless="true" already added.
An explicit data-cookieless on the script tag always wins over the website setting. If your installed script forces cookieless mode, the setting is switched on automatically, so the dashboard shows the mode that is actually active.
Cookieless mode:
- Uses no cookies and no localStorage; the server derives an anonymous ID that changes every day
- Creates no persistent identifiers
- Stores nothing on the device, which is what ePrivacy cookie rules are about (check your own obligations)
- Keeps the pages of one visit together, but does not recognise returning visitors across days
Other tools you load (Meta Pixel, Google Analytics, and similar) still set their own cookies and may require consent regardless of this setting.
Cookieless Until Consent
With data-cookieless="until-consent", you don't have to delay loading the script. The tracker starts cookieless and switches to its first-party cookie only after the visitor accepts:
<script
defer
data-tracking-code="YOUR_TRACKING_CODE"
data-cookieless="until-consent"
src="https://api.zenovay.com/z.js"
></script>
Then tell the tracker what the visitor chose:
function acceptCookies() {
localStorage.setItem('analytics_consent', 'granted');
// Switches to the first-party cookie and keeps the ongoing visit
window.zenovay && window.zenovay('consent', 'granted');
}
function declineCookies() {
localStorage.setItem('analytics_consent', 'denied');
// Removes Zenovay's cookies and storage and continues cookieless
window.zenovay && window.zenovay('consent', 'denied');
}
'granted'switches to the cookie and keeps the IDs of the ongoing visit, so the visit is not split in two.'denied', also when consent is withdrawn later, removes Zenovay's cookies and storage and continues cookieless.- On later pages, the tracker starts in cookie mode if the visitor consented before.
- The same call also gates session replay and heatmaps. For those, call it with the stored decision on every page load.
Conditional Loading
Basic Consent Check
// Check consent before loading
function loadZenovayIfConsented() {
const consent = localStorage.getItem('analytics_consent');
if (consent === 'granted') {
loadZenovay();
} else if (consent === 'denied') {
// Don't load analytics
} else {
showConsentBanner();
}
}
function loadZenovay() {
const script = document.createElement('script');
script.src = 'https://api.zenovay.com/z.js';
script.setAttribute('data-tracking-code', 'YOUR_TRACKING_CODE');
script.defer = true;
document.head.appendChild(script);
}
// Run on page load
loadZenovayIfConsented();
Simple Consent Banner
<div id="cookie-banner" style="display: none;">
<div class="cookie-content">
<p>We use analytics to improve your experience.</p>
<button onclick="acceptCookies()">Accept</button>
<button onclick="declineCookies()">Decline</button>
<a href="/privacy">Learn more</a>
</div>
</div>
<script>
function showConsentBanner() {
document.getElementById('cookie-banner').style.display = 'block';
}
function acceptCookies() {
localStorage.setItem('analytics_consent', 'granted');
document.getElementById('cookie-banner').style.display = 'none';
loadZenovay();
}
function declineCookies() {
localStorage.setItem('analytics_consent', 'denied');
document.getElementById('cookie-banner').style.display = 'none';
// Optionally load in cookieless mode (no browser storage)
loadZenovayCookieless();
}
function loadZenovayCookieless() {
// data-cookieless="true": no cookies, no localStorage.
// The server derives an anonymous ID that changes every day.
const script = document.createElement('script');
script.src = 'https://api.zenovay.com/z.js';
script.setAttribute('data-tracking-code', 'YOUR_TRACKING_CODE');
script.setAttribute('data-cookieless', 'true');
script.defer = true;
document.head.appendChild(script);
}
</script>
Popular Consent Platforms
Cookiebot Integration
// Wait for Cookiebot consent
window.addEventListener('CookiebotOnAccept', function() {
if (Cookiebot.consent.statistics) {
loadZenovay();
}
});
// Handle consent changes
window.addEventListener('CookiebotOnDecline', function() {
// Consent withdrawn - stop tracking
if (window.zenovay) {
window.zenovay('disable');
}
});
Cookiebot Configuration (only needed in cookie mode or with data-cookieless="until-consent"; in cookieless mode Zenovay sets no cookies):
- Add Zenovay to the "Statistics" category
- Declare two cookies:
zenovay_visitor_idandzenovay_session_id - Set provider:
zenovay.com - Set type:
HTTP - Set expiry:
zenovay_visitor_id30 days by default (or the Visitor Cookie Lifetime you chose, up to 1 year);zenovay_session_iduntil the visit ends
OneTrust Integration
// OneTrust consent callback
function OptanonWrapper() {
if (OnetrustActiveGroups.includes('C0002')) {
// Performance cookies consented
loadZenovay();
} else {
// Load in cookieless mode
loadZenovayCookieless();
}
}
OneTrust Categories:
- C0001: Strictly Necessary
- C0002: Performance (Zenovay)
- C0003: Functional
- C0004: Targeting
Osano Integration
// Osano consent callback
window.Osano.cm.addEventListener('osano-cm-consent-changed', function(event) {
if (event.ANALYTICS === 'ACCEPT') {
loadZenovay();
}
});
Termly Integration
// Termly consent callback
window.addEventListener('termly-consent-preferences-updated', function(e) {
if (e.detail.analytics) {
loadZenovay();
}
});
Google Consent Mode
Integration
// Initialize Google Consent Mode
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'analytics_storage': 'denied'
});
// Update on consent
function updateConsent(granted) {
gtag('consent', 'update', {
'analytics_storage': granted ? 'granted' : 'denied'
});
if (granted) {
loadZenovay();
}
}
With Zenovay
Use the conditional loading approach shown above to only load Zenovay after consent is granted through Google Consent Mode:
// Load Zenovay only after analytics consent is granted
function updateConsent(granted) {
gtag('consent', 'update', {
'analytics_storage': granted ? 'granted' : 'denied'
});
if (granted) {
const script = document.createElement('script');
script.src = 'https://api.zenovay.com/z.js';
script.setAttribute('data-tracking-code', 'YOUR_TRACKING_CODE');
script.defer = true;
document.head.appendChild(script);
}
}
React Implementation
Consent Context
// ConsentContext.js
import { createContext, useContext, useState, useEffect } from 'react';
const ConsentContext = createContext();
export function ConsentProvider({ children }) {
const [consent, setConsent] = useState(() => {
return localStorage.getItem('analytics_consent') || 'pending';
});
const grantConsent = () => {
localStorage.setItem('analytics_consent', 'granted');
setConsent('granted');
};
const denyConsent = () => {
localStorage.setItem('analytics_consent', 'denied');
setConsent('denied');
};
return (
<ConsentContext.Provider value={{ consent, grantConsent, denyConsent }}>
{children}
</ConsentContext.Provider>
);
}
export const useConsent = () => useContext(ConsentContext);
Consent Banner Component
// CookieBanner.js
import { useConsent } from './ConsentContext';
export function CookieBanner() {
const { consent, grantConsent, denyConsent } = useConsent();
if (consent !== 'pending') return null;
return (
<div className="cookie-banner">
<p>We use cookies to analyze website traffic.</p>
<button onClick={grantConsent}>Accept</button>
<button onClick={denyConsent}>Decline</button>
</div>
);
}
Conditional Analytics
// Analytics.js
import { useEffect } from 'react';
import { useConsent } from './ConsentContext';
export function Analytics({ websiteId }) {
const { consent } = useConsent();
useEffect(() => {
if (consent === 'granted') {
const script = document.createElement('script');
script.src = 'https://api.zenovay.com/z.js';
script.setAttribute('data-tracking-code', websiteId);
script.defer = true;
document.head.appendChild(script);
} else if (consent === 'denied') {
// Don't load analytics when consent denied.
// If you still want minimal tracking, load the script with
// data-cookieless="true" instead (no cookies, no localStorage).
}
}, [consent, websiteId]);
return null;
}
Vue.js Implementation
<!-- CookieConsent.vue -->
<template>
<div v-if="showBanner" class="cookie-banner">
<p>We use cookies for analytics.</p>
<button @click="accept">Accept</button>
<button @click="decline">Decline</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const showBanner = ref(false);
onMounted(() => {
const consent = localStorage.getItem('analytics_consent');
if (!consent) {
showBanner.value = true;
} else if (consent === 'granted') {
loadZenovay();
}
});
function accept() {
localStorage.setItem('analytics_consent', 'granted');
showBanner.value = false;
loadZenovay();
}
function decline() {
localStorage.setItem('analytics_consent', 'denied');
showBanner.value = false;
loadZenovayCookieless();
}
function loadZenovay() {
const script = document.createElement('script');
script.src = 'https://api.zenovay.com/z.js';
script.setAttribute('data-tracking-code', 'YOUR_TRACKING_CODE');
document.head.appendChild(script);
}
function loadZenovayCookieless() {
// data-cookieless="true": no cookies, no localStorage.
// The server derives an anonymous ID that changes every day.
const script = document.createElement('script');
script.src = 'https://api.zenovay.com/z.js';
script.setAttribute('data-tracking-code', 'YOUR_TRACKING_CODE');
script.setAttribute('data-cookieless', 'true');
document.head.appendChild(script);
}
</script>
Consent Preferences Page
Allow users to change preferences:
<div class="cookie-preferences">
<h2>Cookie Preferences</h2>
<div class="cookie-option">
<label>
<input type="checkbox" id="necessary" checked disabled>
Necessary Cookies (Required)
</label>
<p>Essential for the website to function.</p>
</div>
<div class="cookie-option">
<label>
<input type="checkbox" id="analytics">
Analytics Cookies
</label>
<p>Help us understand how visitors use our site.</p>
</div>
<button onclick="savePreferences()">Save Preferences</button>
</div>
<script>
// Load current preferences
document.getElementById('analytics').checked =
localStorage.getItem('analytics_consent') === 'granted';
function savePreferences() {
const analytics = document.getElementById('analytics').checked;
localStorage.setItem('analytics_consent', analytics ? 'granted' : 'denied');
// Reload to apply changes
location.reload();
}
</script>
Consent Logging
Track consent for compliance:
function logConsent(type, granted) {
fetch('/api/consent-log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: type,
granted: granted,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
})
});
}
function acceptCookies() {
logConsent('analytics', true);
localStorage.setItem('analytics_consent', 'granted');
loadZenovay();
}
Best Practices
Banner Design
- Clear language: Explain what cookies do
- Equal options: Accept and decline equally prominent
- No dark patterns: Don't trick users into accepting
- Granular control: Let users choose categories
- Easy withdrawal: Allow changing preferences
Technical
- Don't load before consent: Only load tracking after consent
- Respect withdrawal: Stop tracking when consent withdrawn
- Remember preferences: Don't ask repeatedly
- Test thoroughly: Verify consent flow works
Compliance
- Document consent: Log when consent given/withdrawn
- Regular review: Update as regulations change
- Audit trail: Keep records for compliance
Troubleshooting
Tracking Still Active After Decline
Check:
- Script not hardcoded in HTML
- Consent check runs before any tracking
- No caching issues
Banner Shows Repeatedly
Verify:
- localStorage not blocked
- Cookie not expiring immediately
- Same domain for all pages
Consent Platform Not Triggering
Ensure:
- Integration code correct
- Platform fully loaded before check
- Callback names match