E-commerce Tracking in GA4
GA4's e-commerce model: everything is an event with items
Universal Analytics had dedicated e-commerce 'transaction' and 'product' hit types bolted onto the data model. GA4 scraps that distinction: e-commerce is just a set of recommended events (view_item, add_to_cart, begin_checkout, add_shipping_info, add_payment_info, purchase, refund) that all share one structural convention — an items array of objects, each describing a single product with a consistent set of item-scoped parameters.
This uniformity is the whole point. Once you understand the items array shape, you understand the schema for all seven events — only the event-level parameters around it change (e.g. purchase adds transaction_id and value; view_item doesn't need either).
{
"event": "purchase",
"ecommerce": {
"transaction_id": "T_12345",
"value": 72.50,
"tax": 4.50,
"shipping": 5.00,
"currency": "USD",
"coupon": "SPRING10",
"items": [
{
"item_id": "SKU_RED_TSHIRT",
"item_name": "Red T-Shirt",
"item_category": "Apparel",
"item_variant": "Medium",
"price": 25.00,
"quantity": 2,
"item_brand": "Acme",
"discount": 2.50
},
{
"item_id": "SKU_CAP",
"item_name": "Baseball Cap",
"item_category": "Accessories",
"price": 17.50,
"quantity": 1
}
]
}
}Item-scoped vs event-scoped parameters
A persistent source of confusion: currency and value can appear at the event level (total order value) while price and quantity live inside each item. Mixing these up — e.g. putting the full order total into every item's price field — silently inflates revenue reporting by a multiple of the item count, because GA4 sums item-level values when reconciling e-commerce reports.
Item-scoped parameters (item_id, item_name, item_category, item_brand, item_variant, price, quantity, discount, coupon at item level) describe one SKU. Event-scoped parameters (currency, value, transaction_id, shipping, tax, coupon at order level) describe the overall action. item_id is the single most important field — it's the join key GA4 uses to roll up purchases, views, and cart adds into per-product reports, so it must be consistent across every event type for the same SKU (don't send SKU-001 from the PDP and sku_001 from checkout).
Core e-commerce events and when they fire
| Event | Fires when | Required ecommerce fields |
|---|---|---|
| view_item | Product detail page loads | items[] with item_id, item_name, price |
| add_to_cart | User adds a product to cart | items[] with quantity added |
| begin_checkout | Checkout flow starts | items[], value, currency |
| add_shipping_info | Shipping method selected | items[], shipping_tier |
| add_payment_info | Payment method entered | items[], payment_type |
| purchase | Order confirmed | transaction_id, value, currency, items[] |
| refund | Order or line item refunded | transaction_id, (optionally items[] for partial refund) |
Building a reliable data layer
GA4 doesn't read your page's DOM or your database directly — it reads a JavaScript object conventionally called the dataLayer, pushed by your site's frontend (or backend-rendered into the page) just before the corresponding GTM tag fires. The reliability of your e-commerce reporting is entirely bounded by the reliability of this push.
The most common failure pattern: the dataLayer.push() for purchase happens on the client, on the confirmation page render. If the user closes the tab, loses connectivity, or an ad blocker kills the GTM container before that page finishes loading, the purchase event never fires — and GA4 permanently undercounts revenue with no way to recover it after the fact. The fix is the same pattern cross-domain tracking and CAPI both lean on: fire the critical conversion event server-side (via Measurement Protocol or server-side GTM) using your order-confirmation webhook as the trigger, not a client-side page load, and keep the client-side event as a secondary signal rather than the sole source of truth.
window.dataLayer = window.dataLayer || [];
dataLayer.push({
event: "add_to_cart",
ecommerce: {
currency: "USD",
value: 25.00,
items: [{
item_id: "SKU_RED_TSHIRT",
item_name: "Red T-Shirt",
price: 25.00,
quantity: 1
}]
}
});Clear the ecommerce object between pushes
items in the dataLayer and your next push doesn't explicitly overwrite the ecommerce key, GTM's Ecommerce variable can read a mix of old and new item data. The documented fix is to push { ecommerce: null } immediately before pushing the next e-commerce event, clearing the object so each event's data layer read is self-contained.Funnel reporting depends entirely on item_id consistency
GA4's built-in Purchase Journey and e-commerce funnel reports (in Advertising and Explore) stitch view_item → add_to_cart → begin_checkout → purchase together using item_id as the join key per session. If your PDP sends item_id: '4521' but your cart and checkout send the parent product's variant ID instead of the SKU, the funnel reports will show inflated drop-off between view and cart — not because users actually abandoned, but because GA4 can no longer connect the events to the same product identity.
Before trusting any drop-off percentage in a GA4 funnel report, spot-check that the same item_id scheme is used identically across every event in the data layer implementation, ideally sourced from a single product catalog ID rather than independently generated at each step of the frontend.
Enhanced conversions and CAPI both need the same clean data layer
What's next
With clean, item-accurate conversion events flowing, the next layer is making sure identity survives domain handoffs during checkout — see Cross-Domain Tracking — and then feeding these same events server-side into ad platforms, as covered in Meta CAPI Setup.
Next: Cross-Domain Tracking →
I build these systems professionally.
Whether it's a RAG pipeline, analytics migration, or AI workflow — let's talk.