Shopify GA4 Custom Pixel Generator

Get Google Analytics 4 ecommerce tracking as a Shopify custom pixel, with correct item data, consent mode and one purchase per order.

Describe the result you need. AI will set up the tool and ask for any missing details.

Configure with AI

Enter to submit · Shift + Enter for a new line

Sent to AI. Leave out passwords and private customer data.

Your details

Google Analytics

In GA4: Admin → Data streams → your web stream.

Use the same ID as your Google Merchant Center feed if you run Google Ads.

Passes Shopify's consent choices to Google.

Remembers each order ID in the shopper's browser.

Shows events in GA4 DebugView. Turn off when you're done.

Events to send

From page_viewed

From product_viewed

From collection_viewed

From search_submitted

From product_added_to_cart and product_removed_from_cart

From cart_viewed

From checkout_started

From checkout_shipping_info_submitted

From payment_info_submitted

From checkout_completed

Paste this into Settings → Customer events → Add custom pixel

ga4-custom-pixel.js
// Google Analytics 4 custom pixel for Shopify (Web Pixels API).
// Paste into Settings > Customer events > Add custom pixel, save, then click Connect.
const MEASUREMENT_ID = 'G-XXXXXXXXXX';
const ITEM_ID = 'sku'; // 'sku', 'variant' or 'product'
const PURCHASE_VALUE = 'items'; // 'items' or 'total'

// Load gtag.js (custom pixels can't contain HTML, so the script tag is added with JavaScript).
const script = document.createElement('script');
script.setAttribute('src', 'https://www.googletagmanager.com/gtag/js?id=' + MEASUREMENT_ID);
script.setAttribute('async', '');
document.head.appendChild(script);

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

// Consent mode: mirror the shopper's choices from Shopify's cookie banner.
const consentFrom = (privacy) => ({
  analytics_storage: privacy && privacy.analyticsProcessingAllowed ? 'granted' : 'denied',
  ad_storage: privacy && privacy.marketingAllowed ? 'granted' : 'denied',
  ad_user_data: privacy && privacy.marketingAllowed ? 'granted' : 'denied',
  ad_personalization: privacy && privacy.marketingAllowed ? 'granted' : 'denied',
});
gtag('consent', 'default', consentFrom(init.customerPrivacy));
api.customerPrivacy.subscribe('visitorConsentCollected', (event) => {
  gtag('consent', 'update', consentFrom(event.customerPrivacy));
});

gtag('js', new Date());
// Page views are sent from page_viewed below, with the real page URL instead of the sandbox URL.
gtag('config', MEASUREMENT_ID, { send_page_view: false });

/* ---------- Helpers ---------- */
const plainId = (id) => (id == null ? undefined : String(id).split('/').pop());
const money = (m) => (m && m.amount != null ? Number(m.amount) : 0);
const round = (x) => Math.round(x * 100) / 100;
const pageContext = (event) => ({
  page_location: event.context.document.location.href,
  page_title: event.context.document.title,
});

function toItem(variant, quantity, extra) {
  variant = variant || {};
  const product = variant.product || {};
  const ids = { sku: variant.sku || plainId(variant.id), variant: plainId(variant.id), product: plainId(product.id) };
  const variantTitle = variant.untranslatedTitle || variant.title;
  return Object.assign({
    item_id: ids[ITEM_ID] || plainId(variant.id),
    item_name: product.untranslatedTitle || product.title || variant.title,
    item_brand: product.vendor || undefined,
    item_category: product.type || undefined,
    item_variant: variantTitle && variantTitle !== 'Default Title' ? variantTitle : undefined,
    price: money(variant.price),
    quantity: quantity || 1,
  }, extra || {});
}

