What a GTM Consultant Actually Fixes

Google Tag Manager Enhanced conversions Consent Mode Tracking
← All posts

Why people call a GTM consultant

Most people hire a GTM consultant after their conversion numbers stop making sense. The tags were set up once, they worked, and then one day the graph fell off a cliff, or enhanced conversions coverage started sliding, or GA4 filled up with unassigned traffic that nobody could explain.

None of that is exotic. The same handful of problems come up again and again across accounts, and the fixes are well understood once you have seen them a few times. This post walks through the recurring ones, from real implementations with clients anonymised to platform and sector, so you can either diagnose your own setup or know what to expect when you bring someone in.

Everything here is Google Tag Manager work: web containers, server-side containers, Consent Mode, enhanced conversions, and the awkward gap between a form submission and a thank you page.

The most common problem: data on the wrong page

Here is the setup that breaks more enhanced conversions implementations than anything else I open.

Your conversion fires on the thank you page. That is correct and normal. But enhanced conversions need user-provided data, an email and ideally a name, and that data lives on the form page, not the thank you page. By the time the conversion tag fires, the form fields are gone. The tag has nothing to hash and send, so coverage quietly degrades and Smart Bidding loses a signal it was relying on.

The fix is to capture the data at the moment of submission and carry it across the page navigation. Three ways to do it, in rough order of preference.

The data layer, if a developer can push the value on submit

dataLayer.push({
  'event': 'form_submission',
  'user_email': 'user@example.com' // the actual submitted value
});

A Data Layer Variable in GTM then reads user_email and feeds it into the enhanced conversions setup. The cleanest option, but it needs a code change on the site.

localStorage, when everything must live inside the container

When you cannot touch the site's form handler, capture on the form page and read on the thank you page:

// Custom HTML tag, fires on the form page
document.addEventListener('submit', function(e) {
  var email = document.querySelector('input[type="email"]');
  if (email && email.value) {
    localStorage.setItem('ec_email', email.value.toLowerCase().trim());
  }
});
// Custom JavaScript Variable, read on the thank you page
function() {
  return localStorage.getItem('ec_email') || '';
}

A note from doing this repeatedly: lowercase and trim the value on the way in. Google's own documentation requires exactly that normalisation before hashing in manual implementations, because a hash of  User@Site.com and a hash of user@site.com are different values and the match simply fails. Even where Google normalises for you, clean input costs nothing.

And add a cleanup tag on the thank you page, sequenced to fire after the conversion, that clears the stored keys. There is no reason to hold user data around longer than the conversion needs it.

AJAX forms silently defeat the submit listener

This one costs people days because nothing errors. The trigger just never fires.

Plenty of form plugins, and Quform is a repeat offender in my accounts, submit over AJAX and keep the user on the same page. A native submit event listener never gets the event it is waiting for, so your capture script does nothing and gives you no obvious reason why.

The fix is to stop listening for submit and listen for a click on the submit button instead, targeting the button's class:

document.addEventListener('click', function(e) {
  if (e.target.closest('.quform-submit')) {
    // capture fields here
  }
});

If the form submits without a page change, assume the native submit event is unreliable and go to the button click. It is the first thing I check when a form trigger "should work" but does not.

While on the subject of brittle triggers: be wary of any trigger built on a long, positional CSS selector like #form > div > div > div > h4. The moment a developer changes that h4 to an h2, or adds a wrapper div, the selector no longer matches and the tag stops firing, with no GTM change to blame. If the last container edit predates the drop, the culprit is usually a change on the page that your selector was silently depending on. Prefer stable IDs and form classes over DOM paths.

The debugging order that saves the most time

When a form conversion drops, the instinct is to dive into the tag. Wrong first move. The question to answer before anything else is whether you have a tracking problem or a submissions problem.

Check the form backend, CRM or inbox for actual submission counts over the same window. Then:

  • Backend steady but GA4 or Ads dropped: it is a tracking break. Go into the container.
  • Both dropped: it is a site or UX problem, and no amount of tag work will fix it.

Only once you have confirmed it is tracking do you open GTM Preview, submit the form yourself, and watch whether the trigger fires and the tag executes. If the trigger fires but the tag does not, it is tag configuration. If the trigger never fires, something on the page changed. This ordering sounds obvious written down, and it is routinely skipped.

A quick console check on the form page tells you whether the data layer is even receiving anything when you submit:

dataLayer.push = function(obj) { console.log('DL push:', obj); }

Consent Mode is where good setups go to break

Two consent failures come up constantly, and they look nothing alike.

