Mastering Google's dataLayer.push()
If you’ve worked with Google Tag Manager, you’ve seen dataLayer.push() everywhere. But most implementations treat it as a black box — push some data in, hope GTM picks it up. Let’s fix that.
What is the dataLayer?
The dataLayer is a plain JavaScript array that acts as a message bus between your website and Google Tag Manager. When you call dataLayer.push(), you’re adding an object to this array. GTM watches it and reacts to new entries by evaluating triggers and firing tags.
<!-- GTM initializes this before its container snippet -->
<script>
window.dataLayer = window.dataLayer || [];
</script> The anatomy of a push
Every dataLayer.push() call merges the pushed object into GTM’s internal data model. This isn’t a simple array append — GTM processes each push through its internal state machine.
// Basic event push
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'T-12345',
value: 59.99,
currency: 'USD',
items: [{
item_id: 'SKU-001',
item_name: 'Marketing Platform Pro',
price: 59.99,
quantity: 1,
}],
},
}); The event key is special — it’s what triggers GTM’s tag evaluation. Without it, data is stored but nothing fires.
How GTM processes the dataLayer
Key detail: GTM uses a recursive merge strategy. Nested objects are merged, not replaced. This means pushing a partial update to a nested object extends it rather than overwriting it.
The merge trap
This merge behavior is the source of the most common dataLayer bug — stale data bleeding between events.
// Push 1: user adds item to cart
dataLayer.push({
event: 'add_to_cart',
ecommerce: {
items: [{
item_id: 'SKU-001',
item_name: 'Widget A',
price: 29.99
}],
},
});
// Push 2: user views a page (no ecommerce intent)
dataLayer.push({
event: 'page_view',
page_title: 'About Us',
});
// BUG: ecommerce.items still has Widget A
// in GTM's data model! The fix is simple — push a null clear before each ecommerce event:
// Clear stale ecommerce data first
dataLayer.push({ ecommerce: null });
// Then push the new event
dataLayer.push({
event: 'add_to_cart',
ecommerce: {
items: [{
item_id: 'SKU-002',
item_name: 'Widget B',
price: 49.99
}],
},
}); A reusable pushEvent helper
Untyped pushes are error-prone. Here’s a helper that auto-clears ecommerce data:
function pushEvent(event) {
window.dataLayer = window.dataLayer || [];
// Auto-clear ecommerce before ecommerce events
if (event.ecommerce) {
window.dataLayer.push({ ecommerce: null });
}
window.dataLayer.push(event);
}
// Usage — auto-clears ecommerce
pushEvent({
event: 'purchase',
ecommerce: {
transaction_id: 'T-99',
value: 120.0,
currency: 'USD',
items: [{
item_id: 'SKU-010',
item_name: 'Pro Plan',
price: 120.0,
quantity: 1
}],
},
}); The eventCallback pattern
Need to know when GTM finishes processing a push? Use eventCallback:
dataLayer.push({
event: 'outbound_link',
link_url: 'https://example.com',
eventCallback: function() {
// Fires after all tags have executed
window.location.href =
'https://example.com';
},
eventTimeout: 2000,
}); This is critical for outbound link tracking, form submissions, and any scenario where you need tags to fire before navigation.
Debugging tips
Three things I check on every GTM debug session:
- GTM Preview mode — use it, always. The Variables tab shows exactly what GTM’s data model contains after each push.
- Check for race conditions — if your SPA pushes events before GTM finishes loading, those events are queued but triggers may not match. Use
gtm.loadas a dependency. - Watch array merges — arrays in the dataLayer are replaced, not merged. Pushing
{ items: [B] }after{ items: [A] }results in[B], not[A, B]. This is actually the correct behavior for ecommerce, but catches people off guard elsewhere.
Try it yourself
Edit the code below and hit Run (or Ctrl+Enter) to see how dataLayer.push() works in real time. The sandbox simulates a dataLayer — push events, see them logged, and experiment with the merge trap.
Try reproducing the merge trap: push an ecommerce event, then push a non-ecommerce event, and notice how the ecommerce data persists. Then try the fix with dataLayer.push({ ecommerce: null }) before your next ecommerce push.
That’s the foundation. In future posts, I’ll cover server-side GTM, consent mode integration, and building a type-safe tracking SDK on top of the dataLayer.