Step 1: export and inventory
Export the container (Admin → Export Container). The JSON lists every tag, trigger, variable and folder. Build the inventory from it — a spreadsheet is enough — with one row per tag: vendor, tag type, trigger, variables read, and a classification:
- Recognised vendor tag (GA4, Google Ads, Meta, TikTok, LinkedIn, Microsoft, Pinterest, Snapchat, Reddit, X, Taboola, Outbrain, Criteo, affiliate pixels) with a matching destination template.
- Data layer push grouped by event name, with the variables each tag reads.
- Custom HTML tag, classified as vendor snippet with a known equivalent, first-party helper (scroll depth, form listeners), or unknown.
- Paused tags and tags without recent firings (GTM's tag metadata shows when a tag last fired), flagged as retirement candidates.
The inventory is the migration's source of truth. Nothing is created from it automatically; you review each row and decide.
Step 2: translate into a tracking plan
GTM thinks in tags and triggers; Track thinks in events and destinations. The translation:
| GTM concept | Track concept |
|---|---|
dataLayer.push({event: 'purchase', ecommerce: {...}}) | tsq.push(['track', 'purchase', {...}]) with the canonical commerce schema |
| Trigger "Custom Event equals purchase" | the event itself; destinations subscribe by event name |
| Trigger "Click — All Elements" with CSS filter | a declarative click rule (selector, event name, allowed properties) |
| Data layer variable | a property in the event schema, typed and validated |
| Lookup table | a mapping table in the destination, versioned and diffable |
| Consent initialisation tag | the CMP integration; consent is read, never set |
| GA4 config + event tags | one GA4 destination with event mappings, browser and server |
| Google Ads conversion tag per action | one Google Ads destination with one mapping per conversion action |
The plan lists every event with its properties, its consent purpose and the destinations it feeds. Critical events are marked so the health score watches them.
Step 3: parallel run with deduplication
Do not switch off GTM on day one. Install the Track SDK alongside it. The SDK observes GA4-shaped data layer pushes without executing anything, so a container that already pushes purchase with an ecommerce object feeds both systems from the same push.
For each destination, start with server-side only in Track while the GTM browser tag keeps firing:
- For Google Ads and GA4, purchases carry the same transaction id on both paths and the vendor deduplicates on it.
- For platforms that deduplicate on event id (Meta, TikTok, Microsoft, Pinterest, Snapchat, Reddit) run the server path in test mode first, compare counts in the vendor's test view, then cut over directly — do not run both live with different ids.
The destination monitor shows acceptance per destination; when it matches the GTM tag's numbers, the browser tag is redundant.
Step 4: cut over per destination
For each destination, in this order: switch Track to hybrid (its own browser template plus server), pause the GTM tag, watch the vendor's event count for two days, then delete the GTM tag. Analytics destinations first, advertising destinations one at a time, affiliate networks last because their postbacks need the click-id store warmed up for the cookie window.
Step 5: retire the container
When every tag is paused and the vendor counts match, remove the GTM snippet. Keep the exported container in the migration record; the audit log shows who cut over what and when.
What has no equivalent, on purpose
- Custom HTML and custom JavaScript tags. Track executes no site-authored code. Vendor snippets become declarative templates; first-party helpers become declarative rules; anything else is a decision to make explicitly, usually by moving the logic into the site's own code where it belongs.
- Consent overrides. No tag can be set to fire regardless of consent.
- Tag sequencing and firing priorities. Events are routed by policy, not raced.
- Regex-based scraping of the page into variables. Properties come from the data layer or declarative rules, not from DOM text.
Mark each of these in your inventory so you know before you start how much of the container is real tracking and how much is accumulated scaffolding.