Writing · Case studies
How I found 3 faults blocking every Google Ads conversion
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.
It was neither a Google Ads problem nor a form tracking problem. The tracking was working perfectly and Google was throwing the results away, because of the order two things happened in as the page loaded. Two more faults sat underneath that one.
Three months is not surprising, and that is the whole reason this kind of bug is worth writing about.
The tracking on this site lives in Google Tag Manager, a container that sits on every page and decides what information gets sent where. It was doing its job. It recorded the conversion as sent. Its preview mode showed it being sent. Every check you would normally run says the tracking is working, because the tracking really is working.
The failure happens after that, outside the container, when Google receives the data and discards it. Nothing inside Tag Manager shows you that. So you can check the tracking for a very long time and find nothing wrong, because there is nothing wrong with it.
Fault one: the conversions were being blocked, not lost
A cookie banner does one job: it tells Google whether it may use that visitor’s data. Google’s system for listening to that answer is called Consent Mode. It starts every page assuming the answer is no and waits to be told otherwise.
On this site the visitor’s real answer was stored in a cookie by the consent banner, and a separate piece of tracking read that cookie and passed the answer on. The waiting is where it went wrong.
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 small piece of code set to run before anything else on the page. In
Tag Manager that means a Custom HTML tag on the Initialization - All Pages
trigger, the only one that runs that early. It reads the banner’s cookie, works
out which permissions the visitor granted, and tells Google straight away, so
the answer is already correct by the time any tracking fires.
Here are the four permissions before and after the fix. The first two decide whether Google may store anything at all for ads and for analytics. The second two decide whether it may use that data to target advertising.
| 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
Consent was being checked twice for the same form submission: once by Google’s own Consent Mode, and once by a hand-built condition inside Tag Manager that read a different source. Both were deciding whether the same tracking was allowed to run.
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 hand-built condition came out instead of being corrected, and one shared check now covers both the analytics and the ads tracking instead of each platform carrying its own copy. The cleanup document generalises it for whoever picks the container up next: any check still reading those custom consent variables can be removed, because Consent Mode does that job on its own. The per-platform consent tags that existed to support it 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 worked by watching for a particular form to appear on screen, recognising it by an ID written into the page. That ID no longer existed. The form had been rebuilt at some point and given a new one, so the tracking had never once 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 listened for the form platform’s own announcement that a form had been
submitted, and passed along whichever form it was. Technically that is a
window.postMessage listener feeding a dataLayer event. 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 state of the container: inconsistent naming, an
unused listener, six unused triggers, and no naming convention at all. 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.