Implementation Analytics: Comprehensive Data Layer Guide for GTM

This lesson focuses on understanding the dataLayer, crucial for linking a website with Google Tag Manager (GTM). It details the data layer’s mechanics, architecture principles, and implementation for both e-commerce and non-e-commerce scenarios.

What This Lesson Covers

The dataLayer is the contract between your website and GTM. Everything GTM knows about a page, a user, or an interaction comes from it. A poorly designed data layer produces unreliable analytics, broken tags, and constant firefighting. A well-designed one makes every future analytics and marketing requirement trivially easy to implement.

This lesson covers the data layer from first principles through to a complete GA4 ecommerce implementation, with full non-ecommerce patterns alongside.

Part 1: How the dataLayer Actually Works

The Mechanics

window.dataLayer is a plain JavaScript array. GTM installs a custom push() method on it at load time that intercepts every push and processes the object through the GTM rules engine.

// The dataLayer is initialised before the GTM snippet
window.dataLayer = window.dataLayer || [];
// Every push is an object — GTM processes it immediately
dataLayer.push({
event: 'page_view',
pageName: 'Home'
});
LineWhat it does
dataLayer.push({...})After GTM loads, this is intercepted — GTM reads the object, evaluates triggers, fires matching tags
event: 'page_view'The event key is special — it fires a Custom Event trigger in GTM
pageName: 'Home'Any other key becomes readable via a Data Layer Variable in GTM

State Persistence: the Merge Model

The dataLayer is cumulative and stateful. Each push merges into the running state rather than replacing it. GTM variables resolve against the merged state, not just the most recent push.

// Push 1 — page loads
dataLayer.push({ page: { type: 'article', category: 'Tech' } });
// Push 2 — user logs in 3 seconds later
dataLayer.push({ event: 'login', user: { id: 'usr_123', tier: 'pro' } });
// At this point GTM variables can read BOTH page.type AND user.tier
// even though they came from different pushes

The event Key: GTM’s Trigger Signal

Without the event key, GTM receives the push, merges the values into state, but fires no triggers. The event key is the only way to tell GTM “react to this push right now.”

// Silent push — values available but no trigger fires
dataLayer.push({ user: { tier: 'pro' } });
// Active push — fires Custom Event trigger for 'login'
dataLayer.push({ event: 'login', user: { tier: 'pro' } });

Part 2: Data Layer Architecture Principles

1. Initialise Before GTM

All pushes that supply page-level context must come before the GTM snippet. GTM processes the queue of pre-existing pushes immediately on load.

<head>
<!-- 1. dataLayer initialisation and page-level push -->
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
page: {
type: 'article',
category: 'Technology'
}
});
</script>
<!-- 2. GTM snippet — reads the push above immediately -->
<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-XXXXXX');</script>
</head>

Use Nested Objects for Clarity

Flat structures work but become hard to maintain. Nested objects group related properties and prevent naming collisions.

// Flat — works but gets messy at scale
dataLayer.push({
pageType: 'article',
pageCategory: 'Technology',
userTier: 'pro',
userId: 'usr_123'
});
// Nested — recommended
dataLayer.push({
page: {
type: 'article',
category: 'Technology'
},
user: {
id: 'usr_123',
tier: 'pro'
}
});

In GTM, nested keys are read with dot notation in the Data Layer Variable field: page.typeuser.tier.

Use snake_case for All Keys

GA4’s native schema uses snake_case. Using it throughout the data layer eliminates the need for transformation in GTM.

// Inconsistent casing — requires GTM transformations
dataLayer.push({ pageType: 'article', page_category: 'Tech', PageAuthor: 'Jo' });
// Consistent snake_case — maps directly to GA4 parameters
dataLayer.push({ page_type: 'article', page_category: 'Tech', page_author: 'Jo' });

Always Set Default Values

Push a complete object with default values at page load, even for properties that will be overridden later. This prevents GTM variables from being undefined on pages that do not update them.

// At page load — establish defaults for every variable
dataLayer.push({
page: {
type: 'other', // overridden on specific page types
category: '(not set)',
author: '(not set)'
},
user: {
login_status: 'logged_out',
tier: 'anonymous',
id: ''
}
});

