Feature · consent capture

One script tag, and nothing fires before consent

The banner is the part everybody looks at. The part that decides whether you are actually compliant is the twelve lines above it in the <head>, and what those lines do to every other tag on the page. This is that half of the job: placement, load order, the three blocking mechanisms, and the one of them that is genuinely airtight.

Dinkar Singh

Part of

Consent Management

The widget is the capture surface. The module is the record behind it — purposes, artefacts, withdrawal and the audit trail a regulator would ask for.

Where the tag goes, and why "first in head" is not a style preference

A script that has already executed cannot be un-executed. If Google Analytics loads in line 4 of your head and the consent widget loads in line 40, the pageview has been sent, the cookie has been written, and no amount of banner UI undoes it. This is the single most common reason a site with a perfectly good-looking banner fails a scan.

So the embed instruction is positional, not decorative: the tag goes first in the head, above every analytics, advertising and marketing tag on the page. The widget finds its own script element through document.currentScript, and falls back to scanning for a script whose source ends in widget.js and which carries the publishable key — which means async and defer both work, and which means a caching plugin that rewrites src to data-src does not break the embed either.

  • WordPress — into header.php, or a code snippet plugin set to run at priority 1. Not into a footer widget.
  • Google Tag Manager — into the page head above the container snippet, not as a tag inside GTM. A consent layer that loads inside the thing it is supposed to gate is loading too late by definition.
  • Next.js — next/script with strategy="beforeInteractive", then initialise analytics inside the consent event listener rather than on mount.
  • Shopify — theme.liquid, above the analytics block the theme ships with.

The two attributes the tag actually reads

There are two, and only two. A publishable key, which identifies the tenant and the configuration, and an optional mode that picks the surface — banner, popup, mini, floater or QR. Mode is read at runtime, so the same key can render a full banner on the marketing site and a discreet floater inside the logged-in app.

Everything else that a competitor would make you set as an attribute — position, offsets, pill label, modal title and body, which cookie categories are visible, which purposes are in scope, which languages are offered, whether an email is required — is configuration held against the key and fetched at load. That is a deliberate trade. Attributes mean a deploy on every property to change a word of banner copy; server-held configuration means one change in the dashboard and forty sites pick it up on their next page view.

The key is a publishable one. It identifies a tenant, it is meant to sit in the page source, and it grants nothing beyond rendering the banner and posting a consent decision. One caveat worth knowing rather than discovering: origin allow-listing on a key defaults open — an empty allowed-origins list accepts any origin, which matches how the large CMPs behave but is not what most people assume. Set the list.

Three ways a tracker gets blocked, and only one of them is airtight

Blocking is not one mechanism. It is three, layered, and they have genuinely different reliability. Anyone who tells you their banner "blocks everything" is describing the first layer and hoping you do not ask about the other two.

  • Declarative, and this is the airtight one. You author the tag as type="text/plain" with a category attribute — analytics, marketing or functional. A script with type text/plain is inert; the browser will not run it. On consent the widget clones the node and replaces it, because flipping the type attribute in place does not make a browser re-execute a script it has already parsed. Strictly necessary is never blocked.
  • Observational, and this is best-effort. A MutationObserver watches the document element for added subtrees and neutralises injected scripts whose host matches the tenant's cookie registry, marking them blocked. It catches tags injected by other tags. It cannot catch a tag that beat the observer onto the page.
  • Remedial, and this is the backstop. After every load and every decision, cookies belonging to non-consented categories are deleted — across the bare path, the host, the dot-host and the registrable domain, because that is where the same cookie name gets written from. Prefix patterns are supported, so the four cookies a single analytics vendor writes are one rule. It never purges before a decision exists and never touches the widget's own cookies.

The vendor registry the observer matches against

The registry ships seeded with the vendors that actually appear on Indian sites, classified rather than merely listed: the major analytics suites and product-analytics tools under analytics; the advertising pixels — Meta, LinkedIn, Google Ads, TikTok, X — under marketing; live chat and marketing automation under functional. Four categories exist in the registry and a fifth, uncategorised, exists in the banner so that a cookie nobody has classified yet is visible rather than silently treated as necessary.

You can extend it. The point of it being tenant data rather than a hard-coded list is that the vendor your growth team added last week can be classified without waiting for a platform release.

Consent Mode v2 defaults, set before your container loads

