GA4 · · Last updated: July 21, 2026

GA4 + Shopify: the setup guide that Shopify's docs should have been

Shopify's built-in GA4 integration misses half the ecommerce events. Here's how to set it up properly with GTM.

GA4 + Shopify: the setup guide that Shopify's docs should have been

Shopify has a native GA4 integration. You paste your measurement ID into the settings, toggle it on, and GA4 starts receiving data. Shopify’s documentation makes it sound like you’re done at that point.

You’re not done. You’re maybe 40% done. And the 60% that’s missing is the part that actually matters for making business decisions.

I’ve set up GA4 on probably 30 Shopify stores over the past few years. The native integration has improved since its initial launch, but it still has gaps that will cost you visibility into how customers actually behave on your store. Here’s the full picture of what you get, what you don’t get, and how to fill the gaps properly.

What Shopify’s native integration actually tracks

As of mid-2026, Shopify’s built-in GA4 integration sends these events:

  • page_view on every page
  • view_item on product pages
  • add_to_cart when someone adds a product
  • begin_checkout when the checkout loads
  • purchase when an order completes
  • search when someone uses on-site search

That looks reasonable on paper. The reality is more nuanced.

The view_item event fires, but it often misses variant-level data. If you sell a t-shirt in five colors and three sizes, you want to know which variants people view vs. which they buy. Shopify’s native event sends the product title and price but frequently drops the variant information.

The add_to_cart event has similar issues. It fires, but the product data it sends to GA4 isn’t always complete. I’ve seen stores where the item category was blank on every add-to-cart event. Not a Shopify bug exactly. More of a gap in how Shopify maps its product taxonomy to GA4’s expected data structure.

More importantly, several events that GA4 expects for a complete ecommerce funnel simply aren’t sent by the native integration:

  • view_item_list (product listing views on collection pages)
  • select_item (clicking a product from a listing)
  • add_payment_info (entering payment details at checkout)
  • add_shipping_info (entering shipping details)
  • remove_from_cart (removing items from cart)
  • view_cart (viewing the cart page)

Without these events, your funnel analysis in GA4 has massive blind spots. You can see that someone viewed a product and then purchased, but you can’t see where in the checkout they dropped off. You can’t tell which collection pages drive the most product views. Your ecommerce tracking is incomplete in ways that make it hard to optimize anything beyond the broadest strokes.

Why GTM is the better approach

Google Tag Manager gives you full control over what fires, when, and with what data. On Shopify, the GTM setup is more work upfront, but it pays for itself the first time you need to debug a tracking issue or add a custom event.

Here’s the catch: Shopify doesn’t let you install GTM the way a normal website does. There’s no simple “paste the GTM snippet in the head” on Shopify. Well, technically you can inject it into the theme, but it won’t work on the checkout pages. Shopify’s checkout is sandboxed. Your theme code doesn’t run there.

This means you need a two-part approach:

  1. GTM in the theme for pre-checkout pages (browsing, product pages, cart)
  2. Shopify’s Customer Events (Web Pixel) API for checkout and post-purchase events

Setting up GTM in your Shopify theme

Add the GTM container snippet to your theme.liquid file, right after the opening <head> tag. If you’re using a Shopify 2.0 theme, the file structure is slightly different but the principle is the same. Place the noscript fallback right after the opening <body> tag.

Then you need a data layer. Shopify doesn’t push structured ecommerce data to the data layer by default, so you have to build it. This is where most of the actual work happens.

For product pages, push a view_item event with the full product data:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'view_item',
  ecommerce: {
    currency: '{{ shop.currency }}',
    value: {{ product.selected_or_first_available_variant.price | money_without_currency | remove: ',' }},
    items: [{
      item_id: '{{ product.selected_or_first_available_variant.sku }}',
      item_name: '{{ product.title | escape }}',
      item_brand: '{{ product.vendor | escape }}',
      item_category: '{{ product.type | escape }}',
      item_variant: '{{ product.selected_or_first_available_variant.title | escape }}',
      price: {{ product.selected_or_first_available_variant.price | money_without_currency | remove: ',' }},
      quantity: 1
    }]
  }
});

