← All Guides

Shopify GA4 Ecommerce Tracking with Google Tag Manager: Data Layer, Checkout Customer Events and Purchase Deduplication

A complete Shopify tracking setup through Google Tag Manager: GTM in the theme, a Liquid-powered data layer for view_item and add_to_cart, checkout and purchase events through Shopify Customer Events, GA4 ecommerce tags, Google Ads and Meta purchase tags, and deduplication by order ID.

Atiqul Islam
Written by Atiqul Islam

Tracking & analytics specialist. 278+ GTM, GA4 and server-side tracking projects for clients in the US, UK and Australia.

Rather not do this yourself? I set this up for clients every week. Free 15-minute audit, no obligation.

Book a Free Audit

Shopify stores lose tracking in a predictable place: the checkout. The storefront is easy to tag, but checkout and the thank-you page run on Shopify's own infrastructure where theme code does not execute. Add the usual problems of duplicate purchase events, missing values and a sales channel app fighting with GTM, and you have the reason so many Shopify GA4 properties show revenue that does not match the Shopify admin.

This is the setup I use for Shopify stores. It puts Google Tag Manager in charge, builds a clean data layer for the storefront, uses Customer Events for checkout, and ends with deduplicated purchase events in GA4, Google Ads and Meta.

The Architecture

Shopify store, then data layer, then Google Tag Manager, then GA4, Google Ads and Meta. Two places push to the data layer: the theme for storefront events and a custom pixel for checkout events.

Step 1: Install GTM in the Theme

In the Shopify admin, open Online Store, then Themes, then Edit code. In theme.liquid, paste the GTM script immediately after the opening <head> tag and the noscript snippet immediately after the opening <body> tag. Save and confirm the container loads on the storefront in GTM Preview.

Step 2: Storefront Data Layer

view_item on product pages. Add a snippet to your product template, before the GTM snippet or in the head, using Liquid to pull product data:

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

add_to_cart. Most themes add to cart through an AJAX call to /cart/add.js. Intercept the response rather than the button click so you only record successful additions:

<script>
(function () {
  var origFetch = window.fetch;
  window.fetch = function (input, init) {
    var url = typeof input === 'string' ? input : (input && input.url) || '';
    var p = origFetch.apply(this, arguments);
    if (url.indexOf('/cart/add') > -1) {
      p.then(function (res) { return res.clone().json(); }).then(function (item) {
        if (!item || !item.id) { return; }
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({ ecommerce: null });
        window.dataLayer.push({
          event: 'add_to_cart',
          ecommerce: {
            currency: window.Shopify && Shopify.currency ? Shopify.currency.active : 'USD',
            value: (item.final_price || item.price) / 100 * (item.quantity || 1),
            items: [{
              item_id: item.sku || String(item.variant_id),
              item_name: item.product_title,
              item_variant: item.variant_title,
              price: (item.final_price || item.price) / 100,
              quantity: item.quantity || 1
            }]
          }
        });
      }).catch(function () {});
    }
    return p;
  };
})();
</script>

Place this as a Custom HTML tag in GTM on All Pages, or in the theme head before any theme scripts. Themes that use XMLHttpRequest instead of fetch need an equivalent wrapper.

Step 3: Checkout and Purchase Through Customer Events

In the Shopify admin, open Settings, then Customer events, then Add custom pixel. Give it a name, set permissions to match your consent requirements, and paste a pixel that loads GTM and forwards Shopify's standard events:

window.dataLayer = window.dataLayer || [];
(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');

function mapItems(lineItems) {
  return (lineItems || []).map(function (li) {
    return {
      item_id: (li.variant && li.variant.sku) || (li.variant && li.variant.id) || li.id,
      item_name: li.title,
      item_variant: li.variant && li.variant.title,
      price: li.variant && li.variant.price ? li.variant.price.amount : li.finalLinePrice.amount / li.quantity,
      quantity: li.quantity
    };
  });
}

analytics.subscribe('checkout_started', function (event) {
  var c = event.data.checkout;
  window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push({
    event: 'begin_checkout',
    ecommerce: { currency: c.currencyCode, value: c.totalPrice.amount, items: mapItems(c.lineItems) }
  });
});

analytics.subscribe('checkout_completed', function (event) {
  var c = event.data.checkout;
  window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push({
    event: 'purchase',
    ecommerce: {
      transaction_id: c.order && c.order.id ? String(c.order.id) : c.token,
      value: c.totalPrice.amount,
      tax: c.totalTax ? c.totalTax.amount : undefined,
      shipping: c.shippingLine && c.shippingLine.price ? c.shippingLine.price.amount : undefined,
      currency: c.currencyCode,
      items: mapItems(c.lineItems)
    },
    user_email: c.email ? c.email.toLowerCase() : undefined,
    user_phone: c.phone || undefined
  });
});

Connect the pixel. It runs in a sandboxed frame, so GTM inside it has its own container instance. That is expected. Keep the same container ID so your tags and triggers apply.

Step 4: GTM Variables, Triggers and Tags

Variables: ecommerce.transaction_id, ecommerce.value, ecommerce.currency, ecommerce.items, user_email, user_phone. Custom Event triggers for view_item, add_to_cart, begin_checkout, purchase.

Step 5: Remove Competing Trackers

If the Google and YouTube channel app or the Facebook and Instagram channel app is also sending GA4 or purchase events, you will double count. Either disconnect their tracking or do not create the same events in GTM. One source per event per platform.

Step 6: Test

  1. GTM Preview on the storefront. View a product, add to cart, start checkout. Confirm view_item, add_to_cart and begin_checkout with correct values and items.
  2. Place a test order using Shopify's test gateway or a 100 percent discount code. On the thank-you page, confirm one purchase with the order ID as transaction ID and the correct total.
  3. Check GA4 DebugView for all four events and their items. Check Meta Events Manager Test Events. Inspect the Google Ads request in a tag debugger such as the Insighter Debugger extension.
  4. Reload the thank-you page. The pixel should not fire a second purchase.
  5. After 24 hours, compare GA4 purchase revenue with the Shopify Orders report for the same day. Expect them to match closely, with small gaps from consent and ad blockers.

Common Problems

Going Further

Once browser tracking is clean, add server-side GTM: send the same events to a server container, forward to GA4, Google Ads and Meta Conversions API from there, and deduplicate by transaction and event ID. The Shopify Google Ads server-side guide on this site covers why and how.

Frequently Asked Questions

Should I use the Google and YouTube sales channel app instead of GTM?

The sales channel app is a quick way to get basic GA4 and Google Ads data, but it gives you little control over events, parameters and deduplication, and it does not cover Meta or server-side. For a store that spends real money on ads, GTM with a proper data layer is worth the setup. Do not run both for the same events.

Why do I need Customer Events for checkout?

Shopify checkout and the thank-you page no longer load theme code or Additional Scripts. Customer Events, also called web pixels, are the supported way to run tracking on checkout. A custom pixel can load GTM in a sandbox and receive checkout_started and checkout_completed events with full order data.

How do I stop duplicate purchases when a customer reloads the thank-you page?

Use the Shopify order ID as the transaction ID in every tag. GA4 and Google Ads deduplicate on it. Shopify's checkout_completed event also fires once per order by design, which helps.

Do I need Shopify Plus?

No. Customer Events are available on all plans. Plus adds checkout.liquid access only on legacy setups, which is being retired anyway.

Can I send Meta Purchase events from the same setup?

Yes. Fire the Meta Pixel Purchase tag on the same purchase trigger with value, currency, content IDs and an event ID equal to the order ID, and deduplicate against the Conversions API if you run server-side.

Want This Done Right the First Time?

Book a free 15-minute audit call. I'll review your setup, show you what is broken, and tell you exactly what it takes to fix it.

Chat Now