// Checkout lines: price is the unit price after discounts, discount is the saving per unit.
function checkoutItems(checkout) {
  return (checkout.lineItems || []).map((line, index) => {
    const quantity = line.quantity || 1;
    const unit = money(line.variant && line.variant.price);
    const allocated = (line.discountAllocations || []).reduce((sum, d) => sum + money(d.amount), 0);
    let lineTotal = unit * quantity - allocated;
    if (!line.discountAllocations && line.finalLinePrice) lineTotal = money(line.finalLinePrice);
    const price = round(Math.max(0, lineTotal) / quantity);
    const discount = round(Math.max(0, unit - price));
    return toItem(line.variant, quantity, { index, price, discount: discount || undefined });
  });
}
const itemsValue = (items) => round(items.reduce((sum, item) => sum + item.price * item.quantity, 0));
const couponOf = (checkout) =>
  (checkout.discountApplications || []).filter((d) => d.type === 'DISCOUNT_CODE').map((d) => d.title).join(',') || undefined;
function checkoutPayload(event) {
  const checkout = event.data.checkout;
  const items = checkoutItems(checkout);
  return Object.assign(pageContext(event), {
    currency: checkout.currencyCode,
    value: itemsValue(items),
    coupon: couponOf(checkout),
    items,
  });
}

/* ---------- Events ---------- */
analytics.subscribe('page_viewed', (event) => {
  gtag('event', 'page_view', Object.assign(pageContext(event), {
    page_referrer: event.context.document.referrer,
  }));
});

analytics.subscribe('product_viewed', (event) => {
  const variant = event.data.productVariant;
  gtag('event', 'view_item', Object.assign(pageContext(event), {
    currency: variant.price.currencyCode,
    value: money(variant.price),
    items: [toItem(variant, 1)],
  }));
});

analytics.subscribe('collection_viewed', (event) => {
  const collection = event.data.collection;
  gtag('event', 'view_item_list', Object.assign(pageContext(event), {
    item_list_id: plainId(collection.id),
    item_list_name: collection.title,
    items: (collection.productVariants || []).slice(0, 50).map((variant, index) => toItem(variant, 1, { index })),
  }));
});

analytics.subscribe('search_submitted', (event) => {
  gtag('event', 'search', Object.assign(pageContext(event), {
    search_term: event.data.searchResult.query,
  }));
});

