← Back to Documentation
    DocumentationintegrationsFeb 11, 2025

    GTM Event Tracking & Setup Reference

    Complete reference guide to Google Tag Manager event tracking, including dataLayer fundamentals, trigger and variable types, GA4 tag configuration, debug mode, and implementation best practices.

    Overview

    Google Tag Manager (GTM) is a tag management system that lets you deploy and manage marketing and analytics tags on your website without modifying code directly. GTM uses a dataLayer as its communication bridge between your website and the tags you configure.

    This reference covers everything you need to implement robust event tracking through GTM, from dataLayer fundamentals to advanced debugging techniques.

    dataLayer Fundamentals

    What Is the dataLayer?

    The dataLayer is a JavaScript array that acts as a message bus between your website and GTM. When you push objects onto the dataLayer, GTM reads them and uses the data to fire triggers and populate variables.

    Initialization

    GTM automatically creates the dataLayer array when the container snippet loads. To ensure data pushed before GTM loads is not lost, declare it before the GTM snippet:

    <script>
      window.dataLayer = window.dataLayer || [];
    </script>
    
    <!-- Google Tag Manager -->
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
    <!-- End Google Tag Manager -->
    

    dataLayer.push() Syntax

    Every interaction with the dataLayer uses dataLayer.push():

    // Basic push — key-value pairs
    dataLayer.push({
      'event': 'custom_event_name',
      'key1': 'value1',
      'key2': 'value2'
    });
    

    The event key is special. When present, it tells GTM to evaluate triggers. Without it, data is stored but no triggers fire.

    // Push data without firing a trigger
    dataLayer.push({
      'user_type': 'premium',
      'user_id': '12345'
    });
    
    // Push data AND fire a trigger
    dataLayer.push({
      'event': 'user_login',
      'user_type': 'premium',
      'user_id': '12345'
    });
    

    Object Structure and Persistence

    The dataLayer merges objects. Later pushes override earlier values for the same key:

    dataLayer.push({ 'page_type': 'product' });
    dataLayer.push({ 'page_type': 'category' });
    // GTM now sees page_type = 'category'
    

    For nested objects, GTM performs a shallow merge:

    dataLayer.push({
      'user': {
        'name': 'John',
        'plan': 'free'
      }
    });
    
    dataLayer.push({
      'user': {
        'plan': 'premium'
      }
    });
    // GTM now sees user = { plan: 'premium' }
    // user.name is GONE — shallow merge replaced the entire 'user' object
    

    Clearing dataLayer Values

    To prevent stale data from persisting (critical for e-commerce events), use the null reset pattern:

    // Clear previous ecommerce data before pushing new data
    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'view_item',
      'ecommerce': {
        'items': [{
          'item_id': 'SKU-001',
          'item_name': 'Blue T-Shirt',
          'price': 29.99
        }]
      }
    });
    

    Standard dataLayer Variables

    VariableDescriptionExample
    eventEvent name that triggers tag evaluation'purchase'
    gtm.startTimestamp when GTM loadedAuto-set by GTM
    gtm.uniqueEventIdUnique ID for each pushAuto-set by GTM
    gtm.elementDOM element that triggered the eventAuto-set for click/form triggers
    gtm.elementClassesCSS classes of the triggering elementAuto-set
    gtm.elementIdID of the triggering elementAuto-set
    gtm.elementUrlURL of the triggering elementAuto-set

    Common dataLayer Events

    page_view

    Fired on every page load. For SPAs, fire manually on route change.

    dataLayer.push({
      'event': 'page_view',
      'page_location': window.location.href,
      'page_title': document.title,
      'page_referrer': document.referrer,
      'content_group': 'blog',
      'user_id': 'U-12345'  // if authenticated
    });
    

    For single-page applications (React, Next.js, Vue):

    // React Router example
    useEffect(() => {
      dataLayer.push({
        'event': 'page_view',
        'page_location': window.location.href,
        'page_title': document.title
      });
    }, [location.pathname]);
    

    purchase

    The most critical e-commerce event. Must include transaction_id and value.

    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'purchase',
      'ecommerce': {
        'transaction_id': 'T-20250211-001',
        'value': 149.97,
        'tax': 12.50,
        'shipping': 5.99,
        'currency': 'USD',
        'coupon': 'SUMMER20',
        'items': [
          {
            'item_id': 'SKU-001',
            'item_name': 'Blue T-Shirt',
            'affiliation': 'Online Store',
            'coupon': '',
            'discount': 0,
            'index': 0,
            'item_brand': 'BrandName',
            'item_category': 'Apparel',
            'item_category2': 'T-Shirts',
            'item_variant': 'Blue',
            'price': 29.99,
            'quantity': 2
          },
          {
            'item_id': 'SKU-045',
            'item_name': 'Running Shoes',
            'affiliation': 'Online Store',
            'item_brand': 'BrandName',
            'item_category': 'Footwear',
            'item_variant': 'Black/Size-10',
            'price': 89.99,
            'quantity': 1
          }
        ]
      }
    });
    

    add_to_cart

    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'add_to_cart',
      'ecommerce': {
        'currency': 'USD',
        'value': 29.99,
        'items': [{
          'item_id': 'SKU-001',
          'item_name': 'Blue T-Shirt',
          'item_brand': 'BrandName',
          'item_category': 'Apparel',
          'item_variant': 'Blue',
          'price': 29.99,
          'quantity': 1
        }]
      }
    });
    

    remove_from_cart

    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'remove_from_cart',
      'ecommerce': {
        'currency': 'USD',
        'value': 29.99,
        'items': [{
          'item_id': 'SKU-001',
          'item_name': 'Blue T-Shirt',
          'price': 29.99,
          'quantity': 1
        }]
      }
    });
    

    view_item

    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'view_item',
      'ecommerce': {
        'currency': 'USD',
        'value': 29.99,
        'items': [{
          'item_id': 'SKU-001',
          'item_name': 'Blue T-Shirt',
          'item_brand': 'BrandName',
          'item_category': 'Apparel',
          'item_category2': 'T-Shirts',
          'item_variant': 'Blue',
          'price': 29.99
        }]
      }
    });
    

    view_item_list

    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'view_item_list',
      'ecommerce': {
        'item_list_id': 'category_apparel',
        'item_list_name': 'Apparel',
        'items': [
          { 'item_id': 'SKU-001', 'item_name': 'Blue T-Shirt', 'index': 0, 'price': 29.99 },
          { 'item_id': 'SKU-002', 'item_name': 'Red T-Shirt', 'index': 1, 'price': 29.99 },
          { 'item_id': 'SKU-003', 'item_name': 'Green T-Shirt', 'index': 2, 'price': 24.99 }
        ]
      }
    });
    

    begin_checkout

    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'begin_checkout',
      'ecommerce': {
        'currency': 'USD',
        'value': 149.97,
        'coupon': 'SUMMER20',
        'items': [
          { 'item_id': 'SKU-001', 'item_name': 'Blue T-Shirt', 'price': 29.99, 'quantity': 2 },
          { 'item_id': 'SKU-045', 'item_name': 'Running Shoes', 'price': 89.99, 'quantity': 1 }
        ]
      }
    });
    

    form_submit

    dataLayer.push({
      'event': 'form_submit',
      'form_id': 'contact_form',
      'form_name': 'Contact Us',
      'form_destination': '/thank-you',
      'form_submit_text': 'Send Message'
    });
    

    Lead generation (generate_lead)

    dataLayer.push({
      'event': 'generate_lead',
      'currency': 'USD',
      'value': 50.00,
      'lead_source': 'contact_form',
      'form_id': 'demo_request'
    });
    

    scroll

    // Typically handled by GTM's built-in scroll trigger,
    // but can be pushed manually for custom thresholds:
    dataLayer.push({
      'event': 'scroll',
      'percent_scrolled': 90
    });
    

    Video events

    // video_start
    dataLayer.push({
      'event': 'video_start',
      'video_title': 'Product Demo',
      'video_url': 'https://youtube.com/watch?v=abc123',
      'video_provider': 'youtube',
      'video_duration': 180,
      'visible': true
    });
    
    // video_progress (at 25%, 50%, 75%)
    dataLayer.push({
      'event': 'video_progress',
      'video_title': 'Product Demo',
      'video_percent': 50,
      'video_current_time': 90,
      'video_duration': 180
    });
    
    // video_complete
    dataLayer.push({
      'event': 'video_complete',
      'video_title': 'Product Demo',
      'video_url': 'https://youtube.com/watch?v=abc123',
      'video_duration': 180
    });
    

    sign_up

    dataLayer.push({
      'event': 'sign_up',
      'method': 'google'  // or 'email', 'facebook', etc.
    });
    

    login

    dataLayer.push({
      'event': 'login',
      'method': 'google'
    });
    

    search

    dataLayer.push({
      'event': 'search',
      'search_term': 'blue t-shirt',
      'search_results_count': 42
    });
    

    select_content

    dataLayer.push({
      'event': 'select_content',
      'content_type': 'product',
      'content_id': 'SKU-001'
    });
    

    Trigger Types

    Triggers determine when tags fire. Every tag must have at least one trigger.

    Page View Triggers

    Fire when a page loads. Three sub-types:

    Sub-TypeFires WhenUse Case
    Page Viewgtm.js event (immediately)GA4 config tag, consent checks
    DOM Readygtm.dom event (DOM parsed)Tags that read DOM elements
    Window Loadedgtm.load event (all resources loaded)Tags that depend on images/scripts

    Configuration: Filter by Page Path, Page Hostname, or Page URL using contains, equals, matches RegEx.

    Click Triggers

    Two sub-types:

    Sub-TypeFires WhenUse Case
    All ElementsAny element is clickedGeneric click tracking (buttons, divs, images)
    Just LinksA link (<a>) is clickedOutbound link tracking, download tracking

    Built-in variables available: Click Element, Click Classes, Click ID, Click Target, Click URL, Click Text.

    Example filter: Fire only when Click ID equals cta-signup.

    Custom Event Trigger

    Fires when a specific event value is pushed to the dataLayer:

    Configuration: Set Event Name to match the value of the event key in your dataLayer.push().

    // This push...
    dataLayer.push({ 'event': 'form_submit' });
    
    // ...fires a Custom Event trigger where Event Name = "form_submit"
    

    Supports regex matching for Event Name (e.g., form_submit.* to match form_submit_contact, form_submit_newsletter).

    Element Visibility Trigger

    Fires when a specified element becomes visible in the viewport.

    SettingDescription
    Selection MethodCSS Selector or Element ID
    When to fireOnce per page, once per element, every time element appears
    Minimum % visiblePercentage of element that must be visible (default 50%)
    Minimum on-screen durationHow long element must be visible (in ms)

    Use cases: Lazy-loaded content impressions, ad viewability tracking, scroll-into-view CTAs.

    Timer Trigger

    Fires at a specified interval.

    SettingDescription
    Event NameCustom event name (gtm.timer by default)
    IntervalMilliseconds between firings
    LimitMaximum number of times to fire (blank = unlimited)
    Enable WhenCondition that must be true to start the timer

    Use case: Track time-on-page at intervals (e.g., fire every 30 seconds to measure engagement).

    Scroll Depth Trigger

    Fires when users scroll past specified thresholds.

    SettingDescription
    Vertical/HorizontalWhich scroll direction to track
    PercentagesComma-separated values (e.g., 25, 50, 75, 90)
    PixelsAbsolute pixel values (e.g., 250, 500, 1000)

    Built-in variables: Scroll Depth Threshold, Scroll Depth Units, Scroll Direction.

    YouTube Video Trigger

    Built-in trigger for embedded YouTube videos (via iframe API).

    SettingDescription
    Capture StartFire when video starts playing
    Capture CompleteFire when video ends
    Capture Pause/Seeking/BufferingFire on player state changes
    Capture ProgressFire at percentage thresholds (10%, 25%, 50%, 75%)
    Add JavaScript API supportAuto-enables the iframe API

    Built-in variables: Video Provider, Video Status, Video URL, Video Title, Video Duration, Video Current Time, Video Percent, Video Visible.

    Form Submission Trigger

    Fires when a form is submitted.

    SettingDescription
    Wait for TagsDelay form submission until tags fire (max 2000ms)
    Check ValidationOnly fire if browser validation passes

    Built-in variables: Form Element, Form Classes, Form ID, Form Target, Form URL, Form Text.

    History Change Trigger

    Fires when the URL fragment (#hash) changes or when pushState/replaceState is called. Essential for SPAs.

    SettingDescription
    EventFires on gtm.historyChange

    Built-in variables: New History Fragment, Old History Fragment, New History State, Old History State, History Source.

    Trigger Groups

    Combine multiple triggers so a tag fires only when all triggers in the group have fired at least once.

    Use case: Fire a tag only after both a page view AND a specific user interaction have occurred.

    Variable Types

    Variables provide dynamic values to tags and triggers.

    Data Layer Variable

    Reads a value from the dataLayer.

    Variable Name: dlv_user_id
    Data Layer Variable Name: user_id
    Data Layer Version: Version 2 (recommended)
    Default Value: (not set)
    

    For nested objects, use dot notation:

    Data Layer Variable Name: ecommerce.transaction_id
    

    JavaScript Variable

    Reads a global JavaScript variable.

    Variable Name: jsv_document_title
    Global Variable Name: document.title
    

    DOM Element Variable

    Reads an attribute or text from a DOM element.

    SettingDescription
    Selection MethodCSS Selector or ID
    Element SelectorCSS selector string or element ID
    Attribute NameAttribute to read (blank = innerText)
    Variable Name: dom_hero_heading
    Selection Method: CSS Selector
    Element Selector: h1.hero-title
    Attribute Name: (blank — reads text content)
    

    URL Variable

    Extracts components from the current page URL.

    Component TypeExample for https://example.com:443/path?q=test#section
    Full URLhttps://example.com:443/path?q=test#section
    Protocolhttps
    Host Nameexample.com
    Port443
    Path/path
    Queryq=test (set Query Key to q to get just test)
    Fragmentsection

    Constant Variable

    Stores a fixed value. Useful for IDs and configuration strings.

    Variable Name: const_ga4_measurement_id
    Value: G-XXXXXXXXXX
    

    Use case: Store your GA4 Measurement ID, Meta Pixel ID, or other configuration values in one place so you can update them across all tags by changing one variable.

    Custom JavaScript Variable

    Runs a JavaScript function and returns its value. Must be an anonymous function that returns a value.

    // Get viewport width
    function() {
      return window.innerWidth;
    }
    
    // Calculate cart total from dataLayer
    function() {
      var ecommerce = {{dlv_ecommerce}};
      if (ecommerce && ecommerce.items) {
        return ecommerce.items.reduce(function(sum, item) {
          return sum + (item.price * (item.quantity || 1));
        }, 0);
      }
      return 0;
    }
    
    // Get cookie value
    function() {
      var name = 'user_segment';
      var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
      return match ? match[2] : undefined;
    }
    
    // Classify page type from URL
    function() {
      var path = window.location.pathname;
      if (path === '/') return 'homepage';
      if (path.indexOf('/product/') === 0) return 'product';
      if (path.indexOf('/category/') === 0) return 'category';
      if (path.indexOf('/blog/') === 0) return 'blog';
      if (path.indexOf('/cart') === 0) return 'cart';
      if (path.indexOf('/checkout') === 0) return 'checkout';
      return 'other';
    }
    

    Lookup Table Variable

    Maps input values to output values. Takes another variable as input.

    Input (from Page Path)Output
    /Homepage
    /pricingPricing
    /blogBlog
    /contactContact

    Set Default Value for unmatched inputs.

    RegEx Table Variable

    Like Lookup Table but uses regex patterns for matching.

    Pattern (from Page Path)Output
    ^/$Homepage
    ^/product/.*Product Page
    ^/category/.*Category Page
    ^/blog/.*Blog Post
    ^/checkout.*Checkout

    Settings: Full Match vs. Partial Match, Capture Groups, Case Sensitivity.

    Tag Types

    GA4 Configuration Tag

    Sets up the GA4 measurement stream. Should fire on All Pages (Page View trigger).

    SettingDescription
    Measurement IDG-XXXXXXXXXX (use a Constant variable)
    Send page_viewEnable to auto-send page_view events
    Fields to SetAdditional parameters sent with every event
    User PropertiesUser-scoped dimensions sent with every event

    Fields to Set examples:

    Field NameValue
    debug_modetrue (enables DebugView in GA4)
    send_page_viewfalse (disable auto page_view if firing manually)
    user_id{{dlv_user_id}}

    GA4 Event Tag

    Sends a custom event to GA4. Requires a GA4 Configuration tag (or Measurement ID).

    SettingDescription
    Configuration TagSelect your GA4 Config tag (or enter Measurement ID)
    Event NameThe event name sent to GA4 (e.g., generate_lead)
    Event ParametersKey-value pairs sent with the event

    Event Parameters example (for purchase):

    Parameter NameValue
    transaction_id{{dlv_ecommerce.transaction_id}}
    value{{dlv_ecommerce.value}}
    currency{{dlv_ecommerce.currency}}
    items{{dlv_ecommerce.items}}

    Custom HTML Tag

    Runs arbitrary HTML/JavaScript. Used for third-party scripts, custom pixels, and advanced tracking.

    <script>
      // Example: Send event to a third-party analytics tool
      if (typeof thirdPartyAnalytics !== 'undefined') {
        thirdPartyAnalytics.track('conversion', {
          value: {{dlv_ecommerce.value}},
          order_id: {{dlv_ecommerce.transaction_id}}
        });
      }
    </script>
    

    Settings:

    SettingDescription
    Support document.writeEnable if the script uses document.write() (rare, avoid if possible)
    Inject in<head> or <body>

    Security note: Custom HTML tags can execute arbitrary code. Review all Custom HTML tags regularly and limit access to users who understand the security implications.

    Custom Image Tag

    Fires a pixel request (1x1 image). Used for conversion tracking pixels and simple server-side hits.

    SettingDescription
    Image URLFull URL including query parameters
    Cache BusterAppend a random query parameter to prevent caching
    Enable cache bustingRecommended: true
    Image URL: https://tracking.example.com/pixel?event=purchase&value={{dlv_ecommerce.value}}&order={{dlv_ecommerce.transaction_id}}
    

    Built-In Variables

    GTM provides pre-configured variables that you can enable in the Variables section.

    Click Variables

    VariableDescriptionExample Value
    Click ElementDOM element that was clicked<a href="/signup">
    Click ClassesCSS classes of the clicked elementbtn btn-primary cta
    Click IDID attribute of the clicked elementsignup-button
    Click TargetTarget attribute of the clicked link_blank
    Click URLhref of the clicked linkhttps://example.com/signup
    Click TextText content of the clicked elementSign Up Now

    Form Variables

    VariableDescriptionExample Value
    Form ElementThe form DOM element<form id="contact">
    Form ClassesCSS classes of the formcontact-form validated
    Form IDID attribute of the formcontact
    Form TargetTarget attribute of the form_self
    Form URLAction URL of the form/api/submit
    Form TextText of the submit buttonSubmit

    Page Variables

    VariableDescriptionExample Value
    Page URLFull URL of the current pagehttps://example.com/products?cat=shoes
    Page HostnameHostnameexample.com
    Page PathPath portion of the URL/products
    ReferrerFull referrer URLhttps://google.com/search?q=shoes

    Utility Variables

    VariableDescriptionExample Value
    EventThe event name from the dataLayer messagegtm.click
    Environment NameCurrent GTM environmentLive
    Container IDGTM container IDGTM-XXXXXXX
    Container VersionPublished version number42
    HTML IDThe ID of the Custom HTML tag currently executingtag_12345
    Random NumberRandom integer847293156
    Debug ModeBoolean indicating if Preview mode is activetrue

    Scroll Variables

    VariableDescription
    Scroll Depth ThresholdThe threshold value that was reached
    Scroll Depth Unitspixels or percent
    Scroll Directionvertical or horizontal

    Video Variables

    VariableDescription
    Video Provideryoutube
    Video Statusstart, pause, seeking, buffering, progress, complete
    Video URLURL of the video
    Video TitleTitle of the video
    Video DurationTotal duration in seconds
    Video Current TimeCurrent playback position in seconds
    Video PercentPercentage of video watched
    Video VisibleWhether the video player is visible in the viewport

    Visibility Variables

    VariableDescription
    Percent VisiblePercentage of the element visible in viewport
    On-Screen DurationHow long the element has been visible (ms)

    Debug Mode

    GTM Preview/Debug Mode

    1. Enter Preview mode: Click "Preview" in the GTM workspace. This opens Tag Assistant in a new tab.
    2. Navigate your site: Enter your site URL. A debug panel appears at the bottom showing:
      • Tags Fired and Tags Not Fired for each event
      • dataLayer state at each event
      • Variables and their resolved values
      • Console errors and warnings

    Checking the dataLayer in Browser Console

    Open DevTools (F12 or Cmd+Option+I) and run:

    // View entire dataLayer
    console.table(dataLayer);
    
    // View the most recent push
    console.log(dataLayer[dataLayer.length - 1]);
    
    // Filter for specific events
    dataLayer.filter(function(item) { return item.event === 'purchase'; });
    
    // Watch for new pushes in real time
    (function() {
      var originalPush = dataLayer.push;
      dataLayer.push = function() {
        console.log('dataLayer.push:', arguments[0]);
        return originalPush.apply(dataLayer, arguments);
      };
    })();
    

    GA4 DebugView

    Enable debug mode for your GA4 property to see events in real time:

    Method 1 — via GTM: Add debug_mode: true as a field in your GA4 Configuration tag.

    Method 2 — via dataLayer:

    dataLayer.push({
      'event': 'page_view',
      'debug_mode': true
    });
    

    Method 3 — via URL parameter: Install the Google Analytics Debugger Chrome extension.

    Then view events at: GA4 Admin > DebugView

    Common Debugging Techniques

    1. Verify tag firing order: In GTM Preview mode, click each event in the timeline and check which tags fired. If a tag depends on data from an earlier push, ensure the push happens before the trigger.

    2. Check variable resolution: Click "Variables" in the Preview panel for any event. Verify that Data Layer Variables resolve to expected values, not undefined.

    3. Test trigger conditions: If a tag does not fire, click "Tags Not Fired" and check the trigger conditions. One or more conditions may not be met.

    4. Network tab inspection: Open DevTools Network tab. Filter by collect to see GA4 hits, or filter by the relevant domain for other pixels. Verify the payload contains expected parameters.

    # GA4 hit example in Network tab:
    https://www.google-analytics.com/g/collect?v=2&tid=G-XXXXXXXX&en=purchase&ep.transaction_id=T-001&epn.value=149.97...
    

    5. Monitor dataLayer size: If your dataLayer grows very large (thousands of entries on long-lived SPAs), performance can degrade. Consider periodically slicing old entries.

    Best Practices

    Naming Conventions

    ElementConventionExample
    Tags[Platform] - [Action] - [Detail]GA4 - Event - Purchase
    Triggers[Type] - [Condition]CE - form_submit
    Variables[Type prefix] - [Name]DLV - Transaction ID
    Custom Eventssnake_case, GA4-compatiblegenerate_lead
    FoldersBy platform or functionGA4, Meta, Consent

    Common tag type prefixes: GA4, Meta, LinkedIn, HTML, IMG Common trigger type prefixes: PV (Page View), CE (Custom Event), CL (Click), EV (Element Visibility), SC (Scroll), FO (Form), TM (Timer) Common variable type prefixes: DLV (Data Layer Variable), JSV (JavaScript Variable), CJS (Custom JavaScript), CONST (Constant), DOM (DOM Element), URL (URL), LUT (Lookup Table), REGEX (RegEx Table)

    Folder Organization

    Organize your container with folders:

    GA4/
      GA4 - Config - All Pages
      GA4 - Event - Purchase
      GA4 - Event - Add to Cart
      GA4 - Event - Generate Lead
    Meta/
      Meta - Pixel - Base Code
      Meta - Event - Purchase
      Meta - Event - Lead
    Consent/
      HTML - Consent Banner Init
      HTML - Consent Mode Update
    Utility/
      HTML - DataLayer Enrichment
      HTML - Error Tracking
    

    Workspace Management

    • Default Workspace: Keep clean. Use for quick hotfixes only.
    • Feature Workspaces: Create a new workspace for each feature or campaign. Name them descriptively (e.g., Q1-2025-GA4-ecommerce-migration).
    • Review before publishing: Always use Preview mode in the workspace before creating a version.
    • Limit active workspaces: Merge or delete unused workspaces to avoid conflicts.

    Version Control

    • Create a version before and after significant changes.
    • Write descriptive version names and notes: "v42 - Added GA4 e-commerce tracking for checkout funnel"
    • Use version descriptions to document what changed and why.
    • Roll back to a previous version if issues arise post-publish.

    Tag Sequencing and Firing Priority

    • Use Tag Sequencing (tag settings > Advanced > Tag Sequencing) to ensure a setup tag fires before a dependent tag.
    • Tag firing priority: Higher number = fires first. Use when multiple tags share the same trigger and order matters.
    • Fire once per event vs. once per page: Choose the right firing option to prevent duplicate events.

    Consent Mode

    Implement Google Consent Mode v2 for GDPR/CCPA compliance:

    // Default consent state (before user choice)
    dataLayer.push({
      'event': 'consent_default',
      'consent': {
        'ad_storage': 'denied',
        'analytics_storage': 'denied',
        'ad_user_data': 'denied',
        'ad_personalization': 'denied',
        'functionality_storage': 'granted',
        'personalization_storage': 'denied',
        'security_storage': 'granted'
      }
    });
    
    // Update after user grants consent
    dataLayer.push({
      'event': 'consent_update',
      'consent': {
        'ad_storage': 'granted',
        'analytics_storage': 'granted',
        'ad_user_data': 'granted',
        'ad_personalization': 'granted'
      }
    });
    

    Common Mistakes

    1. Race Conditions with dataLayer

    Problem: Tags fire before the dataLayer contains the required data.

    // BAD: Tag fires on DOM Ready, but data is pushed asynchronously
    fetch('/api/user')
      .then(res => res.json())
      .then(user => {
        dataLayer.push({
          'event': 'user_data_ready',
          'user_id': user.id
        });
      });
    // If a tag triggers on DOM Ready and expects user_id, it will be undefined
    

    Fix: Use Custom Event triggers that fire only when the data is actually available, not Page View triggers that assume data is present.

    2. Incorrect Trigger Firing

    Problem: Tags fire on every page instead of specific pages.

    Fix: Always add trigger filters. For example, if a purchase confirmation tag should only fire on /thank-you, add a filter: Page Path equals /thank-you.

    3. Missing ecommerce Null Reset

    Problem: Stale ecommerce data from a previous push persists and contaminates the next event.

    // BAD: No null reset — previous ecommerce data leaks
    dataLayer.push({
      'event': 'add_to_cart',
      'ecommerce': { 'items': [{ 'item_id': 'SKU-001' }] }
    });
    // If a view_item push happened earlier with more fields,
    // those fields may still be present
    
    // GOOD: Always reset before ecommerce pushes
    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      'event': 'add_to_cart',
      'ecommerce': { 'items': [{ 'item_id': 'SKU-001' }] }
    });
    

    4. Duplicate Tags Firing

    Problem: The same event is sent multiple times.

    Causes:

    • Tag has multiple triggers (fires once per trigger)
    • Trigger conditions are too broad
    • Tag fires "Once per Event" but the event fires multiple times

    Fix: Check tag trigger configuration. Use "Once per Page" firing option if the tag should only fire once. Use Trigger Groups for complex conditions.

    5. Custom HTML Tags Breaking the Page

    Problem: JavaScript errors in Custom HTML tags block other tags or break site functionality.

    Fix: Always wrap Custom HTML in try/catch:

    <script>
      (function() {
        try {
          // Your tracking code here
          thirdPartySDK.track('event');
        } catch (e) {
          console.warn('GTM Custom HTML error:', e);
        }
      })();
    </script>
    

    6. Data Layer Variable Returns Undefined

    Common causes:

    • Variable name does not match the dataLayer key (case-sensitive)
    • Data was pushed after the trigger fired
    • Using Version 1 instead of Version 2 for the Data Layer Variable
    • Nested key path is wrong (e.g., ecommerce.items.0.item_id vs. ecommerce.items[0].item_id — GTM uses dot notation)

    7. SPA Virtual Page Views Not Tracked

    Problem: Route changes in single-page applications are not tracked because GTM's default Page View trigger only fires on full page loads.

    Fix: Use the History Change trigger for hash-based routing, or push a custom page_view event on every route change:

    // Next.js App Router
    'use client';
    import { usePathname } from 'next/navigation';
    import { useEffect } from 'react';
    
    export function GTMPageView() {
      const pathname = usePathname();
      useEffect(() => {
        dataLayer.push({
          'event': 'page_view',
          'page_location': window.location.href,
          'page_title': document.title
        });
      }, [pathname]);
      return null;
    }
    

    8. Cross-Domain Tracking Issues

    Problem: Users navigating between your domains are counted as new sessions.

    Fix: Configure cross-domain tracking in your GA4 Configuration tag:

    SettingValue
    Configure TagGA4 Config tag
    Fields to Set > linker{ 'domains': ['example.com', 'shop.example.com'] }

    Or configure in GA4 Admin > Data Streams > Configure tag settings > Configure your domains.

    9. Tag Firing on GTM Preview Traffic

    Problem: Your own preview/debug sessions inflate analytics data.

    Fix: Add an exception trigger or blocking trigger that prevents tags from firing when Debug Mode built-in variable equals true, or filter internal traffic in GA4 using a traffic_type parameter.

    Next Steps

    Claude Code Skill

    This GTM reference is also available as a free Claude Code skill — use it directly in your terminal:

    # Install
    curl -sSL https://raw.githubusercontent.com/cognyai/claude-code-marketing-skills/main/install.sh | bash
    
    # Use
    /gtm-setup                          # Full GTM reference overview
    /gtm-setup dataLayer purchase       # Show purchase event dataLayer.push()
    /gtm-setup triggers scroll          # Explain scroll depth trigger setup
    

    View on GitHub →

    Resources

    Ready for similar results?
    Start with Solo at $9/mo or talk to us about Cloud.
    ❯ get startedcompare plans →