Building a full ecommerce dataLayer from a Shopify custom pixel

Copy-paste code for view_item through purchase, with item data.

If you run Google Tag Manager on Shopify, everything depends on the data layer your custom pixel builds. Get it right once, with GA4’s ecommerce format and complete item data, and every tag in GTM (GA4, Google Ads, Meta, TikTok, a server container) can use it. This guide gives you the full code, from view_item_list to purchase, and explains each choice.

It assumes you know the basics of Customer events. If not, start with how Shopify Customer Events work. To compare this method with GTM in your theme or an app, see how to set up Google Tag Manager on Shopify.

Before you paste anything

  • Remove other GA4 purchase sources. If the Google & YouTube app sends GA4 events, or GTM is still in theme.liquid, you will count everything twice. Pick one source.
  • Choose your item ID. SKU, product ID or variant ID. If you run Google Shopping, match the ID format in your Merchant Center feed so dynamic remarketing works.
  • Choose what value means. The code below sends the checkout total. Many stores prefer the subtotal after discounts, without shipping and taxes, for ads. Decide once and use it everywhere.
  • Have a GTM web container ready and its ID (GTM-XXXXXXX).

Step 1: create the custom pixel

  1. In Shopify, go to Settings → Customer events → Add custom pixel and name it, for example “GTM”.
  2. Set its permissions. “Not required” lets the code below manage Consent Mode itself; requiring analytics or marketing consent makes Shopify block the whole pixel until consent. See Consent Mode v2 on Shopify to choose.
  3. Paste the code from the steps below, in order, as one script.
  4. Save, then click Connect. A saved pixel that isn’t connected doesn’t run.

Step 2: consent and GTM loading

Consent defaults must be pushed before GTM loads. The pixel reads the visitor’s current choice from init.customerPrivacy and listens for changes.

const GTM_ID = "GTM-XXXXXXX";

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

const consentState = (p) => ({
  analytics_storage: p.analyticsProcessingAllowed ? "granted" : "denied",
  ad_storage: p.marketingAllowed ? "granted" : "denied",
  ad_user_data: p.marketingAllowed ? "granted" : "denied",
  ad_personalization: p.marketingAllowed ? "granted" : "denied"
});

gtag("consent", "default", { ...consentState(init.customerPrivacy), wait_for_update: 500 });

customerPrivacy.subscribe("visitorConsentCollected", (event) => {
  gtag("consent", "update", consentState(event.customerPrivacy));
});

(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_ID);

The last block is GTM’s standard snippet without its <script> tags, as in Shopify’s own GTM tutorial.

Step 3: helpers

Shopify sends money as objects ({ amount, currencyCode }) and products as variants. These helpers turn them into GA4’s format, so every event uses the same item structure.

const num = (money) => (money && money.amount != null ? Number(money.amount) : undefined);

const toItem = (variant, quantity = 1, extra = {}) => ({
  item_id: variant?.sku || variant?.product?.id,
  item_name: variant?.product?.title,
  item_variant: variant?.title,
  item_brand: variant?.product?.vendor,
  item_category: variant?.product?.type,
  price: num(variant?.price),
  quantity,
  ...extra
});

const pageData = (event) => {
  const doc = event.context.document;
  return {
    page_location: doc.location.href,
    page_referrer: doc.referrer,
    page_title: doc.title
  };
};

const push = (name, event, ecommerce, extra = {}) => {
  if (ecommerce) window.dataLayer.push({ ecommerce: null });
  window.dataLayer.push({
    event: name,
    event_id: event.id,
    ...pageData(event),
    ...extra,
    ...(ecommerce ? { ecommerce } : {})
  });
};

const checkoutData = (c) => ({
  currency: c.currencyCode,
  value: num(c.totalPrice),
  coupon: (c.discountApplications || [])
    .filter((d) => d.type === "DISCOUNT_CODE")
    .map((d) => d.title)
    .join(",") || undefined,
  items: c.lineItems.map((li) => toItem(li.variant, li.quantity))
});

Two details matter. ecommerce: null is pushed before each ecommerce event so GTM doesn’t merge items from the previous event. And event_id uses Shopify’s unique event ID, which you can reuse as the deduplication key for Meta’s Conversions API.

Step 4: browsing events

analytics.subscribe("page_viewed", (event) => push("page_view", event));

analytics.subscribe("collection_viewed", (event) => {
  const c = event.data.collection;
  push("view_item_list", event, {
    item_list_id: c.id,
    item_list_name: c.title,
    items: (c.productVariants || []).slice(0, 20).map((v, i) => toItem(v, 1, { index: i }))
  });
});

analytics.subscribe("search_submitted", (event) => {
  push("search", event, null, { search_term: event.data.searchResult.query });
});

analytics.subscribe("product_viewed", (event) => {
  const v = event.data.productVariant;
  push("view_item", event, {
    currency: v.price.currencyCode,
    value: num(v.price),
    items: [toItem(v)]
  });
});

Collection lists are capped at 20 items to keep the hit size reasonable. GA4 doesn’t need every product in a long collection.