Part 3: Non-Ecommerce Data Layer Patterns

3.1 Page Metadata

// Fires at page load, before GTM snippet
dataLayer.push({
page: {
name: 'Resources:Blog:How AI is Changing Analytics',
type: 'blog_post',
category: 'AI & Data',
subcategory: 'Analytics',
author: 'Sarah Chen',
publish_date: '2024-11-15',
word_count: 3200,
tags: ['ai', 'analytics', 'machine-learning'],
language: 'en-GB'
}
});
LineWhat it does
page.nameFull colon-delimited page name — maps to GA4 page_title override if needed
page.typeClassifies the page for trigger conditions in GTM
page.category / subcategoryContent taxonomy — powers GA4 content groupings
page.authorEnables author-level engagement reporting
page.publish_dateLets you analyse content freshness vs. engagement
page.word_countCorrelate with scroll depth and time on page
page.tagsArray — requires processing in GTM (join to string or use first value)

3.2 User / Authentication State – example

// Fires immediately if user is known at page load (server-rendered)
// Fires inside onLoginSuccess() callback if authenticated via AJAX
dataLayer.push({
event: 'user_detected',
user: {
login_status: 'logged_in',
id: 'usr_8a3f92k', // hashed internal ID
hashed_email: 'b94d27...c2', // SHA-256 for audience matching
account_type: 'organisation',
subscription_tier: 'pro',
account_age_days: 312,
crm_segment: 'power_user'
}
});

3.3 Form Interactions example

Track the full form funnel: start → field interaction → error → submit → success.

// Form started (first interaction with any field)
dataLayer.push({
event: 'form_start',
form_id: 'contact-us-main',
form_name: 'Contact Us',
form_type: 'contact'
});
// Field-level error
dataLayer.push({
event: 'form_error',
form_id: 'contact-us-main',
field_name: 'email',
error_type: 'invalid_format'
});
// Successful submission
dataLayer.push({
event: 'form_submit',
form_id: 'contact-us-main',
form_name: 'Contact Us',
form_type: 'contact',
lead_type: 'inbound_enquiry'
});

3.4 CTA and Internal Link Clicks – example

// Fires on click of a tracked element (via GTM Click trigger or custom listener)
dataLayer.push({
event: 'cta_click',
cta_text: 'Start Free Trial',
cta_location: 'homepage_hero',
cta_type: 'primary_button',
destination: '/signup/'
});

3.5 Video Engagement – example

// Video play
dataLayer.push({
event: 'video_start',
video_title: 'Product Demo — Reporting Dashboard',
video_provider: 'youtube',
video_id: 'dQw4w9WgXcQ',
video_duration: 243 // seconds
});
// Progress milestones
dataLayer.push({
event: 'video_progress',
video_title: 'Product Demo — Reporting Dashboard',
video_percent: 50, // 25, 50, 75, 90
video_current_time: 121
});
// Completion
dataLayer.push({
event: 'video_complete',
video_title: 'Product Demo — Reporting Dashboard',
video_id: 'dQw4w9WgXcQ'
});

Part 4: GA4 Ecommerce Data Layer

GA4’s ecommerce schema uses a strict structure. Every ecommerce event requires an items array. Each item in the array follows a fixed set of parameters. Deviating from the schema silently breaks GA4’s ecommerce reports.

The Items Array: Universal Structure

Every product in every ecommerce event uses the same object shape:

// Template — use this structure for every item in every ecommerce event
{
item_id: 'SKU-00441', // required
item_name: 'Alpine Parka', // required
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_category2: 'Jackets', // up to item_category5
item_variant: 'Navy / L',
item_list_id: 'cat_outerwear', // only on list events
item_list_name: 'Outerwear', // only on list events
index: 3, // position in list
price: 299.99, // unit price, not line total
quantity: 1,
coupon: 'WINTER20',
discount: 60.00 // absolute discount amount
}
ParameterRequiredNotes
item_idYesSKU or product ID — must be consistent across all events
item_nameYesHuman-readable product name
item_brandNoBrand name for brand-level reporting
item_categoryNoPrimary category — up to 5 levels via item_category2item_category5
item_variantNoSize, colour, configuration
item_list_idNoMachine-readable list identifier
item_list_nameNoHuman-readable list name
indexNoProduct position in a list (1-based)
priceNoUnit price after discount
quantityNoDefaults to 1 if omitted
couponNoItem-level coupon code
discountNoAbsolute discount amount per unit

4.1 view_item_list: Product List / Category Page

Fires when a user sees a list of products (category page, search results, recommendations).

dataLayer.push({ ecommerce: null }); // clear previous ecommerce object first
dataLayer.push({
event: 'view_item_list',
ecommerce: {
item_list_id: 'cat_outerwear',
item_list_name: 'Outerwear',
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_category2: 'Jackets',
item_variant: 'Navy / L',
item_list_id: 'cat_outerwear',
item_list_name: 'Outerwear',
index: 1,
price: 299.99
},
{
item_id: 'SKU-00389',
item_name: 'Fur Trim Ski Jacket',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_category2: 'Jackets',
item_list_id: 'cat_outerwear',
item_list_name: 'Outerwear',
index: 2,
price: 129.99
}
]
}
});

4.2 select_item: Product Click from a List

dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'select_item',
ecommerce: {
item_list_id: 'cat_outerwear',
item_list_name: 'Outerwear',
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_list_id: 'cat_outerwear',
item_list_name: 'Outerwear',
index: 1,
price: 299.99
}
]
}
});

4.3 view_item: Product Detail Page

dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'view_item',
ecommerce: {
currency: 'GBP',
value: 299.99, // unit price of the item being viewed
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_category2: 'Jackets',
item_variant: 'Navy / L',
price: 299.99,
quantity: 1
}
]
}
});

4.4 add_to_cart and remove_from_cart

// Add to cart — fires on button click, not on page load
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'add_to_cart',
ecommerce: {
currency: 'GBP',
value: 299.99,
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_variant: 'Navy / L',
price: 299.99,
quantity: 1
}
]
}
});
// Remove from cart — fires when item is removed from cart page
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'remove_from_cart',
ecommerce: {
currency: 'GBP',
value: 299.99,
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
price: 299.99,
quantity: 1
}
]
}
});

4.5 view_cart: Cart Page

dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'view_cart',
ecommerce: {
currency: 'GBP',
value: 429.98, // total cart value
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
price: 299.99,
quantity: 1
},
{
item_id: 'SKU-00389',
item_name: 'Fur Trim Ski Jacket',
price: 129.99,
quantity: 1
}
]
}
});

4.6 begin_checkout

dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'begin_checkout',
ecommerce: {
currency: 'GBP',
value: 429.98,
coupon: 'WINTER20', // cart-level coupon if applied
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
price: 299.99,
quantity: 1,
coupon: 'WINTER20',
discount: 60.00
},
{
item_id: 'SKU-00389',
item_name: 'Fur Trim Ski Jacket',
price: 129.99,
quantity: 1
}
]
}
});

4.7 add_shipping_info and add_payment_info

// Fires when user selects a shipping method and proceeds
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'add_shipping_info',
ecommerce: {
currency: 'GBP',
value: 429.98,
shipping_tier: 'standard_3-5_days',
items: [ /* same items array */ ]
}
});
// Fires when user selects a payment method and proceeds
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'add_payment_info',
ecommerce: {
currency: 'GBP',
value: 429.98,
payment_type: 'credit_card',
items: [ /* same items array */ ]
}
});

4.8 purchase: Order Confirmation

The most important ecommerce event. Every parameter matters.

dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'ORD-2024-98765', // unique, required
currency: 'GBP',
value: 409.98, // revenue AFTER discount, BEFORE tax/shipping
tax: 68.33,
shipping: 9.99,
coupon: 'WINTER20',
affiliation: 'Online Store',
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
item_variant: 'Navy / L',
price: 239.99, // price after item-level discount
quantity: 1,
coupon: 'WINTER20',
discount: 60.00
},
{
item_id: 'SKU-00389',
item_name: 'Fur Trim Ski Jacket',
item_brand: 'NorthRidge',
item_category: 'Outerwear',
price: 129.99,
quantity: 1
}
]
}
});

4.9 refund

// Full refund
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'refund',
ecommerce: {
transaction_id: 'ORD-2024-98765',
currency: 'GBP',
value: 409.98 // full order value
}
});
// Partial refund — specify only refunded items
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'refund',
ecommerce: {
transaction_id: 'ORD-2024-98765',
currency: 'GBP',
value: 239.99, // partial amount refunded
items: [
{
item_id: 'SKU-00441',
item_name: 'Alpine Parka',
price: 239.99,
quantity: 1
}
]
}
});

Part 5: GTM Variable Configuration

For every ecommerce.* key you push, create a corresponding Data Layer Variable in GTM:

GTM Variable NameData Layer KeyUsed In
DLV - Ecommerce Itemsecommerce.itemsAll ecommerce event tags
DLV - Transaction IDecommerce.transaction_idPurchase tag
DLV - Valueecommerce.valueAll ecommerce event tags
DLV - Currencyecommerce.currencyAll ecommerce event tags
DLV - Shippingecommerce.shippingPurchase tag
DLV - Taxecommerce.taxPurchase tag
DLV - Couponecommerce.couponCheckout and purchase tags
DLV - Payment Typeecommerce.payment_typeadd_payment_info tag
DLV - Shipping Tierecommerce.shipping_tieradd_shipping_info tag

In each GA4 Event tag in GTM, map these variables to GA4 parameters:

GA4 Event Tag: purchase
Event Name: purchase

Event Parameters:

  • transaction_id → {{DLV – Transaction ID}}
  • value → {{DLV – Value}}
  • currency → {{DLV – Currency}}
  • tax → {{DLV – Tax}}
  • shipping → {{DLV – Shipping}}
  • coupon → {{DLV – Coupon}}
  • items → {{DLV – Ecommerce Items}}

Part 6: The ecommerce: null Pattern Explained

// Always clear before a new ecommerce push
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'view_item',
ecommerce: { /* new data */ }
});

Without this clear, the dataLayer’s merge model means a purchase event’s items array is still readable after the purchase event has fired and can contaminate a subsequent view_item event on the same page if the user navigates back without a full reload.

Common Pitfalls

1. Forgetting ecommerce: null before ecommerce pushes Items from a previous product view persist in the merged dataLayer state. A purchase event tag reading ecommerce.items gets the items from the last view_item instead of the actual cart.

2. Pushing ecommerce data after the GTM snippet with no event key GTM reads the push but no trigger fires the GA4 event tag never executes.

3. Using value as the unit price instead of the total For add_to_cart with quantity 3 at £29.99 each, value must be 89.97, not 29.99. GA4 uses value for revenue metrics.

4. Inconsistent item_id across events If view_item uses 'SKU-00441' but purchase uses '441', GA4 cannot stitch the product journey. Use the same ID format everywhere.

5. Not clearing ecommerce: null on SPAs In single-page apps, the page does not reload between views. Without the null clear, every subsequent ecommerce event carries forward items from all previous ones.

6. Pushing the entire product catalogue into view_item_list If your category page loads 200 products, sending 200 items in the items array creates an enormous image request that may be truncated. Send only what is visible in the viewport, or the first page of results.

7. Using value inclusive of tax and shipping on purchase GA4 convention: value = revenue after discounts, before tax and shipping. Tax and shipping go in their own fields. Including them in value inflates revenue metrics.

8. Triggering view_item on page load via a GTM All Pages trigger If the All Pages trigger fires before the ecommerce push, the GA4 tag sends an event with an empty items array. Use a Custom Event trigger matching view_item instead.

9. Relying on GTM’s Auto-Event triggers to scrape ecommerce data from the DOM Reading price from a <span class="price"> element is fragile, locale-dependent, and breaks when the template changes. A proper dataLayer push is always preferable.

See you soon.

View Comments (6)

Leave a Reply

Prev Next

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Datalad - Data Science and ML

Subscribe now to keep reading and get access to the full archive.

Continue reading