Every tracking migration ends at the same question: what exactly do Shopify's customer events give me, and what do I forward to my analytics? This is the reference I wish existed when I started. The funnel events that matter, their real payload shapes, and the GA4 wiring for each.
If you haven't created a pixel yet, start with adding a custom pixel without an app, then come back. Shopify's own standard events reference is the canonical field list; what follows is the practical subset.
The funnel in four events
analytics.subscribe("product_viewed", (event) => { /* PDP view */ });
analytics.subscribe("product_added_to_cart", (event) => { /* ATC */ });
analytics.subscribe("checkout_started", (event) => { /* begin checkout */ });
analytics.subscribe("checkout_completed", (event) => { /* purchase */ });
Every handler receives the same envelope: event.context is a snapshot of where it happened (page, navigator, window) and event.data is the commerce payload, different per event. You also get event.timestamp and event.id for deduplication.
product_added_to_cart
The payload is a cartLine:
analytics.subscribe("product_added_to_cart", (event) => {
const line = event.data.cartLine;
line.quantity; // how many were just added
line.cost.totalAmount.amount; // line cost, e.g. "39.98"
line.cost.totalAmount.currencyCode; // "USD"
line.merchandise.product.title; // parent product
line.merchandise.title; // variant title
line.merchandise.sku; // variant SKU
line.merchandise.price.amount; // unit price
});
And the GA4 mapping:
gtag("event", "add_to_cart", {
currency: line.cost.totalAmount.currencyCode,
value: Number(line.cost.totalAmount.amount),
items: [{
item_id: line.merchandise.sku || line.merchandise.product.id,
item_name: line.merchandise.product.title,
item_variant: line.merchandise.title,
quantity: line.quantity,
price: Number(line.merchandise.price.amount),
}],
});
checkout_started and checkout_completed
Both carry event.data.checkout. Same shape, two moments in time:
analytics.subscribe("checkout_completed", (event) => {
const checkout = event.data.checkout;
checkout.order?.id; // the order, only on completed
checkout.totalPrice.amount; // grand total after discounts + tax
checkout.subtotalPrice.amount; // before shipping and tax
checkout.currencyCode;
checkout.discountApplications; // applied discounts, with codes
checkout.lineItems; // [{ title, quantity, variant, ... }]
checkout.email; // when provided and consented
});
The purchase event, mapped:
analytics.subscribe("checkout_completed", (event) => {
const checkout = event.data.checkout;
gtag("event", "purchase", {
transaction_id: checkout.order?.id,
currency: checkout.currencyCode,
value: Number(checkout.totalPrice.amount),
coupon: checkout.discountApplications?.[0]?.title,
items: checkout.lineItems.map((item) => ({
item_id: item.variant?.sku || item.variant?.product?.id,
item_name: item.title,
quantity: item.quantity,
price: Number(item.variant?.price?.amount ?? 0),
})),
});
});
Two things worth internalizing before you ship this:
checkout_completedfires when the thank-you page renders. A buyer who closes the tab a beat early is a real order your pixel never saw. Browser-side numbers run a little under Shopify's order count for everyone. That's the nature of pixels, not a bug in yours. Treat the Shopify order admin as the source of truth and your analytics as directional.- Consent gates delivery. If the visitor hasn't granted the consent categories your pixel declared, the events simply don't arrive. Don't burn an afternoon debugging what is actually your privacy settings working as intended.
Custom events when the standard ones aren't enough
The sandbox can't watch your DOM. For anything Shopify doesn't publish, like size-guide opens or quiz completions, publish from theme code where the interaction happens:
// In the theme (has DOM access):
document.querySelector("#size-guide")?.addEventListener("click", () => {
Shopify.analytics.publish("size_guide_opened", {
productHandle: window.location.pathname.split("/").pop(),
});
});
// In the pixel:
analytics.subscribe("size_guide_opened", (event) => {
gtag("event", "size_guide_opened", {
product_handle: event.customData?.productHandle,
});
});
That split, theme publishes and pixel subscribes, keeps every outbound network call in one auditable place. The publish API is documented in the Web Pixels API reference.
Verify against reality
After wiring, place a test order and check that three numbers agree: Shopify's order total, the purchase event's value in your tracker's debug view, and the line-item count. If they match on one real order, your mapping is right. If conversions later drift low by a few percent, re-read the first note above before rewriting anything.
The same structured thinking applies to how AI agents read your store. Events are how you measure; structured data is how you're understood. The Customer Events migration guide covers the first, and AgentReady covers the second.

Comments
Every comment here comes from a verified email. Write yours, confirm from your inbox, and it's live.
Loading comments…