Skip to content

Instantly share code, notes, and snippets.

@semanticentity
Created June 18, 2025 22:14
Show Gist options
  • Select an option

  • Save semanticentity/381628f1953508a28cf2e4dc5a95c041 to your computer and use it in GitHub Desktop.

Select an option

Save semanticentity/381628f1953508a28cf2e4dc5a95c041 to your computer and use it in GitHub Desktop.
Awesome Google Tag Manager Quick Wins

Awesome GTM Quick Wins

A collection of dead-simple, high-impact Google Tag Manager recipes for marketers, analysts, and developers who want results without touching the production codebase.

This is a plug-and-play library of GTM tags, triggers, and variables that will supercharge your analytics, unlock conversion insights, and make your clients or boss wonder how they ever lived without you.


How This Works (For Beginners)

Google Tag Manager (GTM) injects snippets of code (called "Tags") into your website based on rules (called "Triggers"). The magic happens when these tags push information into a special JavaScript array called the dataLayer.

The core pattern for most of these wins is:

  1. Create a Tag: In GTM, create a new Custom HTML Tag and paste the code from this guide.
  2. Create a Trigger: Set the Tag to fire on a specific trigger, usually All Pages or DOM Ready.
  3. Push to the dataLayer: The script runs and pushes a custom event, like dataLayer.push({'event': 'rage_click'});.
  4. Capture the Event: Create a new Trigger of type Custom Event, with the Event Name rage_click.
  5. Send to Analytics: Create a GA4 Event Tag that uses your new Custom Event Trigger. This sends the clean, newly captured data to Google Analytics.

Now, let's get to the wins.


Directory of Wins


Part 1: The Core Ten (Essential Wins)

1. Scroll Depth Tracking

  • πŸ”§ How to do it (No Code):
    • Trigger Type: Scroll Depth -> Settings: Vertical Scroll Depths, Percentages: 25, 50, 75, 90.
    • Tag Type: GA4 Event -> Event Name: scroll_depth -> Event Parameters: percent_scrolled : {{Scroll Depth Threshold}}.

2. Outbound Link Click Tracking

  • πŸ”§ How to do it (No Code):
    • Trigger Type: Click - Just Links -> Configuration: Some Link Clicks -> Click URL does not contain yourdomain.com.
    • Tag Type: GA4 Event -> Event Name: outbound_click -> Event Parameters: link_url : {{Click URL}}.

3. Marketing Parameter Persistence (UTM, GCLID, etc.)

  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger): Saves any marketing parameters from the URL to a 90-day cookie, making them available for attribution on later conversion events.
    <script>
      (function() {
        var params = new URLSearchParams(window.location.search);
        var paramKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid', 'msclkid'];
        var cookieDays = 90;
    
        paramKeys.forEach(function(key) {
          var value = params.get(key);
          if (value) {
            var expires = new Date();
            expires.setTime(expires.getTime() + (cookieDays * 24 * 60 * 60 * 1000));
            document.cookie = key + '=' + encodeURIComponent(value) + ';expires=' + expires.toUTCString() + ';path=/';
          }
        });
      })();
    </script>

4. YouTube Video Engagement

  • πŸ”§ How to do it (No Code):
    • Enable Variables: Go to Variables -> Built-In Variables and enable all Video variables.
    • Trigger Type: YouTube Video -> Capture: Check Start, Complete, Progress (e.g., 10, 25, 50, 75, 90).
    • Tag Type: GA4 Event -> Event Name: video_engagement -> Parameters: video_title: {{Video Title}}, video_status: {{Video Status}}.

5. Phone Number & Email Click Tracking

  • πŸ”§ How to do it (No Code):
    • Trigger 1 (Phone): Click - Just Links -> Some Link Clicks -> Click URL starts with tel:.
    • Trigger 2 (Email): Click - Just Links -> Some Link Clicks -> Click URL starts with mailto:.
    • Create a GA4 Event tag for each trigger (phone_click and email_click).

6. Form Abandonment Tracking

  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger): Fires when a user starts filling a form but leaves the page without submitting.
    <script>
      (function() {
        var formSelector = 'form'; // IMPORTANT: Change if needed, e.g., '#contact-form'
        var forms = document.querySelectorAll(formSelector);
        if (!forms.length) return;
    
        var interacted = false;
        var submitted = false;
    
        forms.forEach(function(form) {
          form.addEventListener('input', function() { interacted = true; }, { once: true });
          form.addEventListener('submit', function() { submitted = true; });
        });
    
        window.addEventListener('beforeunload', function() {
          if (interacted && !submitted) {
            dataLayer.push({'event': 'form_abandonment'});
          }
        });
      })();
    </script>

7. Exit Intent Tracking (Desktop)

  • πŸ”§ How to do it (Custom HTML Tag, DOM Ready Trigger): Fires when a desktop user's mouse moves towards the top of the browser window to close the tab.
    <script>
      document.addEventListener('mouseleave', function (e){
        if (e.clientY < 0) {
          dataLayer.push({'event': 'exit_intent_detected'});
        }
      }, {once: true}); // Fire only once per page view
    </script>

8. Text Copy Event

  • πŸ”§ How to do it (Custom HTML Tag, DOM Ready Trigger): Captures when a user copies text from your site.
    <script>
      document.addEventListener('copy', function(){
        var copiedText = window.getSelection().toString();
        if (copiedText.length > 5) { // Avoid capturing accidental short copies
            dataLayer.push({
                'event': 'text_copied',
                'copied_text': copiedText.substring(0, 100) // cap at 100 chars
            });
        }
      });
    </script>

9. Time on Page Milestones

  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger): Fires events at set time intervals to measure engagement.
    <script>
      setTimeout(function() { dataLayer.push({'event': 'engaged_user_30s'}); }, 30000);
      setTimeout(function() { dataLayer.push({'event': 'engaged_user_60s'}); }, 60000);
      setTimeout(function() { dataLayer.push({'event': 'engaged_user_120s'}); }, 120000);
    </script>