The endless Consent Update loop

On one Shopify build, consent states were being set correctly, ad_storage and analytics_storage both granted, yet the container fired dozens of consecutive Consent Update events and would not settle. The cause was two consent systems talking over each other: Shopify's native consent management and a custom GTM implementation, each reacting to the other's updates. The lesson is to pick one source of truth for consent. Running a platform's native consent alongside a hand-rolled one produces exactly this kind of feedback loop.

The unassigned traffic spike

After Consent Mode went live on another site, GA4 unassigned traffic jumped and stayed high even though consent looked correctly configured. The usual causes, in order of likelihood: tags firing before consent registers, so the hit lands with no attribution; consent not shared across domains; and a timing gap between when the CMP writes consent and when GA4 wants to fire. The fix is making tags genuinely consent-aware rather than firing on page load and hoping consent arrived first.

The CMP that blocks the entire container

This is the most frustrating version of a consent problem, because it is not a GTM bug at all and no amount of work inside the container will fix it.

On one client site, GTM simply would not load without cookie consent. I confirmed it by opening the site in incognito, declining, and running typeof google_tag_manager in the console, which returned "undefined". The whole container was being blocked before it could initialise.

The cause was Cookiebot in auto-blocking mode, hardcoded in the page source as data-blockingmode="auto" on the Cookiebot script tag. In that mode the CMP blocks known tracking scripts, including GTM itself, before they run. Two things worth knowing, both learned the hard way:

  • Adding data-cookieconsent="ignore" to a GTM Custom HTML tag does nothing, because GTM strips custom attributes when it injects the tag.
  • Switching the Cookiebot dashboard to manual mode also does nothing, because the hardcoded data-blockingmode="auto" attribute in the site source overrides the dashboard setting.

The only real fix is a source-level change by a developer: change data-blockingmode from auto to manual on the Cookiebot script itself, or add data-cookieconsent="ignore" directly to the GTM script tag in the source. If your container is genuinely not loading when consent is declined, check the CMP's blocking mode before you touch anything else. You can lose a lot of time debugging a container that never had a chance to run.

When client-side has run out of road: server-side GTM

Every fix above is a patch on a client-side setup that browsers are actively working against. Safari's ITP, ad blockers and consent tooling all chip away at client-side tags over time. Past a certain point the answer is not another workaround, it is moving the important measurement server-side.

The clearest case I have had for it was a donation-tracking build where client-side GTM tags were both causing the consent loop and getting blocked, so a large share of completed donations never reached GA4. The eventual working setup fired the conversion from the server, off the actual database record of a successful payment: no JavaScript, no consent dependency, nothing for an ad blocker to catch. Every donation that hit success in the backend was sent directly to GA4, and the now-redundant client-side tags were paused.

The same principle applies to any booking or checkout flow that takes a real payment. Firing on the browser landing on a thank you page under-counts users who never return to it and over-counts on refresh. Firing off the payment event itself, Stripe's checkout.session.completed or payment_intent.succeeded, or a verified PayPal IPN, then sending it via a server-side container or the Ads API with the stored gclid and a unique transaction ID to deduplicate, records every real payment exactly once, regardless of what the browser does.

To be clear about the trade: server-side is not a first resort. It is more infrastructure to run and reason about, and most accounts do not need it. But when tags are dropping to browser restrictions and the revenue signal actually matters, it is the difference between measuring most conversions and measuring all of them.

The through-line

The recurring theme across all of this: Smart Bidding is only ever as good as the signal you feed it. A lot of accounts that look like they have a bidding problem actually have a tracking problem wearing a bidding problem's clothes. Enhanced conversions coverage sliding, a form trigger silently broken by an AJAX submit, consent blocking half your tags, a conversion firing on the wrong event: each one starves the optimiser of accurate data, and the campaign performance that follows gets blamed on the wrong thing.

Good GTM work is mostly this. Making sure the data going in is accurate, complete, and durable against the browser changes steadily eroding client-side tracking. It is not glamorous, but it is the foundation everything else in the account sits on.

If your conversion numbers have stopped matching reality, or your enhanced conversions coverage is drifting down and nobody can say why, that is the work, and it is fixable.

These notes come from real Google Tag Manager implementations across ecommerce, lead generation and non-profit sites, with client details generalised.

Conversion numbers stopped matching reality?

Diagnosing and fixing exactly this is the core of my tracking work: GA4, GTM, enhanced conversions, Consent Mode and server-side tagging where it earns its keep, delivered as a one-off project with documentation you own. No retainer, no black box.

Get in Touch