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.
Your details
Paste this into Settings → Customer events → Add custom pixel
// 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 event | GA4 event | Main data |
|---|---|---|
page_viewed | page_view | Real page URL, title and referrer |
product_viewed | view_item | event.data.productVariant |
collection_viewed | view_item_list | event.data.collection |
search_submitted | search | event.data.searchResult.query |
product_added_to_cart / product_removed_from_cart | add_to_cart / remove_from_cart | event.data.cartLine |
cart_viewed | view_cart | event.data.cart |
checkout_started | begin_checkout | event.data.checkout |
checkout_shipping_info_submitted | add_shipping_info | Selected delivery option as shipping_tier |
payment_info_submitted | add_payment_info | event.data.checkout |
checkout_completed | purchase | Order 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
- 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. - 02
Add the pixel
Go to Settings → Customer events → Add custom pixel, name it, paste the code, click Save, then Connect.
- 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.
- 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.
Related tools
Last reviewed 30 September 2026.
Sources: Shopify: Web Pixels API standard events · Shopify: checkout_completed event · Shopify: Pixel privacy (customerPrivacy) · Shopify: Web Pixels browser API · Shopify Help Center: Create custom pixel code · Shopify Help Center: Manage custom pixels · Shopify Help Center: Pixels sandbox limitations · Google: GA4 recommended events reference
