Skip to content

How to Add Data Layer to Shopify Plus

Step-by-step guide to adding a data layer to Shopify Plus today: GTM with consent defaults in theme.liquid, view_item and add_to_cart pushes from the theme, a custom pixel for begin_checkout to purchase, then GTM setup and testing.

How to Add Data Layer to Shopify Plus

To add a data layer to Shopify Plus, install Google Tag Manager in theme.liquid and push GA4 ecommerce events from your theme on storefront pages, then add a custom pixel under Settings > Customer events that sends checkout and purchase events to GTM. Theme code does not run in checkout, so the pixel covers everything from begin_checkout to purchase.

This guide walks through six steps with code you can adapt, then testing and common mistakes. For the reasoning behind the event schema, naming and consent rules used here, read our Shopify Plus data layer best practices.

What do you need before you start?

  • A decision about the Google & YouTube app. The app already sends view_item, add_to_cart, begin_checkout, purchase and other events to GA4 (Google). If it stays connected to GA4, do not send the same events to the same GA4 property from GTM, or purchases are counted twice.
  • Two GTM web containers, one for the theme and one for the custom pixel. This is our recommendation, not a Shopify rule: it keeps storefront triggers from firing inside the pixel sandbox.
  • Customer privacy settings and a cookie banner configured in Shopify.
  • A duplicate of your live theme. In Online Store > Themes, click ... next to the theme and choose Duplicate, then work on the copy.

Open the theme copy with ... > Edit code. Create a snippet called data-layer.liquid (you will fill it in step 2). In layout/theme.liquid, render it inside <head>, after {{ content_for_header }}, and paste the head part of the GTM container code from Admin > Install Google Tag Manager straight after it:

{{ content_for_header }}
{% render 'data-layer' %}
<!-- Google Tag Manager container code from GTM goes here -->

The order matters. Google's consent defaults must run before any tag (Google), and the snippet uses window.Shopify, which content_for_header loads.

Paste this into snippets/data-layer.liquid. It sets every consent signal to denied, then updates it from Shopify's Customer Privacy API, which combines your privacy settings, the visitor's location and their choice. It also pushes page context and defines a helper for add_to_cart.

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag() { dataLayer.push(arguments); }

  // 1. Consent defaults, before GTM loads
  gtag('consent', 'default', {
    analytics_storage: 'denied',
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied'
  });

  // 2. Real status from Shopify's Customer Privacy API
  function sendConsent() {
    var privacy = window.Shopify && window.Shopify.customerPrivacy;
    if (!privacy) return;
    var marketing = privacy.marketingAllowed() ? 'granted' : 'denied';
    gtag('consent', 'update', {
      analytics_storage: privacy.analyticsProcessingAllowed() ? 'granted' : 'denied',
      ad_storage: marketing,
      ad_user_data: marketing,
      ad_personalization: marketing
    });
  }
  if (window.Shopify && window.Shopify.loadFeatures) {
    window.Shopify.loadFeatures(
      [{ name: 'consent-tracking-api', version: '0.2' }],
      function (error) { if (!error) sendConsent(); }
    );
  }
  document.addEventListener('visitorConsentCollected', sendConsent);

  // 3. Page context (no personal data)
  dataLayer.push({
    page_type: {{ request.page_type | json }},
    language: {{ request.locale.iso_code | json }},
    currency: {{ cart.currency.iso_code | json }},
    customer_logged_in: {% if customer %}true{% else %}false{% endif %}
  });

  // 4. Helper for add_to_cart, called from your cart JavaScript
  window.pushAddToCart = function (item, quantity) {
    var price = item.final_price / 100;
    dataLayer.push({ ecommerce: null });
    dataLayer.push({
      event: 'add_to_cart',
      ecommerce: {
        currency: {{ cart.currency.iso_code | json }},
        value: price * quantity,
        items: [{
          item_id: String(item.sku || item.variant_id),
          item_name: item.product_title,
          item_variant: item.variant_title,
          item_brand: item.vendor,
          price: price,
          quantity: quantity
        }]
      }
    });
  };
</script>

The json filter outputs safe JavaScript strings. Prices from the Ajax Cart API are in the currency's subunit, so the helper divides by 100.

Step 3: How do you push view_item from the product page?

Create a snippet called data-layer-product.liquid with the code below and render it at the end of your main product section with {% render 'data-layer-product', product: product %}. In Dawn that section is sections/main-product.liquid. Online Store 2.0 themes have no product.liquid template file, so look in sections/.

{%- assign variant = product.selected_or_first_available_variant -%}
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({ ecommerce: null });
  dataLayer.push({
    event: 'view_item',
    ecommerce: {
      currency: {{ cart.currency.iso_code | json }},
      value: {{ variant.price | divided_by: 100.0 }},
      items: [{
        item_id: {{ variant.sku | default: variant.id | append: '' | json }},
        item_name: {{ product.title | json }},
        item_variant: {{ variant.title | json }},
        item_brand: {{ product.vendor | json }},
        item_category: {{ product.type | json }},
        price: {{ variant.price | divided_by: 100.0 }},
        quantity: 1
      }]
    }
  });
</script>

Liquid prices are in the currency's subunit (Shopify), hence divided_by: 100.0. Do not use money filters here: they return formatted text. A view_item_list push on collection pages follows the same pattern, with one item per product in collection.products.

Step 4: How do you fire add_to_cart from your cart code?

Themes add to cart in different ways, so call window.pushAddToCart wherever your theme gets a successful response from /cart/add.js. In Dawn that is the product form script. The response is the JSON of the added line item (Shopify):

const quantity = Number(formData.get('quantity') || 1);
fetch(window.Shopify.routes.root + 'cart/add.js', { method: 'POST', body: formData })
  .then((response) => response.json())
  .then((item) => {
    if (item.status) return; // error response, nothing was added
    window.pushAddToCart(item, quantity);
    // ...the theme's own cart update code continues here
  });

Pass the quantity the visitor chose, not the one in the response: if the item was already in the cart, the response holds the new total quantity. If your theme has several add-to-cart paths (quick add, upsell apps), an alternative is to subscribe to product_added_to_cart in the pixel from step 5 instead. Use one source, not both.

Step 5: How do you send checkout and purchase events with a custom pixel?

Shopify's checkout, Thank you and Order status pages do not run theme code or Additional scripts any more. Additional scripts and checkout.liquid were sunset on the Thank you and Order status pages on 28 August 2025 (Shopify). Pixels load on all of these pages (Shopify Help Center). To add one (Shopify Help Center):

  1. Go to Settings > Customer events and click Add custom pixel.
  2. Enter a name, for example "GTM checkout", and click Add pixel.
  3. Under Customer privacy, set Permission to Required and choose the purposes your tags need, usually Analytics and Marketing. Set Data sale to match your tags.
  4. Paste the code below into the Code window, with your pixel container ID instead of GTM-XXXXXXX.
  5. Click Save, then Connect pixel.
// Paste as JavaScript only: no <script> tags in a custom pixel
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }

// Consent mode from Shopify's privacy status
let privacy = init.customerPrivacy;
function sendConsent(mode) {
  const marketing = privacy.marketingAllowed ? 'granted' : 'denied';
  gtag('consent', mode, {
    analytics_storage: privacy.analyticsProcessingAllowed ? 'granted' : 'denied',
    ad_storage: marketing,
    ad_user_data: marketing,
    ad_personalization: marketing
  });
}
sendConsent('default');
api.customerPrivacy.subscribe('visitorConsentCollected', (event) => {
  privacy = event.customerPrivacy;
  sendConsent('update');
});

// GTM loader (the head snippet from GTM, without the script tags)
(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-XXXXXXX');

// Same item ID rule as the theme: SKU, else variant ID
function toItems(checkout) {
  return checkout.lineItems.map((line, index) => ({
    item_id: String(line.variant?.sku || line.variant?.id),
    item_name: line.variant?.product?.title || line.title,
    item_variant: line.variant?.title,
    item_brand: line.variant?.product?.vendor,
    price: Number(line.variant?.price?.amount),
    quantity: line.quantity,
    index: index
  }));
}

// value = subtotal before shipping and tax; change it to match your definition
function pushCheckout(name, event, extra) {
  const checkout = event.data.checkout;
  dataLayer.push({ ecommerce: null });
  dataLayer.push({
    event: name,
    page_location: event.context.window.location.href,
    ecommerce: Object.assign({
      currency: checkout.currencyCode,
      value: Number(checkout.subtotalPrice?.amount),
      items: toItems(checkout)
    }, extra || {})
  });
}

analytics.subscribe('checkout_started', (e) => pushCheckout('begin_checkout', e));
analytics.subscribe('checkout_shipping_info_submitted', (e) => pushCheckout('add_shipping_info', e));
analytics.subscribe('payment_info_submitted', (e) => pushCheckout('add_payment_info', e));
analytics.subscribe('checkout_completed', (e) => {
  const checkout = e.data.checkout;
  const codes = (checkout.discountApplications || [])
    .filter((d) => d.type === 'DISCOUNT_CODE')
    .map((d) => d.title);
  pushCheckout('purchase', e, {
    transaction_id: checkout.order?.id,
    tax: Number(checkout.totalTax?.amount || 0),
    shipping: Number(checkout.shippingLine?.price?.amount || 0),
    coupon: codes.join(',')
  });
});

How this works: the pixel runs in a sandboxed iframe, so it has its own window.dataLayer and cannot read or change your page (Shopify). init.customerPrivacy and visitorConsentCollected keep consent mode in sync (Shopify). The field paths come from the checkout_completed reference, and checkout_completed fires once per checkout, on the first upsell page if you use post-purchase offers. Because window.location inside the sandbox is not your store's URL, the code sends page_location from event.context, as Shopify's GTM tutorial suggests.

Note that Shopify does not support custom pixel code, and the same tutorial says outbound link clicks and other DOM-based triggers do not work inside the sandbox.

Step 6: How do you configure GTM?

  1. Triggers: create a Custom Event trigger for each event name: view_item and add_to_cart in the theme container; begin_checkout, add_shipping_info, add_payment_info and purchase in the pixel container.
  2. Google tag: add your GA4 measurement ID in both containers. In the pixel container, set the configuration parameter send_page_view to false, because the theme already sends page views and the sandbox URL would pollute your reports.
  3. Event tag: create a Google Analytics: GA4 Event tag with Event Name {{Event}}. Under More Settings > Ecommerce, select Send Ecommerce data with Data Source Data Layer, as described in Google's guide. In the pixel container, add the event parameter page_location from a Data Layer Variable.
  4. Other variables: add Data Layer Variables for page_type and customer_logged_in if you want them as parameters.
  5. Publish each container as a named version.

How do you test the setup?

  • Storefront: open GTM Preview for the theme container. Check that view_item appears on a product page with the right price and currency, and add_to_cart after you add a product.
  • Checkout: Tag Assistant's Troubleshoot tag feature does not detect Google tags inside a custom pixel. Use the Shopify Pixel Helper to see the events reach the pixel (Shopify Help Center), and GA4 DebugView to see what arrives.
  • Test order: place an order in your payment provider's test mode. Confirm one purchase with transaction_id, value, currency and items, and check that the checkout events join the same GA4 session as the storefront events.
  • After launch: compare GA4 purchases with Shopify orders daily for the first week, then weekly.

What are the most common mistakes?

MistakeFix
Purchase code left in Additional scriptsIt no longer runs on the Thank you page. Move it to a custom pixel.
Universal Analytics objects (ecommerce.detail, actionField, transactionTotal)Rebuild with GA4 event names and an items array.
Automatic page views left on in the pixel containerSet send_page_view to false there. The theme container sends page views.
Prices output with money filtersUse divided_by: 100.0 for numeric values.
Different item IDs in theme and pixelUse the same SKU or variant ID rule in both.
The same container ID in theme and pixel, with All Pages triggersUse a separate pixel container, or limit its triggers to checkout event names.

Most stores can follow these steps as they are. If yours has a custom cart, several checkout apps or tracking that already disagrees with Shopify's order numbers, we can set it up and test it for you as part of Shopify Plus checkout customization.

FAQ

Where do I put data layer code in Shopify?

Storefront code goes in your theme: a snippet rendered in layout/theme.liquid for site-wide data and one in the main product section for product data. Checkout and purchase code goes in a custom pixel under Settings > Customer events.

Can I track purchases from the theme?

No. The Thank you and Order status pages are part of Shopify's checkout and do not load theme files. Subscribe to checkout_completed in a pixel instead.

Does GTM Preview mode work for the custom pixel?

Shopify's guide does not cover GTM Preview for pixels, and Tag Assistant's Troubleshoot tag feature cannot detect Google tags inside one. Shopify recommends the Pixel Helper. Combine it with GA4 DebugView.

Do I need Shopify Plus for this?

No. Theme data layers and custom pixels work on other plans too. Plus adds checkout UI extensions on the information, shipping and payment steps, which can publish extra custom events to your pixel (Shopify).

Why does GA4 show fewer purchases than Shopify?

Visitors who decline consent, ad blockers and browser tracking protection all remove events that Shopify still records as orders. A stable gap is normal. A sudden change usually means a broken snippet, a disconnected pixel or a duplicate sender.

Scaling on Shopify Plus?

Migration, B2B, architecture, performance — we run the engagements other agencies hand back.

The checklist we run before touching a client's code

Get it by email, plus a few short notes over the next two weeks: what we see most often on Shopify stores and what actually fixes it.

No spam. Unsubscribe anytime. Privacy Policy

Want more of our posts in your Google results? Add Codersy as a preferred source.

Add Codersy on Google(opens in a new tab)