IAnswerable

Implement Cookie Consent Without Breaking Analytics

30 Apr 2026

Most cookie consent implementations break analytics, then get ripped out six months later because nobody trusts the numbers anymore. The fix is gating scripts properly instead of firing everything and hoping the banner covers you legally.

The wrong way, which is also the most common way

Dropping a consent banner on top of a page that already loaded Google Analytics in the <head> does nothing. The tracking script ran before the user clicked anything. The banner is decoration. If you’re doing this today, you don’t have consent — you have a cookie notice, which is not the same thing.

Nothing that sets a non-essential cookie should load until consent is granted. That means your analytics, ad pixels, and most embeds need to sit behind a gate, not just your banner UI.

The sequence below is what the three steps that follow actually build:

sequenceDiagram
    participant Browser
    participant Page
    participant Analytics

    Page->>Analytics: consent default (denied)
    Browser->>Page: load, no cookies set
    Browser->>Page: click accept or reject
    alt accepted
        Page->>Analytics: consent update (granted)
        Analytics-->>Browser: sets cookies, full tracking
    else rejected
        Analytics-->>Browser: modeled data only, no cookies
    end
  

1. Default everything to denied

With Google’s Consent Mode (works whether or not you use GA):

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('consent', 'default', {
    'analytics_storage': 'denied',
    'ad_storage': 'denied',
  });
</script>

This has to run before any tag that might set a cookie, including the GTM container snippet itself.

2. Load the tag manager, but let it stay dormant

GTM or your analytics snippet can load — it just won’t fire anything that touches storage until consent updates:

<script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
function onConsentAccept() {
  gtag('consent', 'update', {
    'analytics_storage': 'granted',
    'ad_storage': 'granted',
  })
  localStorage.setItem('consent', JSON.stringify({ analytics: true, ts: Date.now() }))
}

function onConsentReject() {
  localStorage.setItem('consent', JSON.stringify({ analytics: false, ts: Date.now() }))
}

Store the choice and re-check it on every page load so returning visitors who already answered don’t see the banner again — and so you’re not re-firing scripts that were already correctly blocked.

Don’t lose all your analytics to rejection

Consent Mode’s real advantage over a hard on/off toggle: when a user rejects, GA4 still gets modeled conversion estimates from aggregate signals instead of nothing. You lose per-user tracking, not the entire dataset. This is the actual reason to use the vendor’s consent framework instead of hand-rolling a script blocker — you keep partial signal instead of a hole in your funnel every time someone clicks reject.

What doesn’t need gating

Cookies strictly necessary for the site to function — session tokens, CSRF tokens, a cart — don’t need consent under most frameworks, GDPR included. Don’t gate your own login session behind a marketing consent banner; that’s a UX bug, not compliance.

Cookie type Needs consent?
Session/auth No
Load balancer (sticky session) No
Analytics (GA, Plausible with cookies) Yes
Ad pixels (Meta, Google Ads) Yes
A/B test tool Usually yes

Test it, don’t assume it

Open dev tools, Application tab, clear all cookies, reload without clicking anything. If _ga or an ad pixel cookie shows up before you’ve interacted with the banner, the gate isn’t wired correctly, and the banner you shipped is legally worth nothing.

The concrete next action

Check whether your GTM container fires tags on page load or waits for a trigger tied to your consent state. If every tag fires unconditionally on “All Pages,” that’s the bug — fix the trigger condition before anything else on this list.