{"id":912,"date":"2026-04-14T16:03:42","date_gmt":"2026-04-14T14:03:42","guid":{"rendered":"https:\/\/www.sabatka.net\/cs\/?p=912"},"modified":"2026-04-15T09:47:03","modified_gmt":"2026-04-15T07:47:03","slug":"datalayer-and-recursive-merge","status":"publish","type":"post","link":"https:\/\/www.sabatka.net\/en\/datalayer-and-recursive-merge\/","title":{"rendered":"dataLayer and recursive merge"},"content":{"rendered":"\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>dataLayer is a simple JavaScript array. Push an object, read it in a Google Tag Manager (GTM) variable, done. Nothing complicated about that.<\/p>\n\n\n\n<p>Right.<\/p>\n\n\n\n<p>If you&#8217;re designing dataLayer structures and haven&#8217;t heard of recursive merge yet, consider whether you want to keep reading. You&#8217;ll sleep worse.<\/p>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button scroll_to_subscribe\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.sabatka.net\/en\/kontakt\/\">Contact me<\/a><\/div>\n\n\n\n<div class=\"wp-block-button linkedinShare\"><a class=\"wp-block-button__link wp-element-button\">Share on LinkedIN<\/a><\/div>\n<\/div>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">dataLayer vs. GTM data model \u2014 not the same thing<\/h2>\n\n\n\n<p>Before we jump into examples, let&#8217;s clarify one fundamental thing. In dataLayer discussions, two terms are commonly confused:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>dataLayer<\/strong> \u2014 a JavaScript array (<code>Array<\/code>) you push objects into. It lives in the browser; you can inspect it in the console.<\/li>\n\n\n\n<li><strong>GTM data model<\/strong> (internal state) \u2014 an object GTM maintains internally. When you create a Data Layer Variable in GTM, you&#8217;re reading from this internal model \u2014 not directly from the dataLayer array.<\/li>\n<\/ul>\n\n\n\n<p>Here&#8217;s where it gets fun. When you call <code>dataLayer.push()<\/code>, GTM takes your object and <strong>merges it<\/strong> into its internal data model. And that merge isn&#8217;t a simple overwrite. It&#8217;s a <strong>recursive merge<\/strong>.<\/p>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">What is recursive merge<\/h2>\n\n\n\n<p>Recursive merge (deep merging) means GTM walks through the object structure level by level, merging values. Primitive values (string, number, boolean) get overwritten. But nested objects aren&#8217;t replaced as a whole \u2014 GTM descends into them and merges individual keys.<\/p>\n\n\n\n<p>Sounds harmless. Let&#8217;s see what it means in practice.<\/p>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">dataLayer etudes<\/h2>\n\n\n\n<p>I&#8217;ve prepared several scenarios. For each one, try to answer first \u2014 then read the solution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 1: Simple overwrite<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': 1});\ndataLayer.push({'x': 2});<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-1&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-1-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-1\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">> Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-1\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-1-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><code>x = 2<\/code><\/p>\n\n\n\n<p>No surprises. A primitive value simply gets overwritten. This works exactly as you&#8217;d expect.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 2: Nested object<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': {'a': 1} });\ndataLayer.push({'x': {'b': 2} });\n<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-2&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-2-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-2\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">> Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-2\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-2-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><code>x = {'a': 1, 'b': 2}<\/code><\/p>\n\n\n\n<p>Here&#8217;s where it starts. The second push <strong>didn&#8217;t overwrite<\/strong> the entire object <code>x<\/code>. GTM descended into it and added key <code>b<\/code> to the existing key <code>a<\/code>. The result is a merged object. If you expected <code>{'b': 2}<\/code>, you&#8217;re not alone. But recursive merge works differently \u2014 it walks the structure and merges, not overwrites.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 3: Array<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': &#91;1, 2, 3] });\ndataLayer.push({'x': &#91;4, 5] });<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-3&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-3-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-3\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">> Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-3\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-3-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><code>x = [4, 5, 3]<\/code><\/p>\n\n\n\n<p>The fun begins.<\/p>\n\n\n\n<p>In recursive merge, arrays behave like objects with numeric keys. Index 0 gets overwritten to <code>4<\/code>, index 1 to <code>5<\/code>, and index 2 stays <code>3<\/code> because the second push had no third element. The result isn&#8217;t <code>[4, 5]<\/code> (overwrite) nor <code>[1, 2, 3, 4, 5]<\/code> (concat). It&#8217;s a hybrid that makes no sense in any common programming context.<\/p>\n\n\n\n<p>Watch out.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 4: Product arrays<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': &#91;{'id': 1, 'name': 'Product 1'}] });\ndataLayer.push({'x': &#91;{'id': 2}] });<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-4&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-4-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-4\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">> Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-4\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-4-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><code>x = [{'id': 2, 'name': 'Product 1'}]<\/code><\/p>\n\n\n\n<p>This is the most practical etude. Recursive merge descended into the array (index 0), found an object inside \u2014 and merged it again. Result: the product with <code>id: 2<\/code> inherited <code>name: 'Product 1'<\/code> from the previous push. A frankenstein \u2014 an object describing a product that doesn&#8217;t exist. In e-commerce tracking, this is a nightmare. If you push events without clearing the data model, you might see a product with another product&#8217;s price or with the name of a previous cart item in your Google Analytics 4 (GA4) reports. And since it doesn&#8217;t throw an error, you might not notice for weeks \u2014 until the data stops making sense.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 5: Object + _clear<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': {'a': 1} });\ndataLayer.push({'x': {'b': 2}, _clear: true });<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-5&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-5-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-5\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">> Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-5\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-5-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p>x = <code>{'b': 2}<\/code><\/p>\n\n\n\n<p>The <code>_clear: true<\/code> flag tells GTM: write the keys in this push as a whole, don&#8217;t do recursive merge. So <code>x<\/code> doesn&#8217;t get added to the existing <code>{'a': 1}<\/code> \u2014 it gets completely overwritten to <code>{'b': 2}<\/code>. Key <code>a<\/code> is gone.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 6: _clear and a different key<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': {'a': 1} });\ndataLayer.push({'y': {'b': 2}, _clear: true });<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-6&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-6-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-6\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">> Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-6\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-6-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><code>x = {'a': 1}<\/code><\/p>\n\n\n\n<p><code>_clear: true<\/code> only resets the root keys present in the given push. The second push only writes <code>y<\/code> \u2014 it doesn&#8217;t touch <code>x<\/code>. The value of <code>x<\/code> remains <code>{'a': 1}<\/code>. This is important to understand: <code>_clear<\/code> is not a global reset of the entire data model. It only resets what you&#8217;re pushing.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Etude 7: Array + _clear<\/h3>\n\n\n\n<p>What do I get in the data model for variable <code>x<\/code>?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dataLayer.push({'x': &#91;1, 2, 3] });\ndataLayer.push({'x': &#91;4, 5], _clear: true });\n<\/code><\/pre>\n\n\n\n<div data-wp-context=\"{ &quot;autoclose&quot;: false, &quot;accordionItems&quot;: [] }\" data-wp-interactive=\"core\/accordion\" role=\"group\" class=\"wp-block-accordion is-layout-flow wp-block-accordion-is-layout-flow\">\n<div data-wp-class--is-open=\"state.isOpen\" data-wp-context=\"{ &quot;id&quot;: &quot;accordion-item-7&quot;, &quot;openByDefault&quot;: false }\" data-wp-init=\"callbacks.initAccordionItems\" data-wp-on-window--hashchange=\"callbacks.hashChange\" class=\"wp-block-accordion-item is-layout-flow wp-block-accordion-item-is-layout-flow\">\n<h3 class=\"wp-block-accordion-heading\"><button aria-expanded=\"false\" aria-controls=\"accordion-item-7-panel\" data-wp-bind--aria-expanded=\"state.isOpen\" data-wp-on--click=\"actions.toggle\" data-wp-on--keydown=\"actions.handleKeyDown\" id=\"accordion-item-7\" type=\"button\" class=\"wp-block-accordion-heading__toggle\"><span class=\"wp-block-accordion-heading__toggle-title\">Correct answer<\/span><span class=\"wp-block-accordion-heading__toggle-icon\" aria-hidden=\"true\">+<\/span><\/button><\/h3>\n\n\n\n<div inert aria-labelledby=\"accordion-item-7\" data-wp-bind--inert=\"!state.isOpen\" id=\"accordion-item-7-panel\" role=\"region\" class=\"wp-block-accordion-panel is-layout-flow wp-block-accordion-panel-is-layout-flow\">\n<p><code>x = [4, 5]<\/code><\/p>\n\n\n\n<p>With <code>_clear: true<\/code>, the array behaves predictably \u2014 key <code>x<\/code> gets overwritten as a whole. No hybrid index merging.<\/p>\n<\/div>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">What this means in practice<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Data accumulates in the data model<\/h3>\n\n\n\n<p>If I push e-commerce data for product A and then push data for product B, both <strong>remain<\/strong> in the data model. Nested objects get merged, not overwritten.<\/p>\n\n\n\n<p>With _clear:true you have the option to change individual parts of the dataLayer.<\/p>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<div class=\"wp-block-columns is-layout-flex wp-container-core-columns-is-layout-28f84493 wp-block-columns-is-layout-flex\">\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:33.33%\">\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"1024\" height=\"1024\" data-src=\"https:\/\/www.sabatka.net\/wp-content\/uploads\/2026\/04\/null-did-you-know.png\" alt=\"\" class=\"wp-image-936 lazyload\" data-srcset=\"https:\/\/www.sabatka.net\/wp-content\/uploads\/2026\/04\/null-did-you-know.png 1024w, https:\/\/www.sabatka.net\/wp-content\/uploads\/2026\/04\/null-did-you-know-300x300.png 300w, https:\/\/www.sabatka.net\/wp-content\/uploads\/2026\/04\/null-did-you-know-150x150.png 150w, https:\/\/www.sabatka.net\/wp-content\/uploads\/2026\/04\/null-did-you-know-768x768.png 768w\" data-sizes=\"(max-width: 1024px) 100vw, 1024px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 1024px; --smush-placeholder-aspect-ratio: 1024\/1024;\" \/><\/figure>\n<\/div>\n\n\n\n<div class=\"wp-block-column is-layout-flow wp-block-column-is-layout-flow\" style=\"flex-basis:66.66%\">\n<h3 class=\"wp-block-heading\">Scientific note<\/h3>\n\n\n\n<p>It&#8217;s like the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Ship_of_Theseus\" target=\"_blank\" rel=\"noopener\">Ship of Theseus<\/a> \u2014 if each push replaces a part of the object, is it still the same object?<br><br>Mmm&#8230;<\/p>\n<\/div>\n<\/div>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Timing of data reads matters<\/h3>\n\n\n\n<p>On <strong>single page applications<\/strong>, this is a critical problem. The user navigates from a product page to a category \u2014 but the product data is still in the data model. If a tag reads a variable when it shouldn&#8217;t be there anymore, you&#8217;re sending nonsense to GA4.<\/p>\n\n\n\n<p>On <strong>traditional websites<\/strong>, this is less of an issue (the page reloads, dataLayer gets recreated), but it can still hurt. Typical example: a user adds a product to the cart, then removes it. If you push the removal as a new object without clearing, <a href=\"https:\/\/www.sabatka.net\/en\/revenue-in-ga4-do-you-know-what-you-are-looking-at\/\">your GA4 revenue data won&#8217;t add up<\/a> \u2014 because the data model retains remnants of previous pushes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Solutions exist, but none is universal<\/h3>\n\n\n\n<p>There&#8217;s no silver bullet. Each approach has trade-offs, and you need to think it through so the whole solution makes sense in the context of your website:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Clearing on the developer side<\/strong> \u2014 Developers explicitly reset relevant keys before a new push. Reliable, but requires discipline and documentation. Any oversight = silent data error.<\/li>\n\n\n\n<li><strong>Selective reading in GTM<\/strong> \u2014 You don&#8217;t read a variable &#8220;generally,&#8221; but bind it to a specific event. The trigger fires only on the right push, so you read data at the moment it&#8217;s current. Works well, but configuration can get complex.<\/li>\n\n\n\n<li><strong><code>_clear: true<\/code><\/strong> \u2014 Resets the root keys in the given push \u2014 overwrites them as a whole instead of recursive merge. Safe against accumulation within a single key. But note: it doesn&#8217;t reset other root keys. If you need to clear other keys too, you must explicitly set them to <code>null<\/code> or include them in the push.<\/li>\n\n\n\n<li>dataLayer picker &#8211; a template that will only load data from the push that triggered the event from the dataLayer. More about the <a href=\"https:\/\/www.simoahava.com\/custom-templates\/data-layer-picker\/\" data-type=\"link\" data-id=\"https:\/\/www.simoahava.com\/custom-templates\/data-layer-picker\/\" target=\"_blank\" rel=\"noopener\">dataLayer picker was written by Simo Ahava<\/a>.<br>Note: thanks to <a href=\"https:\/\/mareklecian.cz\/\" data-type=\"link\" data-id=\"https:\/\/mareklecian.cz\/\" target=\"_blank\" rel=\"noopener\">Marek Leci\u00e1n<\/a> for adding this option<\/li>\n\n\n\n<li><strong>Combination<\/strong> \u2014 In practice, you&#8217;ll usually end up combining approaches. On my projects, I typically recommend <code>_clear: true<\/code> for e-commerce events (add_to_cart, purchase) and selective reading for the rest.<\/li>\n<\/ol>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Watch out for arrays<\/h3>\n\n\n\n<p>Etude 3 shows why arrays in dataLayer are treacherous. If you push arrays (like a list of products in a cart), recursive merge can break them in ways that are hard to debug. I recommend:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Always push arrays with <code>_clear: true<\/code>, or<\/li>\n\n\n\n<li>wrap them in a new object so they get overwritten as a whole, or<\/li>\n\n\n\n<li>use a dedicated tag in GTM for each event type.<\/li>\n<\/ul>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">How this relates to data quality<\/h2>\n\n\n\n<p>Recursive merge is one of those mechanisms you need to understand very well to measure correct data. Without this knowledge, you&#8217;ll see strange values in GA4 reports \u2014 revenue doesn&#8217;t add up, products get duplicated, events carry data from previous interactions. This is exactly <a href=\"https:\/\/www.sabatka.net\/en\/bugs-in-tracking-measurecamp-czechia-2025\/\">the type of bugs I presented at MeasureCamp<\/a> \u2014 silent, no errors, but with real impact on data.<\/p>\n\n\n\n<p>If you&#8217;re working on <a href=\"https:\/\/www.sabatka.net\/en\/data-quality-monitor-for-ga4\/\">data quality and measurement monitoring<\/a>, this is exactly the kind of problem you should have on your radar. It&#8217;s not a code error that crashes with a stack trace. It&#8217;s a data error that quietly distorts your decision-making.<\/p>\n\n\n\n<p>And if your company plans to use AI on analytics data, you need a <a href=\"https:\/\/www.sabatka.net\/en\/5-levels-of-data-maturity\/\">data infrastructure that&#8217;s ready for it<\/a>. The recursive merge problem in dataLayer is exactly the type of &#8220;detail&#8221; that makes the difference between data you can train a model on and data that teaches it nonsense.<\/p>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The dataLayer array and the GTM data model are two different things.<\/li>\n\n\n\n<li>GTM performs recursive merge \u2014 nested objects get merged, not overwritten.<\/li>\n\n\n\n<li>Arrays get merged by index, leading to unexpected results.<\/li>\n\n\n\n<li><code>_clear: true<\/code> resets root keys in the given push \u2014 it prevents recursive merge but doesn&#8217;t touch other keys.<\/li>\n\n\n\n<li>On SPAs, this is critical. On traditional websites, it hurts with events and e-commerce.<\/li>\n\n\n\n<li>Design your dataLayer architecture upfront \u2014 fixing it retroactively is expensive.<\/li>\n<\/ul>\n\n\n\n<p>If you&#8217;re not sure whether recursive merge is affecting your data, <a href=\"https:\/\/www.sabatka.net\/en\/kontakt\/\">get in touch<\/a> \u2014 we&#8217;ll review your dataLayer architecture.<\/p>\n\n\n\n<div style=\"height:50px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button scroll_to_subscribe\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.sabatka.net\/en\/kontakt\/\">Contact me<\/a><\/div>\n\n\n\n<div class=\"wp-block-button linkedinShare\"><a class=\"wp-block-button__link wp-element-button\">Share on LinkedIN<\/a><\/div>\n<\/div>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>dataLayer is a simple JavaScript array. Push an object, read it in a Google Tag Manager (GTM) variable, done. Nothing complicated about that. Right. If you&#8217;re designing dataLayer structures and haven&#8217;t heard of recursive merge yet, consider whether you want to keep reading. You&#8217;ll sleep worse. dataLayer vs. GTM data model \u2014 not the same [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":922,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9,10],"tags":[],"class_list":["post-912","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-digitalni-analytika","category-gtm"],"_links":{"self":[{"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/posts\/912","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/comments?post=912"}],"version-history":[{"count":21,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/posts\/912\/revisions"}],"predecessor-version":[{"id":947,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/posts\/912\/revisions\/947"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/media\/922"}],"wp:attachment":[{"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/media?parent=912"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/categories?post=912"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sabatka.net\/en\/wp-json\/wp\/v2\/tags?post=912"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}