focosys.io
← Writing
Essay

How to Audit Your GTM Container Before Migrating to sGTM

A practical GTM audit checklist to run before migrating to server-side GTM, covering tag inventory, duplicate tracking, consent gaps, and variable dependencies.

A client asked me to migrate their client-side container to sGTM last spring. Two-week job, they said. The container had "maybe 20 tags."

It had 87 tags. Fourteen of them hadn't fired in over six months. Three different people had installed three different versions of the Meta Pixel at three different points in the company's history, and nobody had removed the old ones. The GA4 config tag had a Google Ads conversion ID hardcoded into a custom HTML tag from 2022, sitting right next to the actual GA4 config tag doing the same job.

Nobody planned that mess. It accumulated one contractor, one agency handoff, one "just add this snippet real quick" at a time. That's exactly what turns a two-week sGTM migration into a six-week one, because you don't find out the container is a disaster until you're halfway through mapping tags to server-side equivalents.

Audit the container first. Before you touch server-side GTM, before you provision a Cloud Run instance, before you pick a tagging server domain. Here's what to actually check, in order.

Why This Matters More for sGTM Than a Normal Cleanup

Migrating to server-side GTM isn't a lift-and-shift. Every tag you move has to be re-evaluated: does it need a server-side equivalent, does it stay client-side, does it get killed entirely. Tags that were harmless clutter in a client-side container become active decisions in a server-side one, because someone has to configure a server client, map the fields, and test the request.

If you migrate a container without auditing it first, you carry every bad decision from the last three years into your new infrastructure. You'll spend migration time debugging tags that should have been deleted in 2023.

Don't migrate technical debt

If a tag hasn't fired in 90 days and nobody can explain what it's for, it doesn't get a server-side equivalent. It gets deleted. Migration is the cheapest time you'll ever have to clean house, because you're already touching every tag anyway.

Step 1: Export and Inventory Every Tag

Go to your GTM workspace, Admin → Export Container. This gives you the full JSON, which is faster to search than clicking through 87 tags in the UI.

Open the JSON and build a spreadsheet with four columns: tag name, tag type, trigger, last fired date. You get "last fired" from GA4's Tag Manager reporting (if you have it connected) or by cross-referencing GTM's built-in "Tag firing" debug view over a week of live traffic.

Anything that hasn't fired in 90 days goes on a "kill or confirm" list. Send that list to whoever owns the marketing stack and ask directly: is this still needed? Most of the time the answer is "I don't know what that is," which is your answer.

Tag Name                    Type              Trigger           Last Fired
GA4 Config                  GA4 Configuration All Pages         Today
Meta Pixel (legacy)         Custom HTML       All Pages         214 days ago
Meta Pixel v2               Facebook Pixel    All Pages         Today
Google Ads Conversion       Google Ads Conv    Purchase          Today
Hotjar                      Custom HTML       All Pages         Today
LinkedIn Insight (old)      Custom HTML       All Pages         189 days ago

That table alone tells you two Pixels are firing simultaneously (double-counting Meta conversions) and two stale tags need to go.

Step 2: Find Duplicate and Conflicting Tags

This is the single most common thing I find in container audits. Search the exported JSON for tag type repeats: multiple GA4 config tags, multiple Meta Pixel base codes, multiple Google Ads conversion tags pointing at the same conversion action.

Search for these strings in the exported JSON:

"type": "gaawc"        // GA4 Configuration
"type": "gclidw"       // Google Ads Conversion Tracking
"facebookIncPixel"     // Meta Pixel (custom template)

Count the matches. If you have more than one GA4 config tag firing on "All Pages," you're either double-firing pageviews or someone set up a second GA4 property nobody's using anymore.

I've seen a container with three GA4 config tags: one from the original setup, one added by an agency who didn't check for an existing one, and one from a "test property" that never got removed after testing. Every pageview fired three times to three different GA4 properties. The main property's numbers were inflated by roughly 8% from a redundant trigger overlap on form submissions.

Step 3: Map Every Tag to a Data Source

For each tag, answer one question: where does its data come from? Dedicated dataLayer push, auto-collected variable, DOM scraping, cookie read, or hardcoded value.