10. Track 404 "Page Not Found" Errors

  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger): Fires on pages where the title indicates a 404 error.
    <script>
      if (document.title.match(/404|Not Found|Page not found/i)) {
        dataLayer.push({
          event: 'page_not_found_404',
          broken_url: window.location.pathname + window.location.search,
          referrer: document.referrer
        });
      }
    </script>

Part 2: Behavioral & UX Signals

All scripts below should be in a Custom HTML tag, typically fired on a DOM Ready trigger.

  1. Rage Click Detector:
    <script>
      (function() {
        var clickCount = 0;
        var lastClickTime = 0;
        var clickThreshold = 5; // 5 clicks
        var timeThreshold = 1000; // within 1 second
    
        document.addEventListener('click', function(e) {
          var now = new Date().getTime();
          if (now - lastClickTime < timeThreshold) {
            clickCount++;
          } else {
            clickCount = 1;
          }
          lastClickTime = now;
          if (clickCount >= clickThreshold) {
            dataLayer.push({'event': 'rage_click', 'click_target': e.target.tagName});
            clickCount = 0; // Reset after firing
          }
        });
      })();
    </script>
  2. Idle User Detection:
    <script>
      (function() {
        var idleTime = 90000; // 90 seconds
        var idleTimer;
        function resetTimer() {
          clearTimeout(idleTimer);
          idleTimer = setTimeout(function() { dataLayer.push({'event': 'user_idle_90s'}); }, idleTime);
        }
        ['mousemove', 'keydown', 'scroll', 'click'].forEach(function(event) {
          window.addEventListener(event, resetTimer, true);
        });
        resetTimer();
      })();
    </script>
  3. Tab Visibility Tracking:
    <script>
      document.addEventListener('visibilitychange', function() {
        if (document.hidden) {
          dataLayer.push({'event': 'tab_hidden'});
        } else {
          dataLayer.push({'event': 'tab_visible'});
        }
      });
    </script>
  4. Scroll Up From Bottom:
    <script>
      (function() {
        var atBottom = false;
        window.addEventListener('scroll', function() {
          // Check if user is within 100px of the bottom
          var isAtBottom = (window.innerHeight + window.scrollY) >= document.body.offsetHeight - 100;
          if (isAtBottom && !atBottom) { atBottom = true; }
          // If they were at the bottom and have now scrolled up
          if (!isAtBottom && atBottom) {
            dataLayer.push({'event': 'scrolled_up_from_bottom'});
            atBottom = false; // Reset to allow re-firing
          }
        });
      })();
    </script>
  5. High-Speed Page Scrollers (Skimmers):
    <script>
      (function() {
        var startTime = Date.now();
        var fired = false;
        window.addEventListener('scroll', function() {
          var scrollPercent = (window.scrollY / (document.body.offsetHeight - window.innerHeight)) * 100;
          if (scrollPercent > 75 && !fired) {
            if (Date.now() - startTime < 5000) { // Scrolled 75% in under 5 seconds
              dataLayer.push({'event': 'fast_scroller'});
            }
            fired = true; // Only fire once per page
          }
        });
      })();
    </script>
  6. Browser Back Button Usage:
    <script>
      window.addEventListener("popstate", function() {
        dataLayer.push({event: "browser_back_button_used"});
      });
    </script>
  7. Text Highlighting:
    <script>
      document.addEventListener("mouseup", function() {
        var selectedText = window.getSelection().toString();
        if (selectedText.length > 10) { // Only for selections > 10 chars
          dataLayer.push({event: "text_highlighted", 'highlighted_text': selectedText.substring(0, 100)});
        }
      });
    </script>
  8. Dark Mode Detection:
    <script>
      if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        dataLayer.push({'event': 'dark_mode_user'});
      }
    </script>
  9. Ad Blocker Detection:
    <script>
      (function() {
        var testAd = document.createElement('div');
        testAd.innerHTML = '&nbsp;';
        testAd.className = 'adsbox';
        testAd.style.cssText = 'position:absolute; top:-9999px; left:-9999px;';
        document.body.appendChild(testAd);
        setTimeout(function() {
          if (testAd.offsetHeight === 0) {
            dataLayer.push({'event': 'ad_blocker_detected'});
          }
          document.body.removeChild(testAd);
        }, 100);
      })();
    </script>
  10. Password Visibility Toggle Click:
    <script>
      // Assumes a button with an attribute like [data-toggle="password"]
      document.addEventListener('click', function(e) {
        if (e.target.matches('[data-toggle="password"]')) {
          dataLayer.push({event: 'password_visibility_toggled'});
        }
      });
    </script>
  11. Accordion / FAQ Toggle Click
  • πŸ”§ How to do it (No Code):
    • Trigger: Click - All Elements -> Some Clicks -> Click Element matches CSS Selector .faq-question, .accordion-header. (IMPORTANT: Adjust selector).
    • Tag: GA4 Event -> Event Name: accordion_toggle -> Parameters: accordion_title: {{Click Text}}.
  1. Device Orientation Change
  • πŸ”§ How to do it (Custom HTML Tag, DOM Ready Trigger): Detects when a mobile user rotates their device.
    <script>
      window.addEventListener('orientationchange', function() {
        var orientation = (screen.orientation || {}).type || screen.mozOrientation || screen.msOrientation;
        dataLayer.push({
          event: 'device_orientation_changed',
          orientation: orientation.includes('portrait') ? 'portrait' : 'landscape'
        });
      });
    </script>

Part 3: eCommerce & Funnel Wins