const cartLineEvent = (name) => (event) => {
  const line = event.data.cartLine;
  if (!line) return;
  gtag('event', name, Object.assign(pageContext(event), {
    currency: line.cost.totalAmount.currencyCode,
    value: money(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;
  gtag('event', 'view_cart', Object.assign(pageContext(event), {
    currency: cart.cost.totalAmount.currencyCode,
    value: money(cart.cost.totalAmount),
    items: (cart.lines || []).map((line) => toItem(line.merchandise, line.quantity)),
  }));
});

analytics.subscribe('checkout_started', (event) => {
  gtag('event', 'begin_checkout', checkoutPayload(event));
});

analytics.subscribe('checkout_shipping_info_submitted', (event) => {
  const delivery = event.data.checkout.delivery;
  const option = delivery && delivery.selectedDeliveryOptions && delivery.selectedDeliveryOptions[0];
  gtag('event', 'add_shipping_info', Object.assign(checkoutPayload(event), {
    shipping_tier: option ? option.title : undefined,
  }));
});

analytics.subscribe('payment_info_submitted', (event) => {
  gtag('event', 'add_payment_info', checkoutPayload(event));
});

analytics.subscribe('checkout_completed', async (event) => {
  const checkout = event.data.checkout;
  const order = checkout.order || {};
  const transactionId = plainId(order.id) || checkout.token;
  // Skip orders this browser has already reported.
  const key = 'ga4_purchase_' + transactionId;
  try {
    if (await browser.localStorage.getItem(key)) return;
    await browser.localStorage.setItem(key, '1');
  } catch (e) {}
  const customer = order.customer || {};
  gtag('event', 'purchase', Object.assign(checkoutPayload(event), {
    transaction_id: transactionId,
    value: PURCHASE_VALUE === 'total' ? money(checkout.totalPrice) : itemsValue(checkoutItems(checkout)),
    tax: money(checkout.totalTax),
    shipping: money(checkout.shippingLine && checkout.shippingLine.price),
    customer_type: customer.isFirstOrder === true ? 'new' : customer.isFirstOrder === false ? 'returning' : undefined,
  }));
});

Replace G-XXXXXXXXXX with your own measurement ID before you save the pixel.

To add it: Settings → Customer events → Add custom pixel. Name it, paste the code into the Code box, click Save, then Connect. By default Shopify only runs custom pixels after the shopper allows Marketing and Analytics in regions that need consent; you can change that under Customer privacy → Permission.

Remove any other GA4 tag for the same property (gtag in theme.liquid, Google Tag Manager, or GA4 through the Google & YouTube app). Otherwise every event, including purchases, is counted twice.

How Shopify events map to GA4

Custom pixels run in a sandbox and can't read your pages, so the pixel listens to Shopify's standard customer events with analytics.subscribe and turns each one into a GA4 ecommerce event.

Shopify eventGA4 eventMain data
page_viewedpage_viewReal page URL, title and referrer
product_viewedview_itemevent.data.productVariant
collection_viewedview_item_listevent.data.collection
search_submittedsearchevent.data.searchResult.query
product_added_to_cart / product_removed_from_cartadd_to_cart / remove_from_cartevent.data.cartLine
cart_viewedview_cartevent.data.cart
checkout_startedbegin_checkoutevent.data.checkout
checkout_shipping_info_submittedadd_shipping_infoSelected delivery option as shipping_tier
payment_info_submittedadd_payment_infoevent.data.checkout
checkout_completedpurchaseOrder ID, totals, tax, shipping, codes

Each item carries item_id, item_name, brand (vendor), category (product type), variant, price and quantity. Names use the untranslated product title when Shopify provides it, so reports don't split by language.

Purchase value, discounts and duplicates

Google asks for value to be the sum of price × quantity for the items, without shipping or tax, and for price to be the discounted unit price with the saving in discount. The pixel works out each line's price after its discount allocations.

One order, two ways to report it

Items after discounts
$132.00
Shipping
$8.00
Tax
$10.56

With Items after discounts, GA4 gets value: 132 plus shipping: 8 and tax: 10.56. With Order total, value is 150.56, which matches Shopify's total sales but not Google's definition.

Shopify fires checkout_completed once per checkout. The pixel sends the order ID as transaction_id and, with duplicate blocking on, also remembers it in the browser, so a reloaded thank-you page never sends a second purchase.

Installing and testing the pixel

  1. 01

    Remove old GA4 code

    Delete GA4 from theme.liquid, Google Tag Manager or checkout additional scripts, and don't send the same property through the Google & YouTube app as well.

  2. 02

    Add the pixel

    Go to Settings → Customer events → Add custom pixel, name it, paste the code, click Save, then Connect.

  3. 03

    Check consent

    By default new custom pixels need Marketing and Analytics permission in regions that require consent. With consent mode on, the pixel also passes each choice to Google.

  4. 04

    Test

    Generate with Debug mode on, browse and place a test order, and watch GA4 → Admin → DebugView. Then switch debug mode off.

Questions people ask

Why does GA4 show a strange page URL from Shopify?

Custom pixels run in a sandbox, so window.location isn't your page. The pixel sends the real URL from event.context.document.location.href instead, and turns off gtag's automatic page view.

Should item_id be the SKU or the variant ID?

Use whatever your Google Merchant Center feed uses, so Google Ads can match products. If you don't run Shopping ads, SKU is the easiest to read in reports.

Will this double count with the Google & YouTube app?

Yes, if both send to the same GA4 property. Use one or the other.

Does it track purchases on the thank-you page after a reload?

Shopify only fires checkout_completed once per checkout, and the pixel also skips order IDs it has already sent from that browser.

Can it track button clicks or form fills?

Not by itself. The sandbox can't watch your pages. Publish custom events from your theme with Shopify.analytics.publish and subscribe to them in the pixel.

Want this builtinto your store?

I'm Emmanuel, a Shopify and WooCommerce developer. If this tool pointed at something you'd rather have fixed for you (a faster store, cleaner product data, a migration, a custom feature), send me a note.

Emmanuel Asika

Emmanuel Asika

Shopify & WooCommerce developer, Dublin

I build and fix online stores: new builds, migrations, speed, SEO and the custom bits apps can't do.

hello@emmanuelasika.com