Consent for Behavioural Capture
Zenovay ships a small public API, zenovay('consent', ...), that lets your own consent mechanism tell the tracker whether it may capture individual behaviour. It gates session replay and heatmaps, the heatmap screenshot included. On a site that has the requirement switched on, neither of them records until you pass a granted decision. Cross-domain tracking is not switched by this call and follows the site's own settings.
This page explains what the API does, what it does not do, and how to call it. It is technical documentation, not legal advice. What you actually need for your site depends on your jurisdiction, your audience and your legal basis, so check what applies to you.
Two separate questions
Consent conversations about analytics usually collapse two different questions into one. Keep them apart:
- Does the tracker store anything on the visitor's device? This is the ePrivacy Art. 5(3) question (and the equivalent national rules, for example TDDDG ยง25 in Germany). It is answered by the per-site cookieless mode setting. With cookieless mode on, the tracker writes no cookies and nothing to
localStorage, and uses window-scoped in-memory IDs instead. With cookieless mode off, it writes a first-party visitor cookie plus a small number oflocalStoragekeys. See Privacy-first. - Is the processing itself lawful? This is the GDPR Art. 6 question. It is a separate question, and cookieless mode does not answer it. Recording a visitor's screen is individual-level behavioural processing whether or not a cookie was involved.
Turning cookieless mode on therefore does not remove the need to think about consent for the features below.
Which features capture individual behaviour
These three go beyond aggregate page counts and generally call for visitor consent:
| Feature | What it captures |
|---|---|
| Session Replay | A reconstructable recording of the visitor's session: DOM mutations, scrolling, clicks and navigation |
| Heatmaps | Per-visitor click, move and scroll coordinates, aggregated into a heat overlay |
| Cross-domain tracking | A shared visitor identifier passed between your domains, which links a single person's activity across sites you own |
Ordinary pageview and custom-event analytics are not in this table. They still have a legal basis you need to be able to point to, but they do not reconstruct an individual's session.
Scope of the API below. zenovay('consent', ...) gates session replay and heatmaps, including the heatmap screenshot, and publishes the decision for other privacy-sensitive code on the page to read. Both features read the same per-site replay_require_consent setting, so one banner and one call cover them. Cross-domain linking is not switched by this call and follows the site's own settings.
Session replay and heatmaps are not available on the Free plan. They require Pro, Scale or Enterprise, and the restriction is enforced server-side, so this page only applies from Pro upwards.
The consent API
Call the tracker with the consent command and a decision:
// The visitor agreed
zenovay('consent', 'granted');
// The visitor declined, or withdrew a previous agreement
zenovay('consent', 'denied');
true and false are accepted as aliases for 'granted' and 'denied'.
What each value does
'granted' allows both features to send. Session replay uploads: anything already buffered in memory for the current page is flushed, and recording continues normally. Heatmaps are allowed to capture clicks, scroll depth and the page screenshot from the moment the tracker arms them, which is not always the moment you call. See When the decision arrives below.
'denied' stops both immediately. Session replay recording stops, the in-memory buffer is discarded so nothing that was already captured is uploaded, and it does not resume for the rest of the page. Heatmaps record no further click or scroll points, whatever is already buffered in memory is never uploaded, and no screenshot is requested. It applies whether or not the site requires consent, so it is always a safe call to make.
The tracker also publishes the decision on window._zenovayConsent so other privacy-sensitive parts of the page can read the same signal.
Before any signal
Before your first call, the decision is unset, which is not the same as granted:
- On a site where the consent requirement is switched on (the per-site
replay_require_consentsetting), unset means no consent. Replay records into memory but uploads nothing, and heatmaps capture nothing at all: no click points, no scroll depth, no screenshot. Nothing leaves the browser until you call'granted'. - On a site where the requirement is not switched on, replay and heatmaps behave as they always did and a
'granted'call is a harmless no-op. A'denied'call still stops both.
Check the site's settings if you are unsure which applies. Do not assume either way from this page.
When the decision arrives
Session replay reads the decision continuously, so 'granted' or 'denied' takes effect at the moment you call it, wherever in the page load that happens.
Heatmaps are armed once, when the tracker starts and has loaded the site's settings. A later 'denied' still stops heatmap capture immediately, because every click, scroll and upload re-reads the decision as it happens. A 'granted' that first arrives after heatmap capture was armed applies from the next page load rather than the current one.
That is the practical reason to re-assert your stored decision on every page load, as early as the page allows, instead of only calling when the visitor clicks a button.
The decision is not persisted
Zenovay deliberately does not store the decision. It lives in memory for the current page only and is gone on the next navigation or reload. There is no consent cookie and no localStorage entry, so this API adds nothing to the visitor's device.
That means your consent mechanism must re-assert the decision on every page load. It is the system of record, not Zenovay. Read your own stored preference as early as you can on each page and call zenovay('consent', ...) with it.
Stopping everything, not just behavioural capture
zenovay('consent', 'denied') covers session replay and heatmaps. If a visitor opts out of analytics altogether, use the broader switch instead:
zenovay('disable'); // stop sending events for this visitor
zenovay('enable'); // re-enable it
zenovay('disable') stops the tracker from sending events for this visitor. It does not unbind heatmap click and scroll capture, which follows the site's heatmaps setting rather than this call.
Global Privacy Control is different, and Zenovay always honours it. When the browser reports GPC, the tracking script stops before any of these features start, so no events are sent and neither session replay nor heatmap capture is initialised. You do not have to call anything for that to happen.
Integration example
The snippet below is an integration example: it shows where the calls go. It is not a compliant consent banner, it is not a drop-in UI, and it is not legal advice. Your banner's wording, granularity, record-keeping and withdrawal handling are yours to design.
// Your own storage. Zenovay does not persist the decision.
function readStoredConsent() {
try {
return localStorage.getItem('my-site-consent'); // 'granted' | 'denied' | null
} catch (e) {
return null;
}
}
// 1. Re-assert on EVERY page load, as early as possible.
var stored = readStoredConsent();
if (stored === 'granted' || stored === 'denied') {
window.zenovay && window.zenovay('consent', stored);
}
// 2. Push the decision when your banner resolves.
function onConsentDecision(decision) { // 'granted' | 'denied'
try { localStorage.setItem('my-site-consent', decision); } catch (e) {}
window.zenovay && window.zenovay('consent', decision);
// Optional: also feed the Consent tab so you can measure your own banner.
window.zenovay && window.zenovay('track', 'consent', {
action: decision === 'granted' ? 'accept' : 'reject'
});
}
Notes on the example:
- The
window.zenovay &&guard makes every call a no-op if the tracker has not loaded yet. If you inject the tracker asynchronously, re-check for it before firing rather than assuming it is ready. - Step 1 matters more than step 2. Skipping the re-assert is the common mistake: the banner works on the page the visitor clicked it on, and every subsequent page starts from unset again.
- The second call is optional and unrelated to the gate. It feeds Consent & Privacy Metrics, which reports on how your banner performs.
With a consent management platform
If you use a CMP, fire from its "consent ready" and "consent changed" callbacks rather than from button handlers, so a later preference change is picked up too:
// Cookiebot, as an example. Adapt to your CMP's callback.
window.addEventListener('CookiebotOnConsentReady', function () {
var c = window.Cookiebot && window.Cookiebot.consent;
var decision = (c && c.statistics) ? 'granted' : 'denied';
window.zenovay && window.zenovay('consent', decision);
});
OneTrust, Osano, Termly and similar platforms expose an equivalent callback. Which of their categories should map to 'granted' is a decision for you and your legal review, not something Zenovay can infer.
Build the banner with an AI coding agent
If you would rather not hand-write the wiring, hand the prompt below to a coding agent that already has your repository open (Claude Code, Codex, Cursor and similar). It describes the integration contract precisely, tells the agent to inherit your existing design tokens instead of inventing new ones, and tells it to honour Global Privacy Control.
Copy it as it is. The only thing you may want to change is the storage lifetime in step 1.
You are adding a visitor consent banner to this codebase. It must drive the Zenovay
analytics tracker through the tracker's public consent API.
FACTS ABOUT THE TRACKER YOU MUST NOT CHANGE
- The tracker exposes one global function: window.zenovay(command, ...args).
- The only consent call is zenovay('consent', 'granted') or zenovay('consent', 'denied').
'granted' allows session replay to upload and flushes what is buffered in memory.
'denied' stops replay recording, discards the in-memory buffer so nothing already
captured is uploaded, and does not resume for the rest of the page load.
- The tracker does NOT persist the decision. It lives in memory for the current page
only. Your code is the system of record and must re-assert it on every page load.
- The tracker also publishes the decision on window._zenovayConsent. Read it, never
write it.
- zenovay('disable') stops the tracker from sending events for this visitor and
zenovay('enable') reverses that. Those are a full analytics opt out, not the
behavioural consent gate. Do not use them in place of the consent call.
- Do not edit the Zenovay tracking snippet or its attributes.
WHAT TO BUILD
1. A store for the decision ('granted' | 'denied' | unset) that uses whatever this
project already uses for client-side preferences. If you add a cookie or a storage
key, keep it first party, store only the decision plus a timestamp, and cap its
lifetime at 365 days.
2. A bootstrap that runs on EVERY page load, including client-side route changes in
this project's router, and calls
window.zenovay && window.zenovay('consent', storedDecision)
whenever a decision is stored. Run it as early as this framework allows. Guard every
call with window.zenovay && so it is a no-op before the tracker has loaded.
3. A banner component that offers accept and decline with equal visual prominence, plus
a way for the visitor to change the decision later (a footer link or a settings
entry) that calls the API again with the new value.
4. Global Privacy Control. On mount, read navigator.globalPrivacyControl. If it is
exactly true, treat the visitor as having declined, do not render the banner, and
call window.zenovay && window.zenovay('consent', 'denied'). Never show a GPC visitor
a prompt to opt back in.
5. Leave navigator.doNotTrack alone. Do not treat it as a decision.
DESIGN CONSTRAINTS
- Read this project's design tokens, theme variables, spacing scale, typography and
component primitives FIRST, then build with them. Do not introduce a new colour, a
new font, a raw hex value, or a new UI or CSS dependency.
- Match the project's existing dark mode mechanism.
- Keyboard reachable, visible focus, labelled for screen readers. Do not trap focus and
do not cover the page with an overlay the visitor cannot dismiss.
- Respect prefers-reduced-motion.
COPY CONSTRAINTS
- Write neutral placeholder wording and mark it clearly as placeholder for the site
owner to replace.
- Do not write legal claims, do not name a legal basis, do not describe either option
as recommended, and do not state what any law requires. That is the site owner's
call, not yours.
DELIVERABLES
- The component, the store and the bootstrap, wired into this project's root layout.
- A short note listing every file you touched and every storage key or cookie you added.
VERIFY BEFORE YOU FINISH, AND REPORT WHAT YOU OBSERVED
- With a stored 'denied', reload and confirm no session replay upload request is sent.
- With a stored 'granted', reload without touching the banner and confirm the re-assert
call fires on load, not only after a click.
- With navigator.globalPrivacyControl forced to true, confirm no banner renders and the
'denied' call is made.
Read the result before you accept it. The agent can see your codebase, but it cannot see your legal position, your audience or the CMP you may already run, so the prompt deliberately leaves the wording of your banner to you.
Verifying it works
- Open DevTools with the tracker's debug mode on and watch for the
Consent granted/Consent deniedlog lines. - Load a page, decline in your banner, and confirm no replay upload request is made.
- Reload the page without touching the banner again and confirm your re-assert path fires. This is the step that catches a missing step 1.
Related
- Privacy-first: how cookieless mode, GPC and Do Not Track are handled
- Consent & Privacy Metrics: reporting on your own banner's accept / reject / dismiss rates
- Session Replay: what replay records and how masking works
- Custom Events: the
zenovay('track', ...)convention used in the example
On this page