Writing · Case studies
The consent timing bug that was blocking Google Ads conversions
Google Ads conversions blocked while the tag fired correctly every time: how Consent Mode ordering causes it, and two more faults in the same container.
In January 2026 the Google Ads specialist at the agency I was working for referred a problem to me: conversions were not arriving in an ASX-listed client’s Google Ads account. It had been open for about three months, worked on by the specialist and by the client’s internal technical team.
What it turned out to be was not a Google Ads problem or a form tracking problem. It was a consent ordering problem, with two more faults in the same container underneath it.
Before the diagnosis, it is worth saying why three months is not surprising, because it is the whole reason this class of bug is worth writing about. The tag fires. GTM reports it as fired. Preview shows it firing. Every check you would normally run says the tracking is working, because the tracking is working. The failure happens after that, outside GTM, where Google discards the hit for a reason nothing in the container surfaces. If you are looking at the tag, the trigger and the dataLayer, you can look for a very long time.
Fault one: the conversions were being blocked, not lost
Google Consent Mode starts from a default state, and on this site the default was denied. The consent platform stored the visitor’s actual choice in its own cookie, and a separate tag read that cookie and pushed the update into Consent Mode.
The order those two things happened in is the entire bug:
- A visitor accepts cookies. The choice is written to the consent platform’s cookie.
- On the next page load, Consent Mode initialises from its denied default.
- The visitor submits the contact form.
- The Google Ads conversion tag fires.
- Google blocks it, because as far as Consent Mode is concerned this visitor has not consented.
- Then the consent update tag runs and grants permission.
Every one of those steps is working exactly as designed. The sequence is what is wrong, and the sequence is the one thing the container does not show you. You only see it by checking what Google actually accepted, rather than what the container sent.
The fix is a Custom HTML tag on the Initialization - All Pages trigger,
which is the only trigger that runs before everything else. It reads the consent
platform’s cookie, decodes which permissions were granted, and calls
gtag('consent', 'update', ...) immediately, so the state is correct before any
measurement tag has a chance to fire.
Verified in GTM Preview, before and after:
| Consent signal | Before | After |
|---|---|---|
ad_storage |
Denied | Granted |
analytics_storage |
Denied | Granted |
ad_user_data |
not set | Granted |
ad_personalization |
not set | Granted |
Two different problems were being solved there, and they are worth keeping apart. One is data accuracy: the ads account was under-reporting, so it was optimising against an incomplete picture, which is the same class of problem as an account whose structure nobody has audited. The other is consent compliance, because analytics was firing on the wrong signal. For a listed company running an enterprise consent platform, the second one is not a reporting nuisance.
Fault two: consent was being checked in two places
The form submission trigger carried its own consent condition, checking a custom variable before it would fire. Google’s native Consent Mode was also running. Both were gating the same tags.
That is the actual root cause, and it is worth being able to defend as a principle rather than a one-off fix. If you gate consent in the trigger and in Consent Mode, the two checks will drift. They are maintained by different people at different times, they read different sources, and nothing forces them to agree. The bug above is what that drift looks like from the outside.
So the trigger-level condition came out rather than being corrected, and the trigger became shared by the analytics and ads tags instead of being duplicated per platform. The cleanup document generalises it for whoever picks the container up next: any trigger still checking those custom consent variables can have the condition removed, because Consent Mode handles the blocking natively. The now-redundant per-platform consent update tags go with them.
Fault three: the site was firing every form event twice
The website pushed two identical form submission events per submission. There was already a workaround in the container for it, a gatekeeper tag I had built to swallow the duplicate.
The recommendation I wrote argues against my own workaround. The duplicate should be fixed at source, in the CMS webform or the global tracking script, which would let the gatekeeper be deleted entirely.
A workaround in the tag manager is a fix in the wrong layer. It works, and it also means the next person to touch that container inherits a tag whose only purpose is to compensate for a bug somewhere they cannot see.
The tracking that had never fired at all
One brand’s form tracking used an element visibility trigger watching a specific form ID. That ID no longer existed on the page. The form had been rebuilt at some point and been given a new one, so the trigger had simply never fired.
The obvious fix is to put the new ID in. I did not do that, because it breaks again the next time anyone rebuilds that form, and it will break silently in exactly the same way.
Instead I built a listener for the form platform’s own JavaScript callback: a
window.postMessage listener for the submit event, pushing a dataLayer event
that carries whatever form ID submitted. It works for every form on the site
including ones that do not exist yet, and it does not care when marketing
rebuilds one.
The audit underneath it
While in there I documented the container’s condition: inconsistent tag naming,
an unused listener tag, six unused triggers, and no naming convention. The
recommendations propose [Platform] - [Type] - [Name], with a rename table, and
sequence the work into three priorities by risk so nothing measurement-critical
gets renamed in the same pass as the safe housekeeping.
Verified live, by the client’s own team
The fixes were published to the live container and verified working in production by the client’s internal technical team, independently of me and not in Preview. That is the strongest thing available to say about this work, and it is worth more than my own sign-off would be: my documents end at a publish procedure written for somebody else to action.
What I do not have is a number. Nobody captured conversion counts either side of the change, so the size of the recovery is unknown. Measuring before you change something is a discipline I now handle with a reporting pipeline rather than good intentions. A confirmed deployment is not a measured result and it would be easy to blur the two.
The diagnosis is the work here. One symptom came in, conversions not arriving. Three separate faults came out, two of which nobody had reported because nobody had seen them. A bug that presents as “everything is configured correctly and it still does not work” is exactly the kind that survives competent people checking the obvious things, because the obvious things are all fine.
Get in touch
Tell me about the role and I will come back to you. If you would rather not use a form, I am on LinkedIn.