<!-- Cogny documentation. Canonical page: https://cogny.com/docs/gtm-event-tracking -->

# 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.

**Author:** Cogny Team  
**Published:** 2025-02-11  
**Updated:** 2026-10-09  
**Canonical:** https://cogny.com/docs/gtm-event-tracking

## 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:

```html
<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()`:

```javascript
// 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.

```javascript
// 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:

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

For nested objects, GTM performs a shallow merge:

```javascript
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:

```javascript
// 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.

```javascript
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):

```javascript
// 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`.

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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)

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

### scroll

```javascript
// 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

```javascript
// 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

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

### login

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

### search

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

### select_content

```javascript
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()`.

```javascript
// 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.

```javascript
// Get viewport width
function() {
  return window.innerWidth;
}
```

```javascript
// 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;
}
```

```javascript
// Get cookie value
function() {
  var name = 'user_segment';
  var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
  return match ? match[2] : undefined;
}
```

```javascript
// 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.

```html
<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

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:

```javascript
// 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:**

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

**Method 3 — via URL parameter:** Install the [Google Analytics Debugger](https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna) 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:

```javascript
// 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.

```javascript
// 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.

```javascript
// 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:

```html
<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:

```javascript
// 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](/docs/ga4-bigquery-export-schema) - Understand the raw data structure in BigQuery
- [Connect Your Data Sources](/docs/connect-your-data-sources) - Your BigQuery warehouse, the GA4 and Search Console links, and Bring Your Own Data
- [AI Report Generation](/docs/ai-report-generation-how-it-works) - 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:

```bash
# 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 →](https://github.com/cognyai/claude-code-marketing-skills/tree/main/skills/gtm-setup)

## Resources

- **GTM Developer Guide:** [developers.google.com/tag-platform/tag-manager](https://developers.google.com/tag-platform/tag-manager)
- **GA4 Event Reference:** [developers.google.com/analytics/devguides/collection/ga4/reference/events](https://developers.google.com/analytics/devguides/collection/ga4/reference/events)
- **GTM Community Gallery:** [tagmanager.google.com/gallery](https://tagmanager.google.com/gallery)
- **Consent Mode:** [developers.google.com/tag-platform/security/guides/consent](https://developers.google.com/tag-platform/security/guides/consent)
- **Claude Code Marketing Skills:** [github.com/cognyai/claude-code-marketing-skills](https://github.com/cognyai/claude-code-marketing-skills)

---

In this section:

- Conversion Tracking Debugger Reference: https://cogny.com/docs/conversion-tracking-debugger
- Core Web Vitals Reference: https://cogny.com/docs/core-web-vitals
- GA4 BigQuery Export Schema Reference: https://cogny.com/docs/ga4-bigquery-export-schema
- GA4 Event Implementation Reference: https://cogny.com/docs/ga4-event-implementation
- Google Ads Query Language (GAQL) Reference: https://cogny.com/docs/gaql-reference
- Google Ads Scripts Reference: https://cogny.com/docs/google-ads-scripts
- **GTM Event Tracking & Setup Reference** (this page): https://cogny.com/docs/gtm-event-tracking
- Meta Conversions API (CAPI) Setup Reference: https://cogny.com/docs/meta-conversions-api
- Schema.org Structured Data Reference: https://cogny.com/docs/structured-data-reference
- UTM Parameter Strategy & Builder Reference: https://cogny.com/docs/utm-strategy

Source page: https://cogny.com/docs/gtm-event-tracking  
All documentation: https://cogny.com/docs  
Cogny for agents: https://cogny.com/llms.txt
