Ecommerce events, start to finish
view_item to purchase with the items[] array done right, the refresh guard, and where revenue quietly goes wrong.
Ecommerce is where tracking earns its keep and where it breaks most expensively. The good news: GA4 has one fixed vocabulary, and if the site pushes it correctly, the container side is four nearly-identical tags.
The funnel
| Step | Event | Pushed when |
|---|---|---|
| Browse | view_item_list |
a list of products renders |
select_item |
a product in a list is clicked | |
| Product | view_item |
product page loads |
| Cart | add_to_cart / remove_from_cart |
the cart changes |
view_cart |
cart page loads | |
| Checkout | begin_checkout |
checkout starts |
add_shipping_info / add_payment_info |
those steps complete | |
| Done | purchase |
the order is confirmed — on the thank-you page |
| Later | refund |
from the back office, usually server-side |
Every one carries an ecommerce object with currency, value, and items[]. purchase adds transaction_id, and should carry tax and shipping so value can be product revenue.
The items[] array
Each item is an object; the fields GA4 understands:
{
item_id: "SKU-1001", // one of item_id / item_name is required
item_name: "Canvas Tote",
item_brand: "TagIntact Goods",
item_category: "Bags", // up to item_category5
item_variant: "Natural",
price: 24, // unit price, number
quantity: 1,
index: 0, // position in a list, for select_item / view_item_list
item_list_id: "search_results",
coupon: "WELCOME10",
discount: 2.4
}
The mistakes that show up in the auditor and in reports, in order of frequency:
priceas a string —"24.00"or"$24". GA4 wants a number. Revenue-by-item reports go blank.itemsas an object instead of an array with one element.valuethat doesn't equal the sum ofprice × quantity— fine on purpose (discounts), confusing by accident.- Missing
transaction_id— GA4 can't dedupe, and a refreshed thank-you page becomes a second order. currencymissing — the hit is accepted; revenue in the reports shows as (not set).
The container side
Because the site does the work, each event is the same tag with a different name:
- GA4 Event tag · Event name
add_to_cart· Send Ecommerce data from Data Layer · trigger: Custom Eventadd_to_cart.
Copy it four or five times, change the name and trigger. Do not type items parameters by hand; that's what Send Ecommerce data is for. The one thing worth adding manually is value and currency as explicit event parameters when the site's push is unreliable — but the real fix is the push.
Google Ads conversions ride the same trigger: a Google Ads Conversion Tracking tag on the purchase Custom Event, with {{DL – ecommerce.value}}, {{DL – ecommerce.currency}} and {{DL – ecommerce.transaction_id}} as the Conversion Value / Currency / Transaction ID. Same event, two tags, one source of truth.
The refresh guard
This is the bug every store has had at least once. The thank-you page pushes purchase on load; the customer refreshes, or comes back from their email; the push runs again; revenue is now 2× for that order.
Fixes, best first:
- Server-side idempotence — the page only renders the push the first time the order is viewed.
- A client-side flag —
sessionStorageor a cookie keyed totransaction_id, checked before pushing. The sandbox thank-you page does exactly this; reload it and watch no secondpurchaserequest appear. - GA4's own dedupe — GA4 will ignore a repeated
purchasewith the sametransaction_idwithin a session. Useful, not sufficient: sessions end.
Prove it end to end
Walk the sandbox from home to thank-you with Preview connected and the network tab filtered to collect. You should see seven events in order, each with one request, purchase carrying pr1… items and epn.value in the payload, and nothing on refresh. That sequence — a journey, replayed, with assertions on each step — is what the monitor automates on a schedule. For now, you've done it by hand, which is how you'll know when the automation is lying to you.
The sandbox store fires real ecommerce dataLayer events and loads your GTM container, so you can build the tag from this lesson against live events.
Open the sandbox