Google Ads and GA4 report that Consent Mode v2 signals are missing — remarketing audiences stop populating and Tag Assistant flags the consent state as incomplete — because the CMP you chose emits the two legacy signals but not the two v2 signals ad_user_data and ad_personalization.
Triage: Confirming the Missing v2 Signals
Work through these checks before changing any configuration. The goal is to prove the CMP is the source of the gap, not GTM or the tag itself.
-
Open GA4 DebugView or Tag Assistant. Load the site in a fresh session with storage cleared. Trigger the consent banner and accept all.
-
Inspect the consent state event. In Tag Assistant, click the consent update event. A correct v2 implementation shows four rows:
ad_storage,analytics_storage,ad_user_data,ad_personalization. If only the first two are present, the CMP is emitting a Consent Mode v1 payload. -
Read the live state from the console. Google exposes the resolved consent state internally; the fastest external check is to inspect the
dataLayerfor theconsentcommands:// List every consent command the page pushed, in order. window.dataLayer .filter((e) => Array.isArray(e) && e[0] === 'consent') .forEach((e) => console.log(e[1], e[2]));A healthy trace shows a
defaultwith all four keys set todenied, then anupdatewith the four keys reflecting the user’s choice. Missingad_user_data/ad_personalizationin either call confirms the defect. -
Run the reproduction checklist:
The four v2 signals are not interchangeable, and the two added in v2 are the two most often missing — a platform upgraded from v1 emits a structurally valid call that Google’s tags treat as incomplete.
Because there is no error, the gap is usually discovered in a reporting review weeks later rather than in testing. Make the four-signal assertion part of your release checks: a browser test that reads the data layer after acceptance and fails when fewer than four keys are present costs almost nothing and catches every regression of this shape.
Root Cause: CMP Emits v1 Signals or Fires update() Too Late
Google Consent Mode v2 became mandatory in March 2024 for advertisers serving EEA users through Google’s advertising products. Version 2 added two signals on top of the original set:
ad_user_data— governs whether user data may be sent to Google for advertising purposes at all.ad_personalization— governs whether that data may be used for personalized advertising (remarketing, similar audiences).
A CMP built for the original Consent Mode only writes ad_storage, analytics_storage, functionality_storage, personalization_storage, and security_storage. Google’s newer tags read ad_user_data/ad_personalization and, finding them undefined, withhold data from advertising features even when the user consented. The remarketing audience simply stops growing.
There are two distinct failure shapes. The first is a missing-signal defect: the CMP template predates v2 and never writes the two new keys. The second is a timing defect: the CMP writes all four keys but calls update() after the Google tag has already read the default state, so the tag operates on stale denied values. Both surface identically in Tag Assistant, which is why the triage above distinguishes them by checking command order in the dataLayer, not just presence.
Selecting a CMP for Consent Mode v2 therefore reduces to three hard requirements. The CMP must (1) write all four signals in both default and update, (2) emit the default synchronously before the Google tag loads, and (3) integrate through Google’s certified CMP partner program or a maintained template so signal names stay in sync as Google evolves them. This sits under the broader guidance on selecting and integrating a consent management platform, which is the governing reference for the source-of-truth architecture this fix plugs into.
Note the relationship to basic vs advanced Consent Mode. In basic mode the Google tags do not load until consent is granted, so the default denied state simply prevents loading. In advanced mode the tags load immediately and send cookieless, anonymized pings under the denied default, then upgrade on update. Advanced mode is what feeds Google’s conversion modeling — but it only works if the four v2 signals are present in the default, because the modeled pings are tagged with the consent state at send time.
Resolution: Emit All Four Signals in the Correct Order
The minimal fix has two halves: a hardcoded default you control directly in <head>, and a CMP update that carries all four signals. Do not rely on the CMP for the default if you can set it yourself — owning the default guarantees signal completeness and correct ordering regardless of CMP quirks.
<!-- head: BEFORE the CMP bundle and BEFORE gtm.js / gtag.js -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
// Consent Mode v2 default: all four ad/analytics signals denied.
// Omitting ad_user_data or ad_personalization here is the exact bug
// that stops remarketing audiences from populating.
gtag('consent', 'default', {
ad_storage: 'denied',
analytics_storage: 'denied',
ad_user_data: 'denied', // v2 — required
ad_personalization: 'denied', // v2 — required
functionality_storage: 'granted',
security_storage: 'granted',
wait_for_update: 500 // ms Google waits for update() before acting on default
});
</script>
<!-- then the CMP bundle, then the Google tag -->
When the user answers the banner, translate the CMP’s choice into a single update carrying all four signals. Drive it from the one place that reads the CMP, so the mapping lives in exactly one module:
// Called from your CMP adapter on every resolved consent change.
function pushConsentModeV2(choice) {
// choice is your normalized object: { analytics, ads } booleans.
// Map both v2 ad signals off the advertising consent, and analytics
// off the analytics consent. Keep the update atomic — one call, four keys.
gtag('consent', 'update', {
analytics_storage: choice.analytics ? 'granted' : 'denied',
ad_storage: choice.ads ? 'granted' : 'denied',
ad_user_data: choice.ads ? 'granted' : 'denied',
ad_personalization: choice.ads ? 'granted' : 'denied'
});
}
// TCF v2.2 CMP: derive the choice, then push. Only act on resolved states.
window.__tcfapi('addEventListener', 2, (tcData, ok) => {
if (!ok) return;
if (tcData.eventStatus !== 'tcloaded' && tcData.eventStatus !== 'useractioncomplete') return;
const p = tcData.purpose.consents;
pushConsentModeV2({
analytics: p[1] === true && p[8] === true, // store + measure content
ads: p[1] === true && p[3] === true && p[4] === true // store + create/select ads profile
});
});
If you use a certified CMP template (OneTrust’s or Cookiebot’s Consent Mode v2 build), the vendor emits these calls for you — your selection criterion is simply to confirm the template is the v2 version and that it is configured to fire the update from the same purpose mapping. Do not run both your own update and the CMP’s; pick one owner for the mapping.
Reading the signals back out
Verification is a matter of reading the same four keys the tags read, at the two moments that matter. Both reads have a distinct expected shape, and a platform that is subtly wrong will produce a plausible-looking value at one of them.
Verification
Reload with storage cleared, open GA4 DebugView, and accept all consent. In the consent state panel the four signals must read: analytics_storage: granted, ad_storage: granted, ad_user_data: granted, ad_personalization: granted. Then reload and reject: all four must read denied. The single decisive check is that ad_user_data and ad_personalization appear and change — their presence and correct toggling is what Google Ads reads to resume audience collection. Within 24–48 hours, the Google Ads audience that had plateaued should resume growing.
Order is part of the contract
The two calls have to happen in a fixed order relative to the container, and the window between them is where every remaining defect lives. The default call must precede the container; the update call must follow the decision; and nothing in between may assume a resolved state.
Common Pitfalls
- Setting the
defaultafter the CMP loads. If thedefaultcommand is not synchronous in<head>before the Google tag, the tag reads an undefined state and assumes granted, firing pre-consent. The default must be the firstdataLayerpush. - Mapping only
ad_storagefrom the ads choice and leaving the two v2 signals denied. All three advertising signals (ad_storage,ad_user_data,ad_personalization) should derive from the same advertising consent unless your legal basis deliberately splits them. Leaving the v2 pair hardwired todeniedsilently kills remarketing. - Running two owners of the
updatecall. A hand-rolledupdateplus the CMP template’supdaterace each other; the loser overwrites the winner. Pick one owner, as covered in the CMP integration guide.
Frequently Asked Questions
Does Consent Mode v2 replace a TCF integration?
No — they answer to different consumers and both may be required at once. Consent Mode is what Google’s own tags read; the TCF string is what IAB-registered vendors in a programmatic auction read. A publisher selling inventory through those vendors needs both, emitted from the same decision, and a site running only Google properties needs only the former.
Treat them as two outputs of one adapter rather than two integrations. Deriving both from a single normalised consent object is what keeps them from drifting apart, which is the failure that produces a visitor consented in one framework and refused in the other.
What should the four signals default to for a visitor outside the EEA?
That depends on which regime applies, and it is the reason the default call should be built from the resolved region rather than hard-coded. Under an opt-out regime the defaults can legitimately be granted, and forcing denied there loses measurement you were entitled to collect. Under an opt-in regime they must be denied.
Because the default call has to run inline before the container, the region has to be known at that moment — which in practice means it comes from the edge, injected into the document, rather than from a client-side lookup that resolves after the container has already booted.