For collection pages, push a view_item_list event with all visible products. For add-to-cart, you need to intercept the cart form submission or the AJAX cart API call and push the event with product data.

I’m not going to include every code snippet here because Shopify themes vary wildly. Dawn’s cart works differently from Debut. Headless Shopify setups are a different beast entirely. The principle is always the same: intercept the user action, build the ecommerce object, push it to the data layer.

Handling the checkout with Customer Events

Shopify’s Customer Events (introduced as part of their Checkout Extensibility platform) replaced the old checkout.liquid approach. If you’re on Shopify Plus, you previously had access to checkout.liquid, where you could inject arbitrary scripts. Shopify has been sunsetting that in favor of their pixel API.

Here’s how this works. In your Shopify admin, go to Settings > Customer Events. Create a custom pixel. This pixel runs in a sandboxed iframe on checkout pages. You can subscribe to Shopify’s standard events and forward them to GA4.

analytics.subscribe('checkout_started', (event) => {
  const checkout = event.data.checkout;
  // Transform Shopify's checkout data into GA4's expected format
  // Send to GA4 via Measurement Protocol or gtag
});

analytics.subscribe('payment_info_submitted', (event) => {
  // Fire add_payment_info event
});

analytics.subscribe('checkout_completed', (event) => {
  // Fire purchase event
});

The important thing to understand: this pixel runs in a sandboxed iframe. It doesn’t have access to your GTM container. It doesn’t share cookies with your main site. You’re essentially running two separate tracking systems and trying to stitch them together.

This is why the _ga cookie and client ID become important. You need to pass the GA4 client ID from the pre-checkout pages into the checkout, so that GA4 can connect the browsing session with the purchase. Shopify provides mechanisms for this, but it doesn’t happen automatically with the native integration.

Shopify tracking setup giving you headaches?I'll audit your current implementation, find the gaps, and build a plan to fix them.

Book a Free Audit →

The cross-domain problem

This is one of the most common issues I see with Shopify stores, and I’ve written about cross-domain tracking in GA4 in detail before. Many Shopify stores use a custom domain for their storefront (shop.yourbrand.com) but the checkout happens on checkout.shopify.com or a slightly different subdomain. If GA4 doesn’t know these domains are the same property, it treats the transition to checkout as a new session.

The result: your checkout page shows a massive “direct” traffic source. All those users who came from Google Ads or organic search and then clicked “Checkout” — GA4 lost track of them at the domain boundary. Your attribution data is now wrong. You’re undercounting every channel except direct.

Shopify’s native integration is supposed to handle this automatically. In my experience, it works about 70% of the time. The other 30%, something goes wrong. A redirect strips the cross-domain parameters. A payment provider redirect breaks the chain. A client-side cookie gets blocked.

With GTM, you have explicit control over cross-domain configuration. You add the checkout domain to your GA4 tag’s cross-domain settings, and you can verify it’s working with the GA4 DebugView.

Server-side tracking on Shopify

If you’re serious about accurate data, you need server-side tracking. Client-side tracking on Shopify has all the usual problems: ad blockers, ITP in Safari, consent management dropping events. But it also has a Shopify-specific problem: the checkout sandboxing means client-side tracking through the full funnel is inherently fragile.

The server-side approach fixes most of these issues. There are two ways to do it on Shopify:

Option 1: Shopify webhooks to server-side GTM (sGTM)

Shopify can fire webhooks on order creation, fulfillment, and other events. You set up an endpoint in your sGTM container that receives these webhooks and forwards the data to GA4 via the Measurement Protocol.

The upside: you never miss a transaction. Every order that Shopify processes gets sent to GA4, regardless of what happened on the client side.

