Don't have one? Create one ↗

Reference

Tips & tricks

The small, practical GTM moves that save an afternoon. No course, no order to follow, just the kind of thing you usually only pick up after debugging enough real sites.

12 tips across 4 areas

Debugging & discovery

See what a page is actually doing before you write a single tag.

Find out whether an embedded iframe talks to your page

When this happens

A booking widget, chat box or form lives inside a cross-origin iframe, so you cannot read its DOM and the built-in form trigger never fires. You need to know if it sends anything out to the parent page.

Do this

Most well-behaved embeds use postMessage to tell the parent window when something happens. Drop a temporary catch-all listener on the page in Preview, interact with the embed, and watch the console. If a payload shows up, you have an event to build on: copy the shape and push it into the dataLayer from a Custom HTML listener.

<script>
  // Temporary: drop on All Pages while previewing, then remove.
  window.addEventListener('message', function (e) {
    console.log('postMessage from', e.origin, e.data);
  });
</script>

Watch out

If nothing logs, the embed is silent and you will need its own JS API or a visibility or click workaround instead. Always remove the listener before publishing.

Catch event names you did not know existed

When this happens

A theme or third-party script pushes events to the dataLayer, but you do not know what they are called, and some fire before GTM Preview has fully attached.

Do this

Wrap dataLayer.push so every push prints to the console, including the ones from scripts you do not control. Run it as the very first thing on the page, read the real event names, then trigger on them properly.

<script>
  // Run first. Logs every dataLayer push, including third-party ones.
  (function () {
    var dl = (window.dataLayer = window.dataLayer || []);
    var original = dl.push;
    dl.push = function () {
      console.log('dataLayer push:', arguments[0]);
      return original.apply(this, arguments);
    };
  })();
</script>

Watch out

This is a discovery tool, not something to leave live. Pull it out once you have the event names you need.

See your own hits in GA4 DebugView without flooding it

When this happens

You flip debug_mode on to test in DebugView, but now every visitor shows up there too, and you forget to turn it back off.

Do this

Enable the built-in Debug Mode variable, then set the GA4 tag's debug_mode field to that variable. It returns true only when GTM Preview or Tag Assistant is running, so DebugView lights up for you and nobody else, with nothing to remember to undo.

GA4 Configuration tag
  Field name:  debug_mode
  Value:       {{Debug Mode}}   // built-in variable, true only in preview

Clicks & triggers

Get tags to fire on exactly the right interaction, no more and no less.

Catch the click when someone taps the icon inside a button

When this happens

Your click trigger works when you click the button text, but not when you click the icon or label sitting inside it. The Click Element is the child node, so your selector misses it.

Do this

Match the button and everything inside it. Add the child wildcard to your CSS selector so a click on any nested element still counts as a click on the button.

Trigger: Click - All Elements
  Condition: Click Element  matches CSS selector
  .add-to-cart, .add-to-cart *

Watch out

Turn on the built-in Click variables (Click Element, Click Classes, Click Text) or the condition has nothing to read.

Stop a form redirect from killing your conversion hit

When this happens

A form submits and the browser navigates away instantly, so the tag's request gets cancelled before it leaves and the conversion never lands.

Do this

Hold the navigation for a beat. Intercept the submit, push your event, wait about half a second so the request can go out, then let the form submit for real. The flag stops it from looping.

<script>
  (function () {
    var form = document.querySelector('#lead-form');
    if (!form) return;
    form.addEventListener('submit', function (e) {
      if (form.dataset.tagged) return;          // 2nd pass: let it through
      e.preventDefault();
      form.dataset.tagged = '1';
      window.dataLayer.push({ event: 'lead_submit' });
      setTimeout(function () { form.submit(); }, 500);
    });
  })();
</script>

Watch out

Half a second is plenty and barely noticeable. If you can use the gtag eventCallback instead, do that, it navigates the instant the hit is acknowledged rather than on a fixed timer.

Fire a tag only when two things have both happened

When this happens

You want an engaged-visitor event that fires only when someone has both scrolled past halfway and stayed for 30 seconds, not when either one happens alone.

Do this

Use a Trigger Group. It fires once all of its member triggers have fired on the page, in any order. Add your scroll trigger and your timer trigger to one group and point the tag at the group.

Trigger type: Trigger Group
  Triggers in group:
    - Scroll Depth  (50%)
    - Timer         (30s)
  Fires when: both have fired this page

Variables & data

Read, reshape and reset values so your tags always have clean inputs.

Swap IDs between staging and production with one variable

When this happens

You keep sending staging traffic into your production GA4 property, or you maintain two containers just to hold different IDs.

Do this

Drive the ID off the hostname. A Lookup Table keyed on Page Hostname returns the right Measurement ID for each environment, with the production ID as the default. One variable, one place to change, no duplicate tags.

Variable: Lookup Table
  Input: {{Page Hostname}}
    staging.example.com  ->  G-STAGINGXXXX
    example.com          ->  G-PRODXXXXXX
  Default value:         G-PRODXXXXXX

Map a hundred URLs to five page groups in one variable

When this happens

You want a clean page_category for reporting, but the site has far too many URL patterns to list one by one, and unmatched pages should not come through blank.

Do this

Use a RegEx Table on the URL path. Each row is a pattern, and a final default catches everything you did not name, so the value is never empty. Tick Ignore case to save yourself a class of bugs.

Variable: RegEx Table
  Input: {{Page Path}}
    ^/blog/     ->  Blog
    ^/product/  ->  Product
    ^/cart      ->  Cart
    ^/checkout  ->  Checkout
  Default value: Other

Clear stale dataLayer values before the next push

When this happens

An ecommerce or details object from an earlier push bleeds into a later event because the new push does not mention those keys, so a variable reads last page's data.

Do this

The dataLayer merges pushes, it does not replace them. Reset the object to null right before you push fresh data, so nothing carries over from the previous step.

window.dataLayer.push({ ecommerce: null });   // clear first
window.dataLayer.push({
  event: 'purchase',
  ecommerce: { /* fresh data only */ }
});

Watch out

This is the usual culprit behind duplicated or wrong items in a purchase event. Reset before every new ecommerce push.

Reliability & hygiene

Keep your data trustworthy and your container fast as it grows.

Keep your own team out of the data with one exception

When this happens

Staff testing, agency QA and your own previewing inflate sessions and conversions, and you do not want to add a filter to every tag by hand.

Do this

Make one blocking trigger that identifies internal traffic, for example a cookie you set on your team's browsers, then add it as an exception on your GA4 config tag. Everything that builds on that config inherits the exclusion.

Trigger: Custom Event or Page View
  Condition: {{Cookie - internal}}  equals  1
Add it as an Exception on the GA4 Configuration tag.

Watch out

A cookie travels with the person across pages and survives the session, which beats trying to keep an IP list current.

Guarantee Conversion Linker runs before the conversion fires

When this happens

A first-time visitor converts on the same page they land on, and the conversion tag races the Conversion Linker, so the click is not always stitched to the conversion.

Do this

Use Tag Sequencing. On the conversion tag, set Conversion Linker as the setup tag that fires first. GTM will not fire the conversion until the linker has run and written its cookies.

Conversion tag  ->  Advanced Settings  ->  Tag Sequencing
  Fire a tag before this tag fires:  Conversion Linker

Stop heavy third-party tags from slowing the first paint

When this happens

Chat widgets, heatmaps and other non-critical scripts load on All Pages and compete with your content for the browser's attention during the most important moment.

Do this

They almost never need to fire that early. Move non-essential tags to the Window Loaded trigger, or a short timer, so the page renders first and the extras come in once the user can already see something.

Trigger: Window Loaded   // instead of All Pages / DOM Ready
  Use for: chat, heatmaps, surveys, anything non-critical

Watch out

Keep anything that must run early, like Consent Mode defaults and the Conversion Linker, on All Pages. This is only for the nice-to-haves.