Nine out of ten GA4 ecommerce tracking via GTM setups I audit are broken. Not “slightly improvable”: broken. An empty items array, a transaction_id that duplicates, a purchase that fires twice, or a value set to null that turns your revenue into zeros in the reports. The worst part is that everything looks fine: events show up, real time animates, everyone is happy. Then someone compares GA4 revenue to Stripe and finds a 30% gap. This guide takes the topic from the ground up and gives you the setup that holds: the exact dataLayer structure, the step-by-step GTM configuration, the five errors that skew your purchase reports, and how to extend the whole thing server-side to recover the signals that ad blockers eat.
If the dataLayer is still fuzzy for you, read the complete GTM dataLayer guide first: it is the foundation everything else rests on. And if your GA4 property is not set up yet, start with setting up GA4 from scratch.
GA4 ecommerce tracking is not Universal Analytics Enhanced Ecommerce
First thing to internalize, because it shapes everything else: forget what you knew from Universal Analytics. The Enhanced Ecommerce you configured through the dedicated GTM plugin is gone. Dead, buried, retired alongside UA. GA4 changed the paradigm.
Concretely, there is no more “plugin” and no special tag type. Everything runs through what Google calls recommended events, a standardized list (view_item, add_to_cart, purchase, and so on), each carrying an items parameter that describes the products involved. You push these events into the dataLayer, GTM reads them and forwards them to GA4 through a plain GA4 Event tag. It is both simpler (one single mechanism) and less forgiving (if your items array is malformed, GA4 will not warn you, it will swallow the data silently).
The other big change is the shape of the items object. In UA you used id, name, category. In GA4 the names changed: item_id, item_name, item_category. Copy-pasting an old UA dataLayer is the fastest way to break everything.
The recommended events to implement
You do not have to track everything on day one. Here are the events by priority, from the bare minimum to the full funnel.
| Event | Fires when | Priority |
|---|---|---|
view_item_list | A list is shown (category, search) | Full funnel |
select_item | A product in a list is clicked | Full funnel |
view_item | A product page is viewed | Recommended |
add_to_cart | Item added to cart | Minimum |
remove_from_cart | Item removed from cart | Full funnel |
begin_checkout | User enters the checkout | Recommended |
add_payment_info | Payment method entered | Full funnel |
purchase | Order confirmed | Minimum |
If you only have the budget for two events, implement add_to_cart and purchase. They give you the add-to-cart rate, the revenue, and they are enough to feed your remarketing audiences and your Google Ads conversions. The rest enriches funnel analysis, but purchase is the one that maps to your invoicing: get it right first.
The exact dataLayer structure
Here is the heart of the matter. Two complete, commented examples, ready to adapt. Remember the golden rule: always push ecommerce: null right before, to clear the previous object so one event’s data does not bleed into the next.
add_to_cart
window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null }); // clear the previous object
dataLayer.push({
event: "add_to_cart",
ecommerce: {
currency: "USD",
value: 29.90,
items: [
{
item_id: "SKU_12345",
item_name: "Organic cotton t-shirt",
item_brand: "MyBrand",
item_category: "Apparel",
price: 29.90,
quantity: 1,
coupon: "SUMMER10" // optional
}
]
}
});
purchase
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: "purchase",
ecommerce: {
transaction_id: "ORD-2026-08-0042", // REQUIRED and UNIQUE
currency: "USD",
value: 59.80, // total; with or without shipping, but stay consistent
tax: 9.97,
shipping: 4.90,
coupon: "SUMMER10",
items: [
{
item_id: "SKU_12345",
item_name: "Organic cotton t-shirt",
item_brand: "MyBrand",
item_category: "Apparel",
price: 29.90,
quantity: 2
}
]
}
});
The item_id, item_name, price, quantity and currency parameters are your non-negotiable core. The optional ones (item_brand, item_category, coupon, item_variant) enrich your product reports, but a missing item_id alone makes an order line unusable. One note on value: it is a number, never a string. "59.80" in quotes is one of the most common mistakes, and GA4 will treat your value as absent.
Step-by-step GTM configuration
Once the dataLayer is in place on the site, the GTM work is mechanical.
- Create a dataLayer variable named
ecommerce, version 2, that pulls the fullecommerceobject from the push. - Create a custom event trigger for each event: type “Custom Event”, event name
purchase(thenadd_to_cart, and so on). - Create a GA4 Event tag: event name
purchase, and under parameters, additemsmapped to{{ecommerce}}. 2026 tip: turn on the “Send Ecommerce data” option and select “Data Layer” so GTM picks up theitemsstructure automatically, without mapping each field by hand. - Attach the tag to its matching trigger.
- Test in Preview mode: add a product, place a test order, and check in the GA4 tab of Tag Assistant that the event fires with a populated
itemsarray and a presenttransaction_id.
Preview mode is your best friend. Never publish an ecommerce container without seeing, with your own eyes, the purchase fire with the right structure on a real test order.
The 5 errors that break your purchase reports
I have audited enough accounts to build a top five. Here they are, in decreasing frequency.
1. The empty or missing items array. The purchase event fires, GA4 records a transaction, but no product is attached to it. Your “Purchases by item” reports stay hopelessly empty. Classic cause: the dataLayer is pushed after the tag fires (a timing race), or the items array is built from a cart that was already emptied after payment.
2. The duplicated transaction_id. GA4 deduplicates purchases on this identifier. If your confirmation page reloads and re-pushes the same transaction_id, GA4 ignores the duplicate (good news), but if you generate a different ID on each reload, you count the same sale several times. Always use the real order number, never a timestamp or a random value.
3. The missing currency. Without a currency, GA4 cannot convert or consolidate your revenue, and the value is ignored. The currency must be present at the ecommerce level (ISO 4217 format: USD, EUR).
4. The event counted twice, client-side and server-side. When you add a server-side layer (see below) without turning off the client-side send, your purchase fires through both paths. Result: doubled revenue. Pick a single send path per conversion event.
5. The value set to null or as a string. value: null, value: "0" or value: "": in all three cases, your revenue collapses to zero in the reports while the transactions themselves are still counted. Make sure value is a strictly positive number.
Extending server-side
Your client-side purchase works. Nice. But it has an Achilles heel: it lives in the browser, so ad blockers, Safari’s ITP and privacy extensions wipe out a share of it before it reaches Google. Depending on your audience, that is 15 to 30% of signals lost.
The fix is to also send the purchase from a server-side GTM (sGTM) container, which receives the data first-party and relays it to GA4 out of reach of blockers. In 2026, 67% of the advanced ecommerce sites I come across have made the move, because those 15 to 30% of recovered conversions directly change the reported profitability of their campaigns.
The principle: your purchase event goes to your sGTM endpoint rather than straight to GA4, the server enriches and deduplicates, then forwards. Watch out for pitfall number 4 above: when you switch to server-side, turn off the client-side send of the same event, or deduplicate cleanly. For the full setup, I walk through it in Server-Side GTM: why and how to migrate. If you are on Shopify, the native server layer is covered in Shopify GA4 server-side. And to send that same purchase signal to Google Ads without relying on the third-party cookie, look at server-side Enhanced Conversions.
Actionable recap
If you keep only the essentials: implement add_to_cart and purchase cleanly first, with a populated items array, a present currency, a numeric value and a unique transaction_id pulled from your real order number. Test each event in Preview mode on a real order before publishing. Then compare your GA4 revenue to your back office: a gap above 5% is the symptom of one of the five errors above. Once the client-side is reliable, move to server-side to recover the 15 to 30% of signals blockers take from you. That order, not the reverse, is how you build ecommerce tracking that holds.