These often require customizing CSS selectors to match your site's structure.

  1. Free Shipping Qualified:

    <script>
      (function(){
        var cartTotalEl = document.querySelector('.cart-total-price'); // IMPORTANT: Change to your selector
        var threshold = 100; // IMPORTANT: Change to your free shipping amount
        if (!cartTotalEl) return;
        var total = parseFloat(cartTotalEl.innerText.replace(/[^0-9.]/g, ''));
        if (total >= threshold) {
          dataLayer.push({event: 'free_shipping_qualified'});
        }
      })();
    </script>
  2. Product View Timer (Hover Intent):

    <script>
      (function() {
        var productCards = document.querySelectorAll('.product-card'); // IMPORTANT: Change to your selector
        productCards.forEach(function(card) {
          var hoverTimer;
          card.addEventListener('mouseenter', function() {
            hoverTimer = setTimeout(function() {
              var productName = card.querySelector('.product-title').innerText; // IMPORTANT: Change to your selector
              dataLayer.push({ event: 'product_hover_intent', product_name: productName });
            }, 3000); // 3 seconds
          });
          card.addEventListener('mouseleave', function() { clearTimeout(hoverTimer); });
        });
      })();
    </script>
  3. Coupon Code Used:

    <script>
      (function() {
        // IMPORTANT: Change selector to match your coupon input and apply button
        var couponInput = document.querySelector('#coupon-code, [name="coupon_code"]');
        var applyButton = document.querySelector('.apply-coupon-btn');
        if (!couponInput || !applyButton) return;
    
        applyButton.addEventListener('click', function() {
          if (couponInput.value) {
            dataLayer.push({ event: 'coupon_code_applied', coupon_code: couponInput.value });
          }
        });
      })();
    </script>
  4. Checkout Payment Field Focus:

    <script>
      // IMPORTANT: Change selector to match your credit card number field
      var paymentField = document.querySelector('#card-number, [name="cc-number"]');
      if (paymentField) {
        paymentField.addEventListener('focus', function() {
          dataLayer.push({event: 'checkout_payment_focus'});
        }, { once: true });
      }
    </script>
  5. Add to Wishlist Tracking:

    • πŸ”§ How to do it (No Code): Use a Click trigger.
    • Trigger Type: Click - All Elements -> Some Clicks -> Click Classes contains wishlist-btn.
    • Fire a GA4 Event named add_to_wishlist.
  6. Payment Method Chosen:

    <script>
      // IMPORTANT: Change selector to match your payment method radio buttons
      document.querySelectorAll('input[name="payment_method"]').forEach(function(radio) {
        radio.addEventListener('change', function(e) {
          dataLayer.push({ event: 'payment_method_selected', payment_method: e.target.value });
        });
      });
    </script>
  7. Shipping Option Detection:

    <script>
      // IMPORTANT: Change selector to match your shipping option radio buttons
      document.querySelectorAll('input[name="shipping_option"]').forEach(function(radio) {
        radio.addEventListener('change', function(e) {
          dataLayer.push({ event: 'shipping_option_selected', shipping_tier: e.target.value });
        });
      });
    </script>
  8. Quick Add vs. PDP Add to Cart:

    • πŸ”§ How to do it (No Code): Create two separate Click triggers on your "Add to Cart" button.
    • Trigger 1 (Quick Add): Click - All Elements -> Click Element matches CSS selector .collection-page .add-to-cart-btn.
    • Trigger 2 (PDP Add): Click - All Elements -> Click Element matches CSS selector .product-detail-page .add-to-cart-btn.
    • Create a GA4 Event for add_to_cart. Use a Lookup Table variable for a parameter like add_location. If {{Page Path}} contains /products/, return pdp. Otherwise, return quick_add.
  9. Returned to Pricing Page:

    <script>
      (function() {
        var pricingPath = '/pricing'; // IMPORTANT: Change to your pricing page path
        if (window.location.pathname.includes(pricingPath)) {
          if (sessionStorage.getItem('visited_pricing_page')) {
            dataLayer.push({event: 'returned_to_pricing'});
          }
          sessionStorage.setItem('visited_pricing_page', 'true');
        }
      })();
    </script>
  10. Remove from Cart Click:

    • πŸ”§ How to do it (No Code):
    • Trigger Type: Click - All Elements -> Configuration: Some Clicks -> Click Classes contains remove-from-cart.
    • Tag Type: GA4 Event -> Event Name: remove_from_cart.
  11. Multi-Step Form Progression:

    <script>
      (function() {
        // Assumes your form has 'next' buttons like <button data-step="2">Next</button>
        document.querySelectorAll('form [data-step]').forEach(function(button) {
          button.addEventListener('click', function(e) {
            dataLayer.push({
              event: 'form_step_completed',
              form_step: e.target.getAttribute('data-step')
            });
          });
        });
      })();
    </script>
  12. Form Completion Time:

    <script>
      (function() {
        var form = document.querySelector('form.track-this-form'); // IMPORTANT: Add class to form
        if (!form) return;
        var startTime;
        form.addEventListener('focusin', function() {
          if (!startTime) startTime = Date.now();
        }, { once: true });
        form.addEventListener('submit', function() {
          if (startTime) {
            var completionTime = (Date.now() - startTime) / 1000; // in seconds
            dataLayer.push({
              event: 'form_completion_time',
              seconds_to_complete: completionTime.toFixed(2)
            });
          }
        });
      })();
    </script>
  13. Cart Quantity Change

  • πŸ”§ How to do it (No Code):
    • Trigger: Click - All Elements -> Some Clicks -> Click Element matches CSS selector .cart-quantity-plus, .cart-quantity-minus. (IMPORTANT: Adjust selector).
    • Tag: GA4 Event -> Event Name: cart_quantity_changed. Use a Lookup Table variable on {{Click Classes}} to determine if it was an increase or decrease.
  1. Internal Promotions / Banner Click
  • πŸ”§ How to do it (No Code): Add a common class like internal-promo to your promotional banners.
    • Trigger: Click - All Elements -> Some Clicks -> Click Classes contains internal-promo.
    • Tag: GA4 Event -> Event Name: internal_promo_click -> Parameters: promo_id: {{Click ID}}, promo_url: {{Click URL}}.
  1. Filter or Sort Usage on Listing Pages
  • πŸ”§ How to do it (Custom HTML Tag, DOM Ready Trigger): Detects when a user interacts with sorting or filtering dropdowns/buttons.
    <script>
      // For a <select> dropdown filter/sort
      document.querySelectorAll('select.product-filter, select.product-sort').forEach(function(el) {
          el.addEventListener('change', function(e) {
              dataLayer.push({
                  event: 'product_list_filtered',
                  filter_type: e.target.name || 'unknown',
                  filter_value: e.target.value
              });
          });
      });
      // For filter links/buttons
      document.querySelectorAll('a.filter-option, button.filter-apply').forEach(function(el) {
          el.addEventListener('click', function(e) {
              dataLayer.push({
                  event: 'product_list_filtered',
                  filter_type: e.target.dataset.filterType || 'unknown',
                  filter_value: e.target.dataset.filterValue || e.target.innerText
              });
          });
      });
    </script>

