The Problem

A developer pushes user_type: "returning_customer" and cart_value: 149 to the data layer on the product page. Your GTM tags read these values correctly. Everything looks fine.

Then the user navigates to the checkout page. The data layer resets. user_type and cart_value are undefined. Your checkout event tag sends incomplete data. Your GA4 audience conditions that depend on user_type do not apply.

This catches a lot of GTM setups off guard - especially multi-step funnel tracking and audience segmentation that relies on data set earlier in the session.


Why the Data Layer Resets

GTM’s data layer is a JavaScript object (window.dataLayer) that lives in the browser’s memory for the current page session.

When the user navigates to a new page (a full page reload - not an SPA navigation), the browser starts fresh. window.dataLayer is re-initialised as an empty array. Any values pushed on the previous page are gone.

This is by design - but it creates a tracking gap for data that needs to persist across pages.


What Gets Lost (and Why It Matters)

User properties set on login: A user logs in on page A. The data layer receives user_id: "12345" and user_type: "subscriber". On page B, these values are gone - unless you re-push them on every page, which usually requires developer involvement on each template.

Cart data set on product pages: cart_value, cart_item_count, coupon_applied pushed on the cart page are lost when the user hits the checkout page.

Funnel step tracking: If you want to know which users visited the pricing page before converting, and the pricing page pushes a viewed_pricing: true variable - that is gone by the time the conversion fires on the thank-you page.

Audience segmentation: GA4 audience conditions like “users who saw a specific product category” require that data to be present when the audience-defining event fires. If the category data was only available on the product page, it will not be present on the checkout page.


The Solution: sessionStorage and localStorage

The browser provides two storage mechanisms that persist across page navigation:

sessionStorage: Persists for the duration of the browser tab session. Cleared when the tab is closed. Appropriate for session-scoped data (cart values, funnel steps, viewed categories).

localStorage: Persists until explicitly cleared, across sessions and browser restarts. Appropriate for user-level data (user_id, user_type, subscription status).

Both are accessible from JavaScript on any page of the same origin - which means you can write to them on page A and read them back on page B.


Implementation: Write to Storage When Values Are Available

Step 1: Create a Custom HTML Tag to Write Values

In GTM, create a Custom HTML tag that fires when the relevant data is available on the page:

<script>
  // Read from the data layer
  var userType = {{DLV - user_type}};
  var userId = {{DLV - user_id}};
  var cartValue = {{DLV - cart_value}};

  // Persist to storage
  if (userType) sessionStorage.setItem('gtm_user_type', userType);
  if (userId) localStorage.setItem('gtm_user_id', userId);
  if (cartValue) sessionStorage.setItem('gtm_cart_value', cartValue);
</script>

Replace {{DLV - user_type}} with your actual Data Layer Variable names.

Trigger: Fire this tag on the page or event where the data layer values are pushed (e.g. on the product page load, or on the add_to_cart event).

Step 2: Create a Custom HTML Tag to Read Values on Page Load

Create a second Custom HTML tag that reads from storage and pushes the values back into the data layer on every page load:

<script>
  window.dataLayer = window.dataLayer || [];

  var restored = {};

  var userType = sessionStorage.getItem('gtm_user_type');
  var userId = localStorage.getItem('gtm_user_id');
  var cartValue = sessionStorage.getItem('gtm_cart_value');

  if (userType) restored.user_type = userType;
  if (userId) restored.user_id = userId;
  if (cartValue) restored.cart_value = parseFloat(cartValue);

  if (Object.keys(restored).length > 0) {
    window.dataLayer.push(restored);
  }
</script>

Trigger: All Pages - Initialization (so this fires before any other tags).

Now on every page load, any previously stored values are restored to the data layer - available to all subsequent tags on that page.


Implementation: GTM Variables to Read Directly from Storage

Instead of restoring values to the data layer, you can read from storage directly using a Custom JavaScript Variable in GTM:

function() {
  return sessionStorage.getItem('gtm_user_type') || undefined;
}

Use this variable directly in your event tags. No data layer restoration needed.

This approach is cleaner if you only need the value in a few specific tags, rather than making it available globally.


Before storing anything in localStorage or sessionStorage, consider what you are storing and whether consent is required.

sessionStorage is generally considered less privacy-sensitive than cookies or localStorage because it is automatically cleared when the tab closes. However, if you are storing data that could identify a user (user_id, email hash, advertising-related data), you should only write to storage after the appropriate consent has been granted.

localStorage persists across sessions. If you store advertising-related variables here, you are creating a persistent identifier that follows the user across visits - which in GDPR terms may require explicit consent.

Safe pattern: Only write user identity data to storage after confirming analytics_storage: 'granted' in your Consent Mode implementation. Wrap the storage write in a consent check:

<script>
  // Only store if consent has been granted
  function hasAnalyticsConsent() {
    // Check your CMP's consent API
    // This varies by CMP  -  check your CMP documentation
    return typeof window.__cmp !== 'undefined'
      ? window.__cmp('consentData', null, function(data) { return data; })
      : true; // fallback if no CMP
  }

  if (hasAnalyticsConsent()) {
    var userType = {{DLV - user_type}};
    if (userType) sessionStorage.setItem('gtm_user_type', userType);
  }
</script>

For most CMPs, the cleanest approach is to fire the storage-write tag on a trigger that only fires after consent is confirmed - such as a Custom Event trigger matching your CMP’s consent-granted event.


Practical Use Cases

Tracking the Full Path to Conversion

You want to know if a user visited the pricing page before converting. On the pricing page:

sessionStorage.setItem('gtm_viewed_pricing', 'true');

On the thank-you page, read the value and include it as an event parameter:

// Custom JS Variable
function() {
  return sessionStorage.getItem('gtm_viewed_pricing') === 'true';
}

Add this as a custom parameter on your conversion event. Now you can segment conversions in GA4 by whether the user visited the pricing page.

Maintaining User Type Across the Funnel

If your site pushes user_type on the homepage or after login but not on the checkout page, store it after login and restore it to the data layer on the checkout page. Your checkout event then has the correct user_type attached.

Preserving Referral Source Data

If a user lands from a specific campaign and you want to attach that source to a later conversion event, store the UTM parameters in sessionStorage on landing:

function() {
  var params = new URLSearchParams(window.location.search);
  var source = params.get('utm_source');
  if (source) sessionStorage.setItem('gtm_first_utm_source', source);
}

Read it back on the conversion page to include first-touch source data in your event parameters.


Debugging

If values are not being restored correctly:

  1. On the page where values are written, open DevTools → Application tab → Session Storage → confirm the values are present with the correct keys
  2. On the next page, open DevTools → Application tab → confirm the values are still present (they should be, within the same tab session)
  3. In GTM Preview mode on the next page, check that the restoration tag fires in the Initialization phase (before other tags)
  4. Check your Custom JavaScript Variable in the Variables tab - confirm it returns the expected value

What Requires Developer Involvement

This approach handles the GTM side without developer changes. However, if the original data layer push only happens on certain templates, a developer may still need to add the push to additional page types.

The persistence mechanism is a GTM-only solution - but the source data still needs to be pushed to the data layer at least once. Discuss with developers which pages push which data, and identify where gaps exist.


Final Thoughts

Data layer persistence is a gap that affects almost every multi-page tracking setup - it is just not always noticed until you build an audience or segment that depends on cross-page data.

sessionStorage is the right tool for session-scoped data. localStorage for user-scoped data. Both are straightforward to implement in GTM with Custom HTML tags and Custom JavaScript Variables - no developer required for the persistence layer itself.

This is the final article in the GTM Advanced Series. The five problems covered - duplicate conversions, SPA pageview tracking, CSP blocks, cross-domain attribution, and data layer persistence - are the advanced scenarios that cause the most debugging hours in real production setups. Understanding each one puts you significantly ahead of the average GTM practitioner.

Related Posts

GA4 Enhanced Ecommerce Tracking - What the Data Layer Needs to Look Like

10 min read

GA4EcommerceData LayerGTMTrackingGA4 Intro Series

GTM on Single-Page Applications - Why Only the First Pageview Gets Tracked and How to Fix It

10 min read

GTMGA4Single-Page ApplicationsReactTrackingGTM Advanced Series

How to Block China Traffic in Google Tag Manager

GTMGoogle Tag ManagerAnalyticsGTM Advanced Series
Adnan Agic

Adnan Agic

Google Ads Strategist & Technical Marketing Expert with 5+ years experience managing $10M+ in ad spend across 100+ accounts.

Need Help With Your Google Ads?

I help e-commerce brands scale profitably with data-driven PPC strategies.

Get In Touch
Back to Blog