The downside: you need to stitch the server-side purchase event with the client-side browsing session. This requires passing the GA4 client ID through the checkout and including it in the webhook payload. Shopify’s webhook doesn’t include GA4 client IDs by default, so you need to store them (typically in Shopify’s cart attributes or order note attributes).

Option 2: Shopify’s native pixel forwarding to sGTM

You can configure the Customer Events pixel to send data to your sGTM endpoint instead of directly to GA4. This gives you the benefits of server-side processing (longer cookie lifetimes, first-party domain, data enrichment) while using Shopify’s built-in event system.

I generally recommend a hybrid approach. Use option 2 for the full event stream (browsing, cart, checkout events) and option 1 as a backup specifically for purchases. If the pixel fails to fire for any reason, the webhook catches it. Belt and suspenders.

Common issues and how to fix them

Duplicate transactions

This is the number one Shopify + GA4 problem. The purchase event fires when the checkout completes, but if the customer refreshes the thank-you page, it fires again. Or if they bookmark the URL and come back later. Or if they click the email confirmation link that takes them back to the order status page.

Fix: use the transaction ID. Every GA4 purchase event should include a unique transaction_id. GA4 is supposed to deduplicate events with the same transaction ID, but “supposed to” and “does” are different things. On the server-side, implement your own deduplication. Keep a log of processed transaction IDs and skip any repeats.

Currency problems

Shopify stores that sell in multiple currencies have a specific issue. The product price in the data layer might be in the store’s base currency, but the customer pays in their local currency. If your purchase event sends the base currency amount but the view_item events send the presentment (local) currency amount, your revenue reports in GA4 are going to be inconsistent.

Fix: decide on one currency for analytics. I recommend using the presentment currency (what the customer actually paid) for all events. Make sure the currency parameter is set correctly on every ecommerce event.

Variant tracking gaps

Shopify’s variant system uses a combination of option values (Size: Large, Color: Blue) but GA4’s item_variant field expects a single string. How you concatenate these matters. “Large / Blue” is fine. But make sure the format is consistent across all events — the variant string on view_item should match the variant string on purchase.

Cart page vs. drawer cart

Many Shopify themes use a slide-out drawer cart instead of a dedicated cart page. The view_cart event should fire when the drawer opens, not just on /cart page loads. If you only track the cart page, you’re missing 80%+ of cart views on themes that default to the drawer.

Testing checklist for Shopify stores

After setting everything up, verify each of these using proper debugging methods:

  • page_view fires on every page, including collection pages
  • view_item fires on product pages with correct variant, price, and SKU
  • view_item_list fires on collection pages with all visible products
  • select_item fires when clicking a product from a collection
  • add_to_cart fires with correct product data, including variant
  • view_cart fires when the cart page loads or drawer opens
  • remove_from_cart fires with the correct removed item
  • begin_checkout fires when checkout starts with full cart contents
  • add_shipping_info fires when shipping step completes
  • add_payment_info fires when payment details are entered
  • purchase fires exactly once per order with correct revenue, tax, and shipping
  • Transaction IDs are unique and match Shopify order numbers
  • Currency is consistent across all events
  • Cross-domain tracking works between storefront and checkout
  • Server-side purchase backup matches client-side data
  • Returning to the thank-you page does not create a duplicate transaction
  • Consent mode integration respects customer preferences

This is a long list. Every item on it represents a bug I’ve found on a real Shopify store. Skip any of them and you’ll end up with data that looks right until you need to make a decision with it. Then you’ll discover the gaps at the worst possible time.

Shopify is a great platform for running an online store. Its analytics integration, honestly, leaves a lot to be desired. The good news is that with the right setup, you can get data quality that matches or exceeds what you’d get on a fully custom site. It just takes more deliberate effort than Shopify’s “paste your measurement ID” setup implies.

AR

Artem Reiter

Web Analytics Consultant

Related Articles

Need help with your analytics?

Free 30-minute discovery call. I'll look at your setup, tell you what's broken, and whether I can help. No commitment.

Or email directly: artem@reiterweb.com