Step 5: cart events

const cartLineEvent = (name) => (event) => {
  const line = event.data.cartLine;
  push(name, event, {
    currency: line.cost.totalAmount.currencyCode,
    value: num(line.cost.totalAmount),
    items: [toItem(line.merchandise, line.quantity)]
  });
};

analytics.subscribe("product_added_to_cart", cartLineEvent("add_to_cart"));
analytics.subscribe("product_removed_from_cart", cartLineEvent("remove_from_cart"));

analytics.subscribe("cart_viewed", (event) => {
  const cart = event.data.cart;
  if (!cart) return;
  push("view_cart", event, {
    currency: cart.cost.totalAmount.currencyCode,
    value: num(cart.cost.totalAmount),
    items: cart.lines.map((l) => toItem(l.merchandise, l.quantity))
  });
});

Step 6: checkout and purchase

analytics.subscribe("checkout_started", (event) => {
  push("begin_checkout", event, checkoutData(event.data.checkout));
});

analytics.subscribe("checkout_shipping_info_submitted", (event) => {
  push("add_shipping_info", event, checkoutData(event.data.checkout));
});

analytics.subscribe("payment_info_submitted", (event) => {
  push("add_payment_info", event, checkoutData(event.data.checkout));
});

analytics.subscribe("checkout_completed", (event) => {
  const c = event.data.checkout;
  push("purchase", event, {
    ...checkoutData(c),
    transaction_id: c.order?.id,
    tax: num(c.totalTax),
    shipping: num(c.shippingLine?.price)
  }, {
    customer_type: c.order?.customer?.isFirstOrder ? "new" : "returning"
  });
});

The order ID becomes transaction_id. It is what lets you reconcile GA4 with Shopify order by order, and it stops GA4 from counting the same order twice if the thank-you page is reloaded.

Step 7: the GTM side

  1. Create Data Layer Variables for page_location, page_referrer, page_title, event_id and customer_type.
  2. Create a Google tag with your measurement ID, fired on Initialization, with send_page_view set to false and page_location, page_referrer, page_title set from the variables.
  3. Create a GA4 Event tag for page_view on a Custom Event trigger named page_view, with the same page parameters.
  4. Create one GA4 Event tag for the ecommerce events, with the event name set to {{Event}}, “Send Ecommerce data” checked (source: Data Layer), and a Custom Event trigger matching the regex ^(view_item_list|view_item|search|add_to_cart|remove_from_cart|view_cart|begin_checkout|add_shipping_info|add_payment_info|purchase)$.
  5. Add customer_type as an event parameter on the purchase, and register it as a custom dimension in GA4.

The page parameters are not optional: without them, GA4 records the sandbox iframe’s address instead of your pages.

Step 8: test it

  • Set debug_mode to true on the Google tag while testing, and watch GA4’s DebugView while you browse, add to cart and check out. GTM’s preview mode often can’t attach to the sandbox.
  • Use Shopify’s Pixel Helper, from the Customer events page, to confirm the pixel loads and receives events.
  • In the browser console, switch the context to the pixel’s sandbox frame (its name starts with web-pixel-sandbox-CUSTOM) and type dataLayer to see what was pushed.
  • Place a real order with a discount code and check transaction_id, value, tax, shipping, coupon and items in DebugView.
  • After a few days, compare GA4 purchases with Shopify orders by order ID.

The full test plan is in Shopify tracking QA: 25 checks, and checkout-specific debugging in how to track Shopify checkout inside the pixel sandbox.

Frequently asked questions

Where do I paste the GTM code on Shopify?

In Settings → Customer events → Add custom pixel, without the <script> tags. Not in theme.liquid, which can’t reach the checkout.

Does GTM preview mode work with a Shopify custom pixel?

Rarely: Tag Assistant can’t reliably attach to a container inside the sandbox iframe. Use GA4 DebugView, Shopify’s Pixel Helper, the console in the sandbox frame, and server-side GTM preview if you have a server container.

Can the theme and the custom pixel share one dataLayer?

No. They live in different windows. If the theme has data the pixel needs, publish it as a custom event with Shopify.analytics.publish() and subscribe to it in the pixel.

Which item ID should I use: SKU, product ID or variant ID?

Whichever matches your Google Merchant Center feed if you run Shopping ads, so dynamic remarketing works. Otherwise SKU is the most readable. The important part is using the same one on every event.

Going further

  • Meta and TikTok can use the same data layer, with event_id as the deduplication key for their server-side APIs.
  • Google Ads enhanced conversions need customer data from the checkout, sent only with consent. See enhanced conversions for Shopify.
  • Server-side GTM can receive this data layer through a GA4 transport and forward it to every platform. See Shopify server-side tracking.

This data layer is the base of my Starter package (1,300€, with GA4 and Consent Mode v2) and Professional package (3,500€, with server-side tracking for every ad platform). See the Shopify tracking packages or book a free 30-minute call.

Elliot Latil

Full Stack Web Analytics expert: GA4, GTM, server-side tracking and Shopify. About me