Every couple of months we get a new client whose ecommerce tracking technically “works”. Purchases are being recorded, revenue is landing in a spreadsheet somewhere, and everyone’s more or less happy. Then we open GA4’s Monetization section and it’s empty. Or the Items report is empty. Or there’s a custom dimension called product_title doing the exact job that item_name was built to do, except none of GA4’s native reports know it exists.
This isn’t a rare mistake. It’s the default outcome when ecommerce tracking is built by someone who knows the business and the checkout flow intimately, but who hasn’t gone through Google’s recommended ecommerce events line by line first. The result is tracking that answers the questions you thought to ask, and nothing else, because every report has to be hand-built from scratch.
Today I want to make the case for following Google’s recommended schema properly, using a real implementation, integrating the promotion event to the ecommerce funnel. When it’s done right, you don’t just get “the data”. You get an entire library of GA4 reports, explorations, audiences, and downstream integrations for free.
The Recommended Schema, Briefly
GA4’s ecommerce events aren’t arbitrary. Each one maps to a stage of the shopping journey, and each one expects a specific, documented set of parameters:
- view_item_list, select_item: browsing and list interactions
- view_item, add_to_cart, remove_from_cart: product and cart interactions
- view_promotion, select_promotion: promotional/merchandising interactions
- begin_checkout, add_shipping_info, add_payment_info: checkout steps
- purchase, refund: kind of self explanatory
Sitting inside (almost) all of these is an items array, where each item carries its own scoped parameters: item_id, item_name, item_category, price, quantity, item_list_name, item_list_id, promotion_id, promotion_name, creative_name, creative_slot, and so on. Event-level parameters like currency, value, transaction_id, and coupon sit outside the array.
None of this is a suggestion in the “nice to have” sense. GA4’s standard reports are quite literally built to read these exact event and parameter names. Rename add_to_cart to addToBasket, or send item_name as a flat event parameter instead of inside the items array, and the reports that were meant to populate automatically simply stay blank. You haven’t broken GA4. You’ve just built tracking it doesn’t recognise.
The Promotion Example
When viewing some items in their cart the user might be presented with an upsell opportunity. +x items for a smaller price. Clicking the upsell would trigger a cart_upsell event, and from then on, the ecommerce checkout events would have an additional custom parameter labelling these items as upsell items. In GA4, custom reports were created to analyse the “upsell funnel”. To some extent, this worked, but the information was very fragmented, and sometimes inaccurate as well. The reported value of a purchase with an upsell was always the total value, so they weren’t able to figure out how much of that purchase actually came from an upsell.
While engineering a way of making this more straightforward and accurate, I reverted to the GA recommended events and thought ‘this is just a promotion’, and GA4 already has a promotion schema for it.
By sending promotion_id and promotion_name (Upsell – Cart, in this case) as item-scoped parameters inside the items array on the relevant events, alongside the standard add_to_cart and checkout_completed/purchase events, we can now see the the Promotions report under Monetization populated automatically. No custom report, no new dimension registration.



Look at what falls out of that for free: items viewed in promotion, items clicked in promotion, item promotion click-through rate, items added to cart, items checked out, items purchased, and item revenue, all segmented by promotion name and by item. That’s a genuinely useful piece of merchandising analysis, and nobody had to write a line of custom reporting logic to get it. It exists because the parameters were named the way Google expects them to be named.

The same principle applies to the funnel. Because add_to_cart, the promotion view/select interactions, and purchase are all standard events with standard item-scoped data, we can build a full funnel exploration, Add to Cart → View Promotion → Select Promotion → Purchase, straight out of GA4’s Explore tab. No new events needed to answer “where do people drop off in the upsell flow”, because the underlying data was already shaped to answer it.

Compare that to the alternative universe where the upsell is tracked with a custom event like upsell_shown and a custom dimension called upsell_product. You’d get the raw counts, sure, but no Promotions report, no automatic item-level revenue attribution, no compatibility with Merchant Center or Performance Max feeds that also read the standard schema, and every new question (“what’s our promotion click-through rate by source?”) means another exploration built by hand instead of a report that already exists.
One last thing, sending the items array correctly will start populating every report that uses items correctly as well, enabling much more nuanced reporting:

Common Pitfalls We See Constantly
A few patterns come up again and again when we audit ecommerce implementations. Most of them come from good intentions, just not from reading the schema first.
Renaming standard events
addToCart, add_item_to_cart, AddToBasket… we’ve seen all of them, and each one quietly opts the site out of every GA4 report built around add_to_cart. The event still fires, GA4 still logs it as a custom event, but none of the ecommerce reporting, none of the Google Ads dynamic remarketing signals, none of the automatic Merchant Center linking, know it happened.
Flattening the items array
Sending item_name and price as single event-level parameters works fine when there’s exactly one product involved. It falls apart the moment a cart, an order, or a promotion view contains more than one item, because there’s nowhere to put the second one. We regularly see event-scoped custom dimensions built to work around this (item_name_2, item_name_3…) when the items array was designed to hold an arbitrary number of items correctly from the start.
Missing or duplicated transaction_id
This is the one that inflates revenue without anyone noticing for months. GA4 deduplicates purchases using transaction_id. If it’s missing, every refresh of the confirmation page counts as a new sale. If it’s reused across genuinely different orders, GA4 silently drops the second one as a duplicate. Either way, finance and analytics stop agreeing on revenue, and nobody can say why.
Getting parameter scope wrong
Promotion and item-list parameters are supposed to live inside each item, not at the event level, because different items in the same cart can belong to different promotions or lists. We regularly see promotion_name set once at the event level for a multi-item add_to_cart hit, which means every item in that cart gets credited to the same promotion, even the ones that had nothing to do with it.
Wrong data types
price and quantity need to be numbers. “29.99” as a string will often still show up in the interface, but it can silently break aggregation, sorting, and some of GA4’s automatic value-based bidding signals downstream in Google Ads. Same story for value and currency, if currency is missing, GA4 defaults to using its property currency, meaning your ¥15,000 JPY sale may be recorded as $15,000 USD, and that is a big no-no.
What You Actually Get When This Is Done Properly
This is the part that, for me, makes the effort worth it. When ecommerce is implemented against the recommended schema, and only then, you get:
- Monetization reports (Ecommerce purchases, Purchase journey, Promotions) populated with zero extra configuration
- Item-level analysis by name, category, brand, list, and promotion, all sliceable and comparable without touching BigQuery
- Funnel and path explorations built on demand, because the events you need are already the ones GA4 expects
- Clean BigQuery export, with items arriving as a proper nested/repeated field instead of a pile of numbered custom dimensions someone has to unpick with string parsing
- Downstream compatibility with Google Ads dynamic remarketing, Merchant Center, and Performance Max, all of which read the same standard event and parameter names GA4 does
- Predictive metrics and audiences (purchase probability, predicted revenue), which Google trains on standard ecommerce events and won’t generate reliably from a bespoke schema
None of this is exotic. It’s all sitting there, switched on, waiting for events and parameters with the right names. The promotions example above isn’t a clever workaround, it’s what happens when you use the schema as documented instead of designing around it.
The Takeaway
Before building a custom event, a custom dimension, or a bespoke report for something ecommerce-shaped, it’s worth five minutes checking whether Google’s recommended schema already covers it. More often than not, it does, and building around it instead of adopting it just means more work for you and a smaller set of reports for everyone using GA4 downstream.
If you want a second pair of eyes on your ecommerce implementation, or overall implementation, whether that’s a full audit against the recommended schema or help untangling a few years of custom events that have crept in, give us a shout at Duga and we’ll take it from there.