This matters because sGTM clients (the GA4 client, the Meta CAPI transform, custom clients) expect specific event structures. A tag reading {{Click URL}} from an Auto-Event Variable works fine client-side. Server-side, there's no DOM to scrape, so that variable is dead on arrival unless the data gets pushed into the dataLayer before the event reaches the server.

Go through your variables list (Variables tab in GTM) and flag anything that's:

  • A Custom JavaScript variable reading document, window, or DOM elements directly
  • A DOM Element variable
  • An Auto-Event variable tied to click/form triggers that scrape page elements

Every one of these needs a dataLayer replacement before migration, because the server container never sees the browser DOM.

The dataLayer is your contract with the server

If a value isn't explicitly pushed to the dataLayer, sGTM never receives it. Client-side GTM can cheat by reading the page directly. Server-side GTM only gets what you send it. Audit every variable that currently cheats this way, because each one is a migration blocker.

Open each tag and check its consent settings (bottom of the tag configuration, "Consent Settings" section). You're looking for two things: is Consent Mode v2 configured, and does the trigger actually respect it.

Export the JSON again and search for "consentSettings". Tags without this block fire unconditionally, consent or not. In a client-side container this is bad. In an sGTM migration it's worse, because you're about to build a consent-aware routing layer server-side, and if the client-side signal was never gated properly, your server container inherits garbage input.

Test it directly: reject all cookies on your site, open DevTools Network tab, filter for your GTM domain and any ad platform domains (google, facebook, doubleclick). If requests still fire after rejection, fix that before migration, not after. Server-side GTM won't fix a consent problem that originates client-side.

Step 5: Identify Server-Side Candidates vs. Client-Side Keepers

Not everything moves to the server. Sort your tag inventory into three buckets:

Move to server: Conversion APIs (Meta CAPI, Google Ads Enhanced Conversions, TikTok Events API), GA4 measurement protocol calls, anything currently vulnerable to ad blockers (check this by loading your site with uBlock Origin enabled and watching what fails in the Network tab).

Keep client-side: UI-dependent tags like Hotjar, on-page personalization scripts, chat widgets, anything that needs to manipulate the DOM in real time. These don't benefit from server-side and often can't function there.

Kill: Anything from Step 1's stale list, plus any tag duplicating another tag's function.

Run the ad-blocker test on your current container before deciding what to prioritize. Load the site in an incognito window with uBlock Origin, open the Network tab, and watch which requests get blocked. I typically see 15-30% of Meta Pixel and Google Ads conversion pings blocked outright on a standard consumer browser with an ad blocker. That's your baseline case for which tags need server-side routing first.

Step 6: Document Trigger Logic Before You Touch Anything

Triggers get weird over time. Someone builds a trigger with four AND conditions, then someone else copies it and adds a fifth condition for one specific campaign, and now you have two nearly-identical triggers firing the same tag under slightly different logic.

Pull every trigger into your spreadsheet alongside the tags. For each one, write in plain language what it's supposed to do: "fires on thank-you page view when URL contains /order-confirmation AND transaction_id variable is not undefined." If you can't write that sentence without opening the trigger and squinting at four nested conditions, the trigger is due for simplification.

This step feels tedious. It's the difference between a migration where you know exactly what event structure the server container needs to replicate, and a migration where you're reverse-engineering trigger logic mid-project because nobody wrote it down the first time.

What to Do With the Audit Results

By the end of this process you should have:

  1. A tag inventory spreadsheet with last-fired dates and a kill/keep/migrate decision on each row
  2. A list of duplicate or conflicting tags to consolidate before migration
  3. A list of DOM-dependent variables that need dataLayer replacements
  4. A consent gap list, tested in DevTools, not just checked in a CMP dashboard
  5. A three-bucket sort of server-side candidates, client-side keepers, and kills
  6. Plain-language documentation of every trigger's actual logic

None of this is optional groundwork you can skip because the deadline is tight. Every hour spent here is an hour you don't spend debugging a broken server-side Meta CAPI tag three weeks into migration because the event_id variable was reading from a DOM element that doesn't exist on the server.

Clean the container first. Migrate second.

Christopher Landaverde — Marketing Systems Engineer More writing →