Part 4: Ads & Attribution Arsenal

  1. Referrer Classification:

    <script>
      (function() {
        var ref = document.referrer;
        var source = 'direct';
        if (document.location.search.match(/gclid|fbclid|msclkid/)) { source = 'paid_search_social'; }
        else if (ref.match(/google|bing|yahoo|duckduckgo/)) { source = 'organic_search'; }
        else if (ref.match(/facebook|twitter|linkedin|instagram|t\.co/)) { source = 'organic_social'; }
        else if (ref && !ref.includes(window.location.hostname)) { source = 'referral'; }
        dataLayer.push({'event': 'source_classified', 'traffic_source_type': source, 'referrer_url': ref || 'direct'});
      })();
    </script>
  2. Affiliate Referrer Capture:

    <script>
      (function() {
        // IMPORTANT: Change 'aff' to your affiliate query parameter (e.g., 'ref', 'partner_id')
        var affId = new URLSearchParams(window.location.search).get('aff');
        if (affId) {
          document.cookie = 'affiliate_id=' + affId + ';path=/;max-age=2592000'; // 30 day cookie
          dataLayer.push({'event': 'affiliate_tracked', 'affiliate_id': affId});
        }
      })();
    </script>
  3. First Visit Timestamp:

    <script>
      (function(){
        var cookieName = 'first_visit_timestamp_ms';
        if (!document.cookie.includes(cookieName + '=')) {
          var d = new Date();
          d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000)); // 1 year expiry
          document.cookie = cookieName + '=' + Date.now() + ';expires=' + d.toUTCString() + ';path=/';
        }
      })();
    </script>
  4. Capture User ID from Cookie:

    • πŸ”§ How to do it (No Code): If your system stores a unique user ID in a cookie (e.g., user_id), you can capture it.
    • Variable Type: 1st Party Cookie -> Cookie Name: user_id. Name this variable {{Cookie - User ID}}.
    • In GA4 Tag: Add a user_id parameter and set its value to {{Cookie - User ID}}.
  5. Inferred Source via Content Type:

    <script>
      (function() {
        var path = window.location.pathname;
        var inferredSource = '';
        if (path.includes('/blog/')) { inferredSource = 'blog_content'; }
        else if (path.includes('/case-study/')) { inferredSource = 'case_study'; }
        else if (path.includes('/webinar/')) { inferredSource = 'webinar'; }
        else if (path.includes('/docs/')) { inferredSource = 'documentation'; }
    
        if (inferredSource) {
          dataLayer.push({'event': 'inferred_source', 'inferred_source_type': inferredSource});
        }
      })();
    </script>
  6. Compare Current vs. Prior Visit Source:

    <script>
      (function() {
        var currentSource = new URLSearchParams(window.location.search).get('utm_source');
        var previousSource = sessionStorage.getItem('last_utm_source');
        if (currentSource && previousSource && currentSource !== previousSource) {
          dataLayer.push({
            event: 'source_changed_in_session',
            previous_source: previousSource,
            current_source: currentSource
          });
        }
        if (currentSource) {
          sessionStorage.setItem('last_utm_source', currentSource);
        }
      })();
    </script>
  7. Redirect Chain Checker:

    <script>
      (function() {
        var knownRedirectors = ['t.co', 'lnkd.in', 'out.reddit.com']; // Common URL shorteners/redirectors
        var referrer = document.referrer;
        knownRedirectors.forEach(function(r) {
          if (referrer.includes(r)) {
            dataLayer.push({event: 'redirector_detected', 'redirecting_domain': r});
          }
        });
      })();
    </script>

