How to check if consent mode is enabled

Load the page with the network log open and read the requests, not the configuration. A default consent state has to be pushed before the first tag call. On the fixture below, the page that pushed it late still sent a hit with analytics_storage=unset and set a cookie, while window.dataLayer looked correct afterwards.

Why check this

Run this on staging before every release that touches tags, the consent banner or the tag manager container, and after anyone edits the container outside the release. The question is narrow: what do the tags do in the window between the first byte and the visitor's decision.

The failure it catches is a default that arrives too late. The snippet is present, the tag manager reports consent mode as configured, and the measurement hit has already gone out with a cookie attached. A reader of window.dataLayer after load sees the consent entry and calls the check passed. The request that left before it is the evidence that matters.

Prerequisites

import { createServer } from 'node:http';
const BOOT = '<script>window.dataLayer=[];function gtag(){dataLayer.push(arguments);}</script>';
const DEF = "<script>gtag('consent','default',{ad_storage:'denied',analytics_storage:'denied',"
  + "ad_user_data:'denied',ad_personalization:'denied'});</script>";
const TAG = '<script src="/tag.js"></script>';
const PAGES = { '/no-default': BOOT + TAG, '/with-default': BOOT + DEF + TAG, '/late-default': BOOT + TAG + DEF };
const TAGJS = "var c=(window.dataLayer||[]).filter(function(e){return e[0]==='consent'&&e[1]==='default';}).pop();"
  + "gtag('config','FIXTURE-1');new Image().src='/collect?analytics_storage='+(c?c[2].analytics_storage:'unset');";
const t0 = Date.now();
createServer((req, res) => {
  const [path, qs] = req.url.split('?');
  const granted = !/analytics_storage=denied/.test(qs || '');
  if (path === '/collect' && granted) res.setHeader('set-cookie', '_fx=8f2c; Path=/');
  console.log(`+${String(Date.now() - t0).padStart(4)}ms ${path}${qs ? '?' + qs : ''}`
    + (path === '/collect' ? (granted ? ' -> Set-Cookie: _fx=8f2c' : ' -> no cookie') : ''));
  if (path === '/tag.js') { res.setHeader('content-type', 'text/javascript'); return res.end(TAGJS); }
  if (path === '/collect') { res.setHeader('content-type', 'image/gif'); return res.end(); }
  res.setHeader('content-type', 'text/html; charset=utf-8');
  res.end(`<!doctype html><title>${path}</title>${PAGES[path] || '<p>unknown</p>'}`);
}).listen(9644, '127.0.0.1', () => console.log('consent fixture on http://127.0.0.1:9644/'));
import { launch } from 'puppeteer-core';
const path = process.argv[2];
const browser = await launch({
  executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe',
  headless: true,
});
const page = await browser.newPage();
const seen = [];
page.on('request', (r) => seen.push(new URL(r.url()).pathname + new URL(r.url()).search));
await page.goto(`http://127.0.0.1:9644${path}`, { waitUntil: 'networkidle2' });
const dl = await page.evaluate(() =>
  (window.dataLayer || []).map((e, i) => `${i}: ${JSON.stringify(Array.from(e))}`));
console.log(`${path}   ${await browser.version()}`);
console.log(`dataLayer\n${dl.join('\n') || '(empty)'}`);
console.log(`requests  ${seen.join(' ')}`);
console.log(`cookies   ${(await page.cookies()).map((c) => c.name + '=' + c.value).join(' ') || '(none)'}`);
await browser.close();