For anyone running Google tooling, the widget writes the Consent Mode v2 default state before any measurement tag has a chance to read it: ad storage, analytics storage, ad user data, ad personalisation and functionality storage all denied, security storage granted, with a short wait-for-update window so the tags pause rather than assume. Every subsequent decision — accept, reject, or a per-category change in the preference centre — pushes a consent update.

This matters on the Indian obligation as much as the Google one. Consent under §6 has to be a clear affirmative action for a specified purpose. A default state of "granted until told otherwise" is not that, regardless of what the banner says.

The JavaScript surface, for the things a banner cannot do alone

The widget exposes a small global and one event. The global opens the preference centre, reads the current decision, and clears it locally. The event fires on every persisted decision and carries the state, which is how you initialise a tag that has to be created in JavaScript rather than declared in HTML — subscribe to the event, initialise there, never on DOM ready.

Global Privacy Control and Do Not Track are read on load. Where the browser signals either, the widget treats it as a rejection rather than showing a banner that asks a question the visitor has already answered at the browser level.

What the browser ends up holding

The decision itself lives in both a first-party cookie and local storage, carrying the choice, the per-category state, the timestamp, a short human-readable receipt key and an expiry. The expiry is a year from the decision, and an expired decision counts as no decision — the banner re-prompts rather than coasting on a consent given fourteen months ago, which is the behaviour §6 implies and most banners do not implement.

Two identifiers sit alongside it, both first-party, both six months: one that ties repeat visits from the same browser to the same consent record so a returning visitor is not asked again, and one anonymous handle used when a visitor interacts with the preference centre without identifying themselves. A session flag holds the age-gate answer for the session only.

Single-page apps, and an honest note about them

There is no route-change hook. No history listener, no hash listener. That is worth saying plainly because plenty of CMP documentation implies otherwise and then quietly relies on the same thing this one does: the banner is rendered once into the document rather than into a route, the mutation observer keeps working across client-side navigations because it is watching the document element, and the persistent pill stays mounted after a decision so the preference centre is always one click away — which is the §6(4) requirement anyway.

For a React or Vue application the practical rule is the one above: initialise measurement inside the consent event listener, not in a component lifecycle method. A component that mounts on route change and initialises analytics will do it again on every route change, consent or not.

What the script tag cannot do for you

It cannot un-execute a tag that loaded before it. It cannot see server-side tagging or a conversions API call made from your backend — those are outside the browser entirely and need a consent check in your own code, which is what the consent API is for. It cannot classify a vendor nobody has classified.

And it does not ship content-security-policy guidance today. If you run a strict script-src, you will need to allow the API host yourself; there is no nonce integration and no subresource-integrity hash published for the bundle. That is a real gap rather than a feature, and it is better to read it here than to find it in a CSP report.

Questions teams ask about this

What happens to a tag that loaded before the widget did?

It ran, and nothing can undo that. The cookie purge will delete cookies it wrote for categories the visitor has not consented to, on that load and after every decision, which limits the damage — but the network request was made. This is why the embed instruction is positional rather than a suggestion, and why a scan reports pre-consent network hosts separately from pre-consent cookies.

Does it work through Google Tag Manager?

The widget must load in the page head above the GTM container snippet, not as a tag inside GTM. A consent layer deployed through the tag manager it is meant to gate has already lost the race. Once it is placed correctly it writes Consent Mode v2 defaults before the container initialises and pushes an update on every decision, so GTM-managed tags respect the choice without individual blocking rules.

Can one key serve several sites with different banners?

The surface can vary per page, because the mode is a tag attribute read at runtime — a full banner on the marketing site and a floater inside the product using the same key. Copy, position, purposes, languages and visible cookie categories are held against the key, so genuinely different banners want separate keys, each with its own origin allow-list.

How long does a consent decision last before the banner asks again?

A year from the decision. The expiry is stored with the decision, and an expired record is treated as no record at all, so the banner re-prompts rather than continuing to rely on it. Withdrawal, by contrast, is immediate and is not tied to that clock.

Do I need a deploy to change the banner wording?

No. The tag carries a key and an optional mode; wording, position, offsets, visible categories, purposes and languages are configuration fetched at load. Changing them is a dashboard change that every embedded property picks up on its next page view, which is the main reason they are not attributes.

Describes how ProtectComply implements one obligation under the DPDP Act, 2023 and the DPDP Rules. General information, not legal advice.

Dinkar Singh — Dinkar covers privacy engineering at ProtectComply — discovery, consent propagation and the evidence trail behind them.

See what your current tag setup is doing

Scan a live URL and get back every script that fires before a visitor has chosen anything.

Scan a site →