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
| Variable | Description | Example |
|---|---|---|
event | Event name that triggers tag evaluation | 'purchase' |
gtm.start | Timestamp when GTM loaded | Auto-set by GTM |
gtm.uniqueEventId | Unique ID for each push | Auto-set by GTM |
gtm.element | DOM element that triggered the event | Auto-set for click/form triggers |
gtm.elementClasses | CSS classes of the triggering element | Auto-set |
gtm.elementId | ID of the triggering element | Auto-set |
gtm.elementUrl | URL of the triggering element | Auto-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-Type | Fires When | Use Case |
|---|---|---|
| Page View | gtm.js event (immediately) | GA4 config tag, consent checks |
| DOM Ready | gtm.dom event (DOM parsed) | Tags that read DOM elements |
| Window Loaded | gtm.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-Type | Fires When | Use Case |
|---|---|---|
| All Elements | Any element is clicked | Generic click tracking (buttons, divs, images) |
| Just Links | A link (<a>) is clicked | Outbound 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.
| Setting | Description |
|---|---|
| Selection Method | CSS Selector or Element ID |
| When to fire | Once per page, once per element, every time element appears |
| Minimum % visible | Percentage of element that must be visible (default 50%) |
| Minimum on-screen duration | How 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.
| Setting | Description |
|---|---|
| Event Name | Custom event name (gtm.timer by default) |
| Interval | Milliseconds between firings |
| Limit | Maximum number of times to fire (blank = unlimited) |
| Enable When | Condition 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.
| Setting | Description |
|---|---|
| Vertical/Horizontal | Which scroll direction to track |
| Percentages | Comma-separated values (e.g., 25, 50, 75, 90) |
| Pixels | Absolute 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).
| Setting | Description |
|---|---|
| Capture Start | Fire when video starts playing |
| Capture Complete | Fire when video ends |
| Capture Pause/Seeking/Buffering | Fire on player state changes |
| Capture Progress | Fire at percentage thresholds (10%, 25%, 50%, 75%) |
| Add JavaScript API support | Auto-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.
| Setting | Description |
|---|---|
| Wait for Tags | Delay form submission until tags fire (max 2000ms) |
| Check Validation | Only 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.
| Setting | Description |
|---|---|
| Event | Fires 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.
| Setting | Description |
|---|---|
| Selection Method | CSS Selector or ID |
| Element Selector | CSS selector string or element ID |
| Attribute Name | Attribute 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 Type | Example for https://example.com:443/path?q=test#section |
|---|---|
| Full URL | https://example.com:443/path?q=test#section |
| Protocol | https |
| Host Name | example.com |
| Port | 443 |
| Path | /path |
| Query | q=test (set Query Key to q to get just test) |
| Fragment | section |
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 |
/pricing | Pricing |
/blog | Blog |
/contact | Contact |
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).
| Setting | Description |
|---|---|
| Measurement ID | G-XXXXXXXXXX (use a Constant variable) |
| Send page_view | Enable to auto-send page_view events |
| Fields to Set | Additional parameters sent with every event |
| User Properties | User-scoped dimensions sent with every event |
Fields to Set examples:
| Field Name | Value |
|---|---|
debug_mode | true (enables DebugView in GA4) |
send_page_view | false (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).
| Setting | Description |
|---|---|
| Configuration Tag | Select your GA4 Config tag (or enter Measurement ID) |
| Event Name | The event name sent to GA4 (e.g., generate_lead) |
| Event Parameters | Key-value pairs sent with the event |
Event Parameters example (for purchase):
| Parameter Name | Value |
|---|---|
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:
| Setting | Description |
|---|---|
| Support document.write | Enable 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.
| Setting | Description |
|---|---|
| Image URL | Full URL including query parameters |
| Cache Buster | Append a random query parameter to prevent caching |
| Enable cache busting | Recommended: 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
| Variable | Description | Example Value |
|---|---|---|
Click Element | DOM element that was clicked | <a href="/signup"> |
Click Classes | CSS classes of the clicked element | btn btn-primary cta |
Click ID | ID attribute of the clicked element | signup-button |
Click Target | Target attribute of the clicked link | _blank |
Click URL | href of the clicked link | https://example.com/signup |
Click Text | Text content of the clicked element | Sign Up Now |
Form Variables
| Variable | Description | Example Value |
|---|---|---|
Form Element | The form DOM element | <form id="contact"> |
Form Classes | CSS classes of the form | contact-form validated |
Form ID | ID attribute of the form | contact |
Form Target | Target attribute of the form | _self |
Form URL | Action URL of the form | /api/submit |
Form Text | Text of the submit button | Submit |
Page Variables
| Variable | Description | Example Value |
|---|---|---|
Page URL | Full URL of the current page | https://example.com/products?cat=shoes |
Page Hostname | Hostname | example.com |
Page Path | Path portion of the URL | /products |
Referrer | Full referrer URL | https://google.com/search?q=shoes |
Utility Variables
| Variable | Description | Example Value |
|---|---|---|
Event | The event name from the dataLayer message | gtm.click |
Environment Name | Current GTM environment | Live |
Container ID | GTM container ID | GTM-XXXXXXX |
Container Version | Published version number | 42 |
HTML ID | The ID of the Custom HTML tag currently executing | tag_12345 |
Random Number | Random integer | 847293156 |
Debug Mode | Boolean indicating if Preview mode is active | true |
Scroll Variables
| Variable | Description |
|---|---|
Scroll Depth Threshold | The threshold value that was reached |
Scroll Depth Units | pixels or percent |
Scroll Direction | vertical or horizontal |
Video Variables
| Variable | Description |
|---|---|
Video Provider | youtube |
Video Status | start, pause, seeking, buffering, progress, complete |
Video URL | URL of the video |
Video Title | Title of the video |
Video Duration | Total duration in seconds |
Video Current Time | Current playback position in seconds |
Video Percent | Percentage of video watched |
Video Visible | Whether the video player is visible in the viewport |
Visibility Variables
| Variable | Description |
|---|---|
Percent Visible | Percentage of the element visible in viewport |
On-Screen Duration | How long the element has been visible (ms) |
Debug Mode
GTM Preview/Debug Mode
- Enter Preview mode: Click "Preview" in the GTM workspace. This opens Tag Assistant in a new tab.
- 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
| Element | Convention | Example |
|---|---|---|
| Tags | [Platform] - [Action] - [Detail] | GA4 - Event - Purchase |
| Triggers | [Type] - [Condition] | CE - form_submit |
| Variables | [Type prefix] - [Name] | DLV - Transaction ID |
| Custom Events | snake_case, GA4-compatible | generate_lead |
| Folders | By platform or function | GA4, 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_idvs.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:
| Setting | Value |
|---|---|
| Configure Tag | GA4 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
- GA4 BigQuery Export Schema - Understand the raw data structure in BigQuery
- BigQuery Service Account Setup - Connect your data
- AI Report Generation - How Cogny analyzes your tracking data
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
Resources
- GTM Developer Guide: developers.google.com/tag-platform/tag-manager
- GA4 Event Reference: developers.google.com/analytics/devguides/collection/ga4/reference/events
- GTM Community Gallery: tagmanager.google.com/gallery
- Consent Mode: developers.google.com/tag-platform/security/guides/consent
- Claude Code Marketing Skills: github.com/cognyai/claude-code-marketing-skills