Steps

  1. Step 1.

    Read the order the markup declares, which is the cheapest check and the one that lies most often.

    for p in no-default with-default late-default; do echo "/$p"; curl -s http://127.0.0.1:9644/$p | grep -o -E "consent','default'|src=\"/tag.js\""; done
    
    /no-default
    src="/tag.js"
    /with-default
    consent','default'
    src="/tag.js"
    /late-default
    src="/tag.js"
    consent','default'

    Two of the three have a default. Only one declares it above the tag. Markup order answers nothing when the consent call is injected by a tag manager, which is why the next steps read the wire.

  2. Step 2.

    Load the page with no default at all.

    MSYS_NO_PATHCONV=1 node consentprobe.mjs /no-default
    
    /no-default   Chrome/152.0.7977.76
    dataLayer
    0: ["config","FIXTURE-1"]
    requests  /no-default /tag.js /collect?analytics_storage=unset /favicon.ico
    cookies   _fx=8f2c

    No consent entry in the data layer, a hit on the wire, and a cookie. This is the baseline shape of a page with consent mode absent.

  3. Step 3.

    Load the page that sets the default first.

    MSYS_NO_PATHCONV=1 node consentprobe.mjs /with-default
    
    /with-default   Chrome/152.0.7977.76
    dataLayer
    0: ["consent","default",{"ad_storage":"denied","analytics_storage":"denied","ad_user_data":"denied","ad_personalization":"denied"}]
    1: ["config","FIXTURE-1"]
    requests  /with-default /tag.js /collect?analytics_storage=denied /favicon.ico
    cookies   (none)

    The request still goes out. What changed is its content and the absence of the cookie. Google's guide states the same for its own tags: with ad_storage denied, "new cookies won't be set for advertising purposes" while "Data sent to Google will still include the full page URL".

  4. Step 4.

    Load the page that sets the same default one line too late.

    MSYS_NO_PATHCONV=1 node consentprobe.mjs /late-default
    
    /late-default   Chrome/152.0.7977.76
    dataLayer
    0: ["config","FIXTURE-1"]
    1: ["consent","default",{"ad_storage":"denied","analytics_storage":"denied","ad_user_data":"denied","ad_personalization":"denied"}]
    requests  /late-default /tag.js /collect?analytics_storage=unset /favicon.ico
    cookies   _fx=8f2c

    The consent default is in the data layer, so a check that searches the array for a consent entry passes. The hit left with unset and the cookie was written. Only the request line separates this page from step 3.

  5. Step 5.

    Read the fixture's terminal for the order and the timing on the server side.

    consent fixture on http://127.0.0.1:9644/
    +2852ms /no-default
    +2870ms /tag.js
    +2876ms /collect?analytics_storage=unset -> Set-Cookie: _fx=8f2c
    +2881ms /favicon.ico
    +5640ms /with-default
    +5658ms /tag.js
    +5666ms /collect?analytics_storage=denied -> no cookie
    +5672ms /favicon.ico
    +7914ms /late-default
    +7930ms /tag.js
    +7937ms /collect?analytics_storage=unset -> Set-Cookie: _fx=8f2c
    +7942ms /favicon.ico

    Every hit landed within 30 ms of the document, long before a banner could be answered. On localhost there is no network in those numbers, so read them as ordering, not as latency.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | No consent entry in the data layer | Consent mode is not configured on this page | Add the default before the tag loads, then rerun steps 2 to 4. | | Consent entry present, hit carries a denied state | The default was applied in time | Check the update call after the banner is answered as well. | | Consent entry present, hit carries no state | The default arrived after the tag fired | Move it above the tag, or into the container's initialisation. | | Cookie written before any decision | Storage happened without consent | Open the response that set it and trace the tag that asked for it. |

Common mistakes

Sign: window.dataLayer contains a consent default, so the check is recorded as passed.Cause: Step 4 shows a page whose data layer holds the identical consent entry as step 3, while the hit on the wire carried no consent state and a cookie was set. Reading the array after load cannot show what had already gone out. Read the requests.
Sign: A denied default is expected to stop the requests, and requests are still there.Cause: Denied consent changes what the tag sends, not whether it sends. The fixture in step 3 sent /collect with the state attached and set no cookie. Assert on the parameters and the cookies, not on request count.
Sign: The check passes in the markup and fails in production.Cause: When the consent call is injected by a tag manager, its position in the HTML says nothing about its position in time. Step 1 reads the markup, steps 2 to 5 read the browser, and only the second pair of eyes is authoritative.
Sign: The fixture behaves and the real site does not.Cause: This fixture is a local stub written to make the ordering visible. A real tag carries its consent state in its own request parameters, so find the equivalent field in your tag's request and diff it between a denied default and a granted one before you trust any assertion.

What to check next

FAQ

How do I check google consent mode v2?

The four consent types are ad_storage, ad_user_data, ad_personalization and analytics_storage. Confirm all four appear in the default call, then measure a page load and read the consent state carried by the tag's own request.

Is there a consent mode checker?

Tag Assistant and the browser network panel both show the calls. The steps above produce the same answer as text output you can diff between builds and attach to a ticket, which a screenshot of a debug panel does not.

Where should the default consent call go?

Before any command that sends measurement data. Step 4 shows a default one line below the tag, which left the data layer looking correct and the hit already sent.

Does consent mode block requests?

No. In step 3 the denied default changed the request and stopped the cookie. Treat "no requests" as an expectation to verify against your own tag rather than as the defined behaviour.

Verified

Verified by Maks Vernycurl 8.21.0Node 22.23.2Chrome 152.0.7977.76

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate12 minpublished updated Maks Verny