Part 5: SEO & Content Monitoring

  1. Long Article Read Completion:

    • πŸ”§ How to do it (No Code): Use a Trigger Group to ensure a user both scrolls and spends time.
    • Trigger 1: Scroll Depth -> Vertical, 90%.
    • Trigger 2: Timer -> 90000 milliseconds (90s).
    • Trigger Group: Create a new trigger, select Trigger Group, and add Trigger 1 and Trigger 2. Fire your GA4 Event article_completed on this group trigger.
  2. Anchor Link Click Tracking:

    • πŸ”§ How to do it (No Code):
    • Trigger: Click - Just Links -> Some Link Clicks -> Click URL contains #.
    • Tag: GA4 Event -> Event Name: anchor_link_click -> Parameters: anchor_target: {{Click URL}}.
  3. Table of Contents Link Click:

    • πŸ”§ How to do it (No Code):
    • Trigger: Click - Just Links -> Some Link Clicks -> Click Element matches CSS Selector .table-of-contents a.
    • Tag: GA4 Event -> Event Name: toc_click -> Parameters: toc_text: {{Click Text}}.
  4. Track Search with No Results:

    <script>
      // IMPORTANT: Change selector to match your "no results" message element
      if (document.querySelector('.no-results-message, #search-results-empty')) {
        var query = new URLSearchParams(window.location.search).get('q'); // Or 's', 'query', etc.
        dataLayer.push({
          event: 'search_no_results',
          search_term: query || 'not_found_in_url'
        });
      }
    </script>
  5. Missing SEO Meta Tag Detector:

    <script>
      if (!document.querySelector("link[rel='canonical']")) {
        dataLayer.push({'event': 'seo_issue', 'error': 'missing_canonical_tag'});
      }
      if (!document.querySelector("meta[name='description']")) {
        dataLayer.push({'event': 'seo_issue', 'error': 'missing_meta_description'});
      }
      if (!document.querySelector("meta[property='og:image']")) {
        dataLayer.push({'event': 'seo_issue', 'error': 'missing_og_image'});
      }
    </script>
  6. Image Load Error Tracking:

    <script>
      document.addEventListener('error', function (e) {
        if (e.target.tagName.toLowerCase() === 'img') {
          dataLayer.push({
            'event': 'image_load_error',
            'image_src': e.target.src,
            'page_path': window.location.pathname
          });
        }
      }, true);
    </script>
  7. Print Page Event:

    <script>
      (function() {
          var beforePrint = function() { dataLayer.push({event: 'page_printed'}); };
          window.onbeforeprint = beforePrint;
          if (window.matchMedia) {
              window.matchMedia('print').addListener(function(mql) {
                  if (mql.matches) { beforePrint(); }
              });
          }
      })();
    </script>
  8. Detect Internal Links Opening in New Tab

  • πŸ”§ How to do it (No Code): This is generally bad for SEO and UX. This trigger finds them.
    • Variable: Create an Auto-Event Variable called Click Target Attribute. Set Variable Type to Element Attribute and Attribute Name to target.
    • Trigger: Click - Just Links -> Configuration: Some Link Clicks -> Click URL contains yourdomain.com AND {{Click Target Attribute}} equals _blank.
    • Tag: GA4 Event -> Event Name: seo_issue_internal_blank_link.

Part 6: Form, Cookie & Frontend Utility

  1. Capture Form Input to Session Storage:
    <script>
      // IMPORTANT: Change selector to be specific to the field you want to capture
      var emailInput = document.querySelector("input[type='email'], input[name='email']");
      if (emailInput) {
        emailInput.addEventListener("blur", function(e) {
          if (e.target.value) { sessionStorage.setItem(`form_email_capture`, e.target.value); }
        });
      }
    </script>
  2. Autofill Detection:
    <script>
      (function() {
        var form = document.querySelector('form'); // IMPORTANT: Or a more specific selector
        if (!form) return;
        setTimeout(function() {
          var hasTyped = false;
          form.addEventListener('keydown', function() { hasTyped = true; }, { once: true });
          form.querySelectorAll('input').forEach(function(input) {
            if (input.value && !hasTyped) {
              dataLayer.push({event: 'autofill_detected', 'form_id': form.id || 'unknown'});
              return;
            }
          });
        }, 500);
      })();
    </script>
  3. Paste Detection in Forms:
    <script>
      document.querySelectorAll('input, textarea').forEach(function(el) {
        el.addEventListener('paste', function(e) {
          dataLayer.push({
            event: 'paste_in_form',
            field_name: e.target.name || e.target.id || 'unknown'
          });
        });
      });
    </script>
  4. Touch vs. Mouse Device:
    <script>
      (function() {
        var detected = false;
        function detect(type) {
          if (detected) return;
          detected = true;
          dataLayer.push({event: 'device_type_detected', device_type: type});
        }
        window.addEventListener('touchstart', function() { detect('touch'); }, { once: true });
        window.addEventListener('mousemove', function() { detect('mouse'); }, { once: true });
      })();
    </script>
  5. Viewport Width Category:
    <script>
      (function() {
        var width = window.innerWidth;
        var category = 'desktop';
        if (width < 768) { category = 'mobile'; }
        else if (width >= 768 && width < 1024) { category = 'tablet'; }
        dataLayer.push({event: 'viewport_categorized', viewport_category: category});
      })();
    </script>
  6. Page Load Performance Marker:
    <script>
      (function() {
        // Use with a Window Loaded trigger for more accurate timing
        var perf = performance.getEntriesByType("navigation")[0];
        if (perf) {
          dataLayer.push({
            event: 'page_performance_metrics',
            page_load_time_ms: perf.domContentLoadedEventEnd,
            server_response_time_ms: perf.responseStart - perf.requestStart,
            total_page_time_ms: perf.loadEventEnd
          });
        }
      })();
    </script>
  7. Slow Connection Detection:
    <script>
      if (navigator.connection && (navigator.connection.effectiveType === '2g' || navigator.connection.effectiveType === 'slow-2g')) {
        dataLayer.push({event: 'slow_connection_detected', 'connection_type': navigator.connection.effectiveType});
      }
    </script>
  8. Battery Low Event:
    <script>
      if ('getBattery' in navigator) {
        navigator.getBattery().then(function(battery) {
          if (battery.level < 0.2 && !battery.charging) {
            dataLayer.push({event: "low_battery_detected"});
          }
        });
      }
    </script>
  9. In-App Browser Detection:
    <script>
      var ua = navigator.userAgent || navigator.vendor || window.opera;
      var browser = 'standard';
      if (/FBAN|FBAV/.test(ua)) { browser = 'facebook'; }
      else if (/Instagram/.test(ua)) { browser = 'instagram'; }
      else if (/Twitter/.test(ua)) { browser = 'twitter'; }
      if (browser !== 'standard') {
        dataLayer.push({event: 'in_app_browser_detected', 'in_app_browser': browser });
      }
    </script>
  10. Capture Data- Attributes on Click*
  • πŸ”§ How to do it (No Code): A powerful way to get context from clicks without coding. Example: <button data-component-name="Header CTA">Click Me</button>.
    • Variable: Create a new Auto-Event Variable. Variable Type: Element Attribute. Attribute Name: data-component-name. Name the GTM Variable {{Clicked Component Name}}.
    • Usage: You can now use {{Clicked Component Name}} as a parameter in any click-based GA4 event tag to know exactly which component was clicked.
  1. Cookie Consent Interaction
  • πŸ”§ How to do it (No Code): Track how users interact with your cookie banner.
    • Trigger 1 (Accept): Click - All Elements -> Some Clicks -> Click ID equals cookie-accept-button. (IMPORTANT: Adjust selector).
    • Trigger 2 (Decline): Click - All Elements -> Some Clicks -> Click ID equals cookie-decline-button. (IMPORTANT: Adjust selector).
    • Tag: Create a GA4 Event cookie_consent_interaction and use a Lookup Table on {{Click ID}} to set a parameter consent_choice to accepted or declined.

Part 7: Video, Media & Micro-Conversions

  1. Click to Reveal Content:
    • πŸ”§ How to do it (No Code):
    • Trigger: Click - All Elements -> Click Element matches CSS selector .reveal-phone-btn, .show-more.
    • Tag: GA4 Event -> Event Name: reveal_content_click.
  2. Social Share Button Clicked:
    • πŸ”§ How to do it (No Code):
    • Trigger: Click - Just Links -> Click URL matches RegEx (ignore case) facebook.com/sharer|twitter.com/intent/tweet|linkedin.com/share.
    • Tag: GA4 Event -> Event Name: social_share_click -> Parameters: social_platform: {{Click Hostname}}.
  3. Autoplay Blocked Detection:
    <script>
      var video = document.querySelector('video[autoplay]');
      if (video) {
        video.play().catch(function() {
          dataLayer.push({event: 'autoplay_blocked'});
        });
      }
    </script>
  4. Video Scrubbing Detection:
    <script>
      document.querySelectorAll('video').forEach(function(vid){
        vid.addEventListener('seeking', function(){
          dataLayer.push({event: 'video_scrubbed', video_source: vid.currentSrc});
        }, {once: true}); // Fire only once per video to avoid spamming
      });
    </script>
  5. Audio Player Started:
    <script>
      document.querySelectorAll('audio').forEach(function(aud){
        aud.addEventListener('play', function(){
          dataLayer.push({event: 'audio_player_started', audio_source: aud.currentSrc});
        }, {once: true});
      });
    </script>
  6. Podcast Episode Completed:
    <script>
      document.querySelectorAll('audio.podcast-player').forEach(function(aud){
        var fired = false;
        aud.addEventListener('timeupdate', function(e){
          if ( !fired && (e.target.currentTime / e.target.duration) > 0.90 ) {
            dataLayer.push({event: 'podcast_completed', audio_source: aud.currentSrc});
            fired = true;
          }
        });
      });
    </script>
  7. Newsletter Signup Intent:
    <script>
      // IMPORTANT: Change selector to match your newsletter email field
      var emailInput = document.querySelector('.newsletter-signup input[type="email"]');
      if (emailInput) {
        emailInput.addEventListener('blur', function(e) {
          if (e.target.value.includes('@')) { // Basic email validation
            dataLayer.push({event: 'newsletter_signup_intent'});
          }
        });
      }
    </script>
  8. Contact Modal Opened:
    • πŸ”§ How to do it (No Code): Use an Element Visibility trigger.
    • Trigger: Element Visibility -> Select your modal's main container (e.g., #contact-modal).
    • Tag: GA4 Event -> Event Name: contact_modal_viewed.
  9. Live Chat Engagement:
    <script>
      // Example for HubSpot. Check your chat provider's JS API docs.
      window.hsConversationsOnReady = [function() {
        window.hsConversations.on('conversationStarted', function() {
          dataLayer.push({'event': 'chat_conversation_started'});
        });
      }];
      // Example for Intercom
      if (typeof window.Intercom === 'function') {
        window.Intercom('onShow', function() { dataLayer.push({'event': 'chat_opened'}); });
      }
    </script>
  10. PDF Download & Document Tracking
  • πŸ”§ How to do it (No Code): Tracks downloads of common file types.
    • Trigger: Click - Just Links -> Some Link Clicks -> Click URL matches RegEx (ignore case) \.(pdf|docx?|xlsx?|pptx?|zip)$.
    • Tag: GA4 Event -> Event Name: file_download -> Parameters: file_extension: {{Click URL}} (use a RegEx Table variable to clean this), file_name: {{Click Text}}.
  1. Image Gallery Interaction
  • πŸ”§ How to do it (No Code): Track clicks on next/previous arrows in a product or blog image gallery.
    • Trigger: Click - All Elements -> Some Clicks -> Click Classes contains gallery-arrow. (IMPORTANT: Adjust selector).
    • Tag: GA4 Event -> Event Name: gallery_interaction. Use a parameter gallery_direction and populate it based on whether the {{Click Classes}} contains next or prev.

Part 8: Advanced Context & User Awareness

  1. First Visit vs. Returning Session:
    <script>
      var userType = sessionStorage.getItem('is_returning_session') ? 'returning_session' : 'new_session';
      dataLayer.push({event: 'session_type', user_session_type: userType});
      sessionStorage.setItem('is_returning_session', 'true');
    </script>
  2. Night Owl Detection (User's Local Time):
    <script>
      var hour = new Date().getHours();
      if (hour >= 22 || hour <= 4) { // 10 PM to 4 AM
        dataLayer.push({event: 'night_owl_session'});
      }
    </script>
  3. Typing Speed Metric:
    <script>
      // IMPORTANT: Change selector to target a specific textarea
      var input = document.querySelector('textarea.comment-box');
      if (input) {
        var keyCount = 0;
        var startTime;
        input.addEventListener('keydown', function() {
          if (!startTime) startTime = Date.now();
          keyCount++;
        });
        input.addEventListener('blur', function() {
          if (startTime && keyCount > 10) {
            var timeDiff = (Date.now() - startTime) / 1000;
            var wpm = ((keyCount / 5) / timeDiff) * 60; // Approximate WPM
            dataLayer.push({event: 'typing_speed_measured', 'typing_wpm': wpm.toFixed(0)});
          }
        });
      }
    </script>
  4. Captcha Render Detection:
    • πŸ”§ How to do it (No Code):
    • Trigger: Element Visibility -> CSS Selector: .g-recaptcha, .h-captcha.
    • Tag: GA4 Event -> Event Name: captcha_rendered.
  5. Session Page View Count:
    <script>
      var count = (parseInt(sessionStorage.getItem('page_view_count') || '0')) + 1;
      sessionStorage.setItem('page_view_count', count);
      dataLayer.push({'event': 'session_page_view', 'page_views_in_session': count});
    </script>
  6. First Click on Page:
    <script>
      document.body.addEventListener("click", function firstClick(e) {
        var targetId = e.target.id ? '#' + e.target.id : '';
        var targetClass = e.target.className ? '.' + e.target.className.split(' ').join('.') : '';
        dataLayer.push({event: "first_page_click", target_element: e.target.tagName + targetId + targetClass});
      }, { once: true, capture: true });
    </script>
  7. Entry Page Classifier:
    <script>
      (function() {
        if (sessionStorage.getItem('entry_page_classified')) return;
        var path = window.location.pathname;
        var type = 'other';
        if (path === '/') { type = 'homepage'; }
        else if (path.includes('/blog')) { type = 'blog'; }
        else if (path.includes('/product')) { type = 'pdp'; }
        else if (path.includes('/pricing')) { type = 'pricing'; }
        dataLayer.push({event: 'entry_page_classified', entry_page_type: type});
        sessionStorage.setItem('entry_page_classified', 'true');
      })();
    </script>
  8. Exit From Checkout Step:
    <script>
      // Tag 1: Fire this on each checkout step page
      // IMPORTANT: Create a Data Layer Variable `checkout_step` pushed by your backend.
      if ({{Data Layer Variable - checkout_step}}) {
          sessionStorage.setItem('last_checkout_step', {{Data Layer Variable - checkout_step}});
      }
    
      // Tag 2: Fire this tag on pages that are NOT part of checkout
      // (e.g., Page Path does not contain '/checkout')
      var exitStep = sessionStorage.getItem('last_checkout_step');
      if (exitStep) {
        dataLayer.push({event: 'checkout_abandoned', 'abandoned_at_step': exitStep});
        sessionStorage.removeItem('last_checkout_step'); // Clear it
      }
    </script>
  9. Weekend vs. Weekday Session
  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger):
    <script>
      var day = new Date().getDay();
      var sessionDayType = (day === 6 || day === 0) ? 'weekend' : 'weekday';
      dataLayer.push({
        event: 'session_day_type',
        day_type: sessionDayType
      });
    </script>
  1. Browser Language Detection
  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger): Capture the user's preferred browser language.
    <script>
      var userLang = navigator.language || navigator.userLanguage;
      if (userLang) {
          dataLayer.push({
              'event': 'browser_language_detected',
              'browser_language': userLang.toLowerCase()
          });
      }
    </script>
  1. PWA "Add to Homescreen" Prompt Event
  • πŸ”§ How to do it (Custom HTML Tag, DOM Ready Trigger): For Progressive Web Apps, this fires when the browser makes the install prompt available.
    <script>
      window.addEventListener('beforeinstallprompt', function(e) {
        dataLayer.push({event: 'pwa_install_prompt_available'});
      });
    </script>

Part 9: Source, Journey & Attribution Mapping

  1. Click Path Journey Builder:
    <script>
      var path = sessionStorage.getItem("click_path_journey") || "";
      var currentPage = window.location.pathname;
      // Avoid adding the same page twice if reloaded
      if (!path.endsWith(currentPage)) {
        path += (path ? " > " : "") + currentPage;
      }
      sessionStorage.setItem("click_path_journey", path);
      // To use this: create a GTM JS Variable that reads sessionStorage.getItem("click_path_journey")
      // and include it with your conversion event.
    </script>
  2. Multi-Channel Page View Tracker:
    <script>
        var visitedPages = JSON.parse(sessionStorage.getItem('visited_page_types') || '{}');
        var path = window.location.pathname;
    
        if (path.includes('/blog/')) { visitedPages.blog = true; }
        else if (path.includes('/pricing')) { visitedPages.pricing = true; }
        else if (path.includes('/product/')) { visitedPages.product = true; }
    
        sessionStorage.setItem('visited_page_types', JSON.stringify(visitedPages));
        // To use: Create a JS variable that returns Object.keys(JSON.parse(sessionStorage.getItem('visited_page_types'))).length
        // This will give you the count of unique page *types* visited.
    </script>
  3. Affiliate ID Inference from Referrer:
    <script>
      var ref = document.referrer;
      var inferredAffiliate = '';
      if (ref.includes('someknownaffiliate.com')) { inferredAffiliate = 'aff_blog_1'; }
      if (ref.includes('anotherpartner.com/reviews')) { inferredAffiliate = 'partner_review_2'; }
      if (inferredAffiliate) {
        dataLayer.push({event: 'inferred_affiliate', 'inferred_affiliate_id': inferredAffiliate});
      }
    </script>
  4. Referral Spoof Detection:
    <script>
      // Fires if a user has UTMs suggesting a source, but no actual referrer.
      // This can indicate "dark social" or manual URL entry.
      if (!document.referrer && window.location.search.includes('utm_source=')) {
        dataLayer.push({event: 'possible_referrer_spoof'});
      }
    </script>
  5. A/B Test Parameter Capture:
    <script>
      var testId = new URLSearchParams(window.location.search).get('test_id'); // IMPORTANT: Change to your test parameter
      if (testId) {
        dataLayer.push({event: 'ab_test_parameter_detected', 'test_variant_id': testId});
      }
    </script>
  6. Landing Page & Exit Page Pairing:
    <script>
      // Tag 1: On All Pages, set the landing page
      if (!sessionStorage.getItem('landing_page_url')) {
        sessionStorage.setItem('landing_page_url', window.location.href);
      }
      // Tag 2: Use a "beforeunload" custom event from a different tag, or trigger with exit intent
      // On your exit event tag (e.g. exit_intent_detected), add these parameters:
      // landing_page: {{JS - Read sessionStorage landing_page_url}}
      // exit_page: {{Page URL}}
    </script>
  7. Last Interaction before Conversion:
    <script>
      // Tag 1: On relevant interactive elements
      document.querySelectorAll('.cta-button, .demo-request-link').forEach(function(el){
        el.addEventListener('click', function(){
          sessionStorage.setItem('last_interaction_detail', el.innerText || 'cta_click');
        });
      });
      // Tag 2: On your Thank You / Conversion page
      var lastInteraction = sessionStorage.getItem('last_interaction_detail');
      if (lastInteraction) {
        dataLayer.push({event: 'conversion', 'last_interaction_before_conv': lastInteraction});
      }
    </script>

Part 10: QA, Debugging & The Fun Stuff

  1. DataLayer Push Logger:
    <script>
      // Logs every dataLayer.push to the console for easy debugging.
      (function() {
        if (window.dataLayer && typeof window.dataLayer.push === 'function') {
          var originalPush = dataLayer.push;
          dataLayer.push = function(obj) {
            console.log('GTM Push:', obj);
            return originalPush.apply(dataLayer, arguments);
          };
        }
      })();
    </script>
  2. DOM Mutation Logger (Fire Once):
    <script>
      // Detects if the page content changes dynamically after initial load (e.g., in a Single Page App).
      var observer = new MutationObserver(function() {
        dataLayer.push({event: "dom_mutated"});
        observer.disconnect(); // Fire once to prevent loops
      });
      observer.observe(document.body, {childList: true, subtree: true});
    </script>
  3. External Script Load Watcher:
    <script>
      // Example: wait for Stripe.js to become available.
      var scriptName = 'Stripe'; // The global object name (e.g., 'jQuery', 'HubSpot')
      var checkInterval = setInterval(function() {
        if (window[scriptName]) {
          clearInterval(checkInterval);
          dataLayer.push({event: scriptName.toLowerCase() + '_js_loaded'});
        }
      }, 500);
      setTimeout(function(){ clearInterval(checkInterval); }, 10000); // Stop after 10s
    </script>
  4. Duplicate GTM Container Detector:
    <script>
      var gtmScripts = Array.from(document.querySelectorAll('script')).filter(function(s) {
        return s.src.includes('googletagmanager.com/gtm.js');
      });
      if (gtmScripts.length > 1) {
        console.error('GTM DUPLICATE TAG DETECTED!', gtmScripts.map(s => s.src));
        dataLayer.push({event: 'duplicate_gtm_tag_error'});
      }
    </script>
  5. Konami Code Easter Egg:
    <script>
      var kkeys = [];
      var konami = 'ArrowUp,ArrowUp,ArrowDown,ArrowDown,ArrowLeft,ArrowRight,ArrowLeft,ArrowRight,b,a';
      document.addEventListener('keydown', function(e) {
        kkeys.push(e.key);
        if (kkeys.toString().indexOf(konami) >= 0) {
          dataLayer.push({'event': 'konami_code_activated'});
          alert('Konami Code Activated!');
          kkeys = [];
        }
        if (kkeys.length > 20) { kkeys.shift(); }
      });
    </script>
  6. Console Log Easter Egg:
    <script>
      console.log("%cπŸ‘‹ Psst... We're watching. And we're hiring!", "color: #ff6f61; font-size: 16px; font-weight: bold;");
    </script>
  7. Hidden Message in Page Source (Not a GTM Tag):
    • πŸ”§ How to do it: This isn't a GTM tag, but a fun "win" for developers. Add this directly into your website's <head> or <body> template code.
    <!--
      Hi there, fellow data nerd. You found our little secret.
      We take our tracking seriously. If you do too, check out our careers page.
      πŸ‘‹
    -->
  8. JavaScript Error Tracking
  • πŸ”§ How to do it (Custom HTML Tag, All Pages Trigger): Automatically captures unhandled JavaScript errors and pushes them to the data layer for debugging in GA4.
    <script>
      window.addEventListener('error', function(e) {
        dataLayer.push({
          event: 'js_error',
          error_message: e.message,
          error_filename: e.filename,
          error_lineno: e.lineno,
          error_colno: e.colno
        });
      });
    </script>
  1. GTM Container Blocked Detector
  • πŸ”§ How to do it (No GTM Tag - Pure HTML): This is a clever trick. Since this code is outside GTM, it can detect when GTM itself is blocked (e.g., by a browser extension). You place this right after your GTM snippet in the <body>.
    <!-- This goes in your website's template, NOT in GTM -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX"
    height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- GTM Blocked Fallback Pixel -->
    <script>window.gtmDidRun = true;</script>
    <img src="https://your.analytics-server.com/pixel.gif?event=gtm_blocked"
         style="display:none"
         onerror="this.style.display='none'"
         onload="var gtmCheck = setInterval(function(){if(window.gtmDidRun){this.parentNode.removeChild(this);clearInterval(gtmCheck);}}.bind(this), 500);">
    How it works: The script sets a flag gtmDidRun. The <img> tag attempts to load a pixel. Its onload event checks if the GTM script ran. If it did, the pixel is removed. If GTM is blocked, gtmDidRun is never set, the onload event never removes the pixel, and the pixel successfully sends a hit to your server indicating GTM was blocked.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment