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.
- Google tag with your GA4 measurement ID on All Pages.
- GA4 Event tags for each ecommerce event, set to read ecommerce data from the dataLayer.
- Google Ads Conversion Tracking on
purchasewith value, transaction ID and currency, a User-Provided Data variable from email and phone for Enhanced Conversions, and the Conversion Linker on All Pages with cross-domain linking if your checkout uses a different domain. - Meta Pixel base on All Pages and a Purchase event on
purchasewith value, currency,content_idsfrom the item IDs,content_typeofproduct, advanced matching andeventIDequal to the transaction ID.
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
- GTM Preview on the storefront. View a product, add to cart, start checkout. Confirm
view_item,add_to_cartandbegin_checkoutwith correct values and items. - Place a test order using Shopify's test gateway or a 100 percent discount code. On the thank-you page, confirm one
purchasewith the order ID as transaction ID and the correct total. - 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.
- Reload the thank-you page. The pixel should not fire a second
purchase. - 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
- Purchases missing in GA4. The custom pixel is not connected, or consent permissions on the pixel block it. Check the pixel status and consent settings.
- Revenue too high. Duplicate sources, usually the channel app plus GTM.
- Items array empty. The mapping function expects fields your line items do not have. Log
event.data.checkoutonce in the pixel to confirm the structure. - add_to_cart never fires. The theme uses XMLHttpRequest or a form post. Adapt the interceptor or use the theme's own cart event if it exposes one.
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.
