Loading tutorials…
Loading tutorials…
Every GTM tutorial assumes you know what dataLayer is. Most don't actually explain it. This one does — in plain English, with examples you can copy. No JavaScript prerequisites.
Who this is forMarketing operators who manage GTM but don't write code. If you've ever seen {{DLV - ecommerce.value}} in a GTM config and felt lost, this is the right starting point. After this you'll know what dataLayer is, how to inspect it, and how to push your own events.
What you'll need
Step 1
dataLayer is a JavaScript array that lives on every page where GTM is installed. It carries context (what page, what user, what just happened) from your site to GTM.
Imagine your site is a kitchen and GTM is the chef. The chef needs to know: what dish was ordered (event), what ingredients are at hand (variables), and when to start cooking (trigger).
dataLayer is the order ticket. Your site writes to dataLayer ("a customer just submitted a form with ID=contact-page"), and GTM reads from it ("got it — fire the lead-conversion tag").
Technically: dataLayer is window.dataLayer, a JavaScript array. Each entry is an object that describes something that just happened or some context that exists.
When GTM loads on the page, it scans dataLayer top-to-bottom for any events it should react to (via Triggers) and any values it should read into Variables.
Step 2
Open any page on your site, open DevTools Console, and type 'window.dataLayer' to see what's there.
Open your site in Chrome. Right-click → Inspect → Console tab.
Type: window.dataLayer (no quotes). Press Enter.
You should see an array of objects. Each object is one dataLayer push. Click each to expand.
On most sites, you'll see entries like: { event: 'gtm.js' } (when GTM loaded), { event: 'gtm.dom' } (DOM ready), { event: 'gtm.load' } (page fully loaded).
On WooCommerce sites with GTM4WP, or Shopify with Custom Pixels firing dataLayer, you'll also see entries like { event: "view_item", ecommerce: { value: 29.99, currency: "USD", items: [...] } }.
These are pushes. GTM reads them and fires tags based on their event name.
Step 3
Pushing to dataLayer is one line: window.dataLayer.push({event: 'event_name', ...other_data}). GTM listens and reacts.
Syntax: window.dataLayer = window.dataLayer || []; window.dataLayer.push({event: 'my_custom_event', key1: 'value1', key2: 'value2'});
The first line ensures dataLayer exists (creates an empty array if not). The second line pushes an object.
The 'event' key is special — GTM looks at this field to fire Custom Event triggers.
Other keys are arbitrary — you decide what context to pass. Common: form_id, product_id, user_logged_in, page_template.
Push order matters: if GTM has already loaded and you push an event, GTM picks it up immediately. If you push before GTM loads, the event waits in the array and GTM processes it when it loads.
Step 4
Variables → New → Data Layer Variable. Set Data Layer Variable Name to the path you want (e.g., 'ecommerce.value'). Use {{Variable Name}} in tags.
GTM → Variables → User-Defined → New → Variable Configuration → Choose Variable Type → Data Layer Variable.
Data Layer Variable Name: the path inside dataLayer. For top-level: 'form_id'. For nested: 'ecommerce.value' or 'user.email'.
Variable Name (in GTM): "DLV - Form ID" or "DLV - Purchase Value" — use a consistent prefix like "DLV -" so you spot them in variable lists.
Default Value: leave blank, or 'undefined,' or a sentinel like 'not_set' if you want graceful fallback.
In tags, reference with {{DLV - Form ID}} or {{DLV - Purchase Value}}. GTM resolves these at fire time.
Step 5
Push your own dataLayer event with one line. Use it on button clicks, modal opens, video plays — anywhere you want GTM to react.
Example: track when users click a "Get Started" button.
In your site code (HTML or via a developer): <button onclick="window.dataLayer = window.dataLayer || []; window.dataLayer.push({event: 'get_started_click', button_location: 'hero'});">Get Started</button>
In GTM: Triggers → New → Custom Event → Event Name: get_started_click → save.
Tags → New → GA4 Event → Event Name: get_started_clicked → Parameter: button_location = {{DLV - Button Location}} → Trigger: the custom event.
Save, publish, test. Click the button. GA4 → DebugView shows the event within seconds.
Why this pattern is durable: the dataLayer push is in your HTML, not your tag. If GTM changes, the push still happens. If the button moves to a new page, the push still happens. Decoupled.
Step 6
Three tools: DevTools Console, GTM Preview mode (Data Layer tab), and the dataLayer Inspector extension.
In DevTools Console, watch dataLayer in real time: dataLayer.push = (function(originalPush) { return function() { console.log('dataLayer push:', ...arguments); return originalPush.apply(this, arguments); }; })(dataLayer.push);. Paste this once per page load and every push will log to console.
In GTM Preview mode, the Data Layer tab shows every push that happened on the connected tab, with timestamps and values.
Install the "GTM Sonar" or "dataLayer Inspector+" Chrome extension for a sidebar view of dataLayer activity without console tinkering.
Most common issues: (1) push fires but key has a typo — GTM looks for "purchase_value", you pushed "purchaseValue"; (2) push fires before dataLayer is initialized — fix by wrapping in window.dataLayer = window.dataLayer || []; (3) values are objects when GTM expects primitives — flatten the object.
Common mistakes
Pushing camelCase when GTM expects snake_case (or vice versa)
What goes wrong: Your push has a key like "formId" but GTM Variable is configured for "form_id". Variable resolves to undefined. Tags fire with empty parameter values. Reports look fine until you check the parameter columns.
How to avoid: Pick one convention (snake_case is GA4-standard) and use it everywhere — in dataLayer pushes AND Variable configurations. Audit existing pushes for casing drift.
Pushing before window.dataLayer is initialized
What goes wrong: Your dataLayer.push() call runs before window.dataLayer = window.dataLayer || []; has executed. JavaScript error: 'dataLayer is not defined.' Event never fires.
How to avoid: Always include window.dataLayer = window.dataLayer || []; immediately before your push. Or place pushes after the GTM snippet, never before.
Pushing complex objects when GTM expects primitives
What goes wrong: You push {event: 'purchase', items: [{id: 1}, {id: 2}]}. GTM Variable for 'items' returns the array as an object. Your GA4 Event parameter ends up as '[object Object]' in reports.
How to avoid: For arrays, flatten or stringify. For nested objects, use dot notation in Variable Name: 'ecommerce.items.0.id' to access the first item's id. Or use Custom JavaScript Variables to transform.
Reusing event names across different pushes
What goes wrong: You push event='form_submit' from the contact form, the newsletter form, AND the demo request form. GTM fires the same tag for all three because the trigger doesn't distinguish. Reports merge.
How to avoid: Either use distinct event names (contact_submit, newsletter_submit, demo_submit) OR include a form_id key and use it in trigger conditions to differentiate.
Pushing data your tags never read
What goes wrong: You push 10 keys per event "just in case." dataLayer bloats. Page memory increases. Future operators wade through dead keys. Maintenance burden compounds.
How to avoid: Push only the keys you actually use in tags. Audit annually — pause Variables that haven't been referenced in 90 days, then remove the matching pushes.
Recap
Done — what's next
How to fire GA4 through Google Tag Manager — without double-counting
Read the next tutorial
Hand it off
Understanding dataLayer is the bridge between 'GTM operator' and 'GTM specialist.' The conceptual jump is small; the operational discipline of clean schemas, consistent naming, and durable pushes takes time. A GTM specialist will build the schema, document it, and train your team. Typically $400-800 one-time at $14-16/hr.
See specialist rates
You can use GTM with just Built-in Variables (Page URL, Click ID, Form ID, etc.) for simple tracking. dataLayer becomes necessary when you need context that isn't in DOM attributes — user properties, cart contents, login state. Most stacks beyond a brochure site need it.
Yes — server-side rendered code can inject dataLayer pushes as inline <script> tags. WordPress (PHP), Shopify (Liquid), Drupal (Twig), and React (via dangerouslySetInnerHTML) all support this. The push lives in HTML and GTM reads it client-side.
Most likely a case mismatch (camelCase vs snake_case) or a path mismatch (you pushed 'ecommerce.value' but configured Variable Name as 'ecommerce_value'). Use GTM Preview → Data Layer tab to see the exact push, then mirror the path in your Variable.
No — dataLayer is an append-only log. You can push a new event that 'resets' a value (e.g., {user_logged_in: false}), but you can't remove past entries. This is by design — keeps the history immutable for debugging.
Often yes. The dataLayer event is what triggers your GTM tag, which then fires the GA4 event. They can have the same name (e.g., dataLayer event 'purchase' → triggers a tag that fires GA4 event 'purchase'). It's a clean pattern that's easy to debug.
Google Tag Manager
Firing GA4 through GTM is the foundation of every modern analytics stack. Done right, it's one tag and a clean event map. Done wrong, you double-count every event and pollute six months of reports. This is the right way.
Google Tag Manager
The default Form Submission trigger handles 70% of forms. The other 30% (AJAX forms, React forms, Webflow forms) need a custom pattern. This walks through both, and which one you need.
Google Tag Manager
Server-side GTM is the difference between 'tracking works most of the time' and 'tracking works on iOS users behind ad blockers too.' It's not for everyone. But if you're past $5K/mo in ad spend, you're already losing money to client-side strip.
Google Tag Manager
If a tag isn't firing — or worse, firing when it shouldn't — Tag Assistant is the only honest source of truth. Most operators use it wrong. This walks through the workflow specialists actually use.
Google Tag Manager
DIY GTM works fine for simple stacks. It starts breaking down when you need GA4 + Meta + TikTok + Google Ads all firing accurately with deduplication. Here's the honest framework for when to hire.