GA4 · · Last updated: May 19, 2026

GA4 ecommerce tracking: the setup guide nobody gave you

GA4 ecommerce events look simple in the docs. In practice, most implementations are missing half the data. Here's how to do it properly.

GA4 ecommerce tracking: the setup guide nobody gave you

Google’s documentation makes ecommerce tracking look like a 15-minute job. Drop in some events, push an items array, done. I’ve audited over 60 GA4 ecommerce implementations in the last two years, and I can tell you: maybe three of them were sending complete, accurate data on day one.

The rest? Missing currency codes. Duplicate transactions inflating revenue by 30%. Item arrays that only contain the product name and nothing else. And my personal favorite: a Shopify store that had been running for eight months with a purchase event that fired on the order confirmation page and the “thank you for signing up for our newsletter” page.

This guide covers what Google’s docs skip. I’m going to walk through the full event chain, the parameters most setups forget, the platform-specific traps, and how to actually verify your data before you start making business decisions based on it.

The full ecommerce event chain

GA4 ecommerce tracking isn’t a single event. It’s a sequence, and each step feeds into the next. Here’s the complete chain:

  1. view_item_list - User sees a product listing (category page, search results)
  2. select_item - User clicks on a product from a list
  3. view_item - User lands on a product detail page
  4. add_to_cart - User adds a product to their cart
  5. view_cart - User views their cart
  6. begin_checkout - User starts the checkout process
  7. add_shipping_info - User enters shipping details
  8. add_payment_info - User enters payment details
  9. purchase - Transaction completes
  10. refund - Transaction gets refunded (we’ll get to this one)

Most implementations I see have three of these. Usually view_item, add_to_cart, and purchase. Sometimes begin_checkout if someone was feeling thorough.

The problem with skipping events isn’t just incomplete funnel data. It’s that you lose the context that connects them. When you fire select_item with an item_list_name of “search_results” or “homepage_recommendations,” you can trace which product discovery mechanism actually drives revenue. Without it, you’re guessing.

What most setups miss: item-level parameters

Here’s where things get ugly. The items array is where 80% of implementations fall apart.

A minimal items array looks like this:

items: [{
  item_id: "SKU_12345",
  item_name: "Blue Running Shoes"
}]

That’s technically valid. GA4 won’t throw an error. But it’s nearly useless for analysis.

Here’s what a complete items array should contain:

items: [{
  item_id: "SKU_12345",
  item_name: "Blue Running Shoes",
  item_brand: "Nike",
  item_category: "Footwear",
  item_category2: "Running",
  item_category3: "Men's",
  item_variant: "Size 10 / Blue",
  price: 129.99,
  quantity: 1,
  discount: 20.00,
  coupon: "SUMMER20",
  item_list_id: "category_running",
  item_list_name: "Running Shoes",
  index: 3
}]

The item_category hierarchy (up to 5 levels) is something I almost never see populated correctly. People either stuff everything into item_category as a slash-separated string like “Footwear/Running/Men’s” or they leave category2 through category5 empty. The slash-separated approach breaks all the built-in GA4 reports that expect hierarchical categories.

Currency is another constant problem. The currency parameter needs to be set at the event level, not the item level. And it needs to be a valid ISO 4217 code. I’ve seen “USD,” “usd,” “US Dollar,” ”$,” and my favorite, “dollar.” Only the first one works.

dataLayer.push({
  event: "purchase",
  ecommerce: {
    transaction_id: "T_12345",
    value: 129.99,
    currency: "USD",  // Event level, ISO 4217
    tax: 10.40,
    shipping: 5.99,
    coupon: "SUMMER20",
    items: [{ ... }]
  }
});

Transaction ID deduplication

This one costs people real money in terms of bad data.

The purchase event fires on your order confirmation page. User completes an order, lands on the page, event fires. Great. Now the user bookmarks the page. Or refreshes it. Or clicks “back” in their browser and then forward again. Every time that page loads, the purchase event fires again.

GA4 does have built-in deduplication based on transaction_id. If you send two purchase events with the same transaction_id within 72 hours, GA4 should deduplicate them. But I’ve seen this fail in practice, especially when there’s a delay between events or when the events arrive through different data streams.

Don’t rely on it. Handle deduplication on your end.

The simplest approach: set a flag in sessionStorage after the purchase event fires.

if (!sessionStorage.getItem('purchase_fired_' + transactionId)) {
  dataLayer.push({
    event: "purchase",
    ecommerce: { ... }
  });
  sessionStorage.setItem('purchase_fired_' + transactionId, 'true');
}

For server-side implementations, track fired transaction IDs in your database and check before sending the Measurement Protocol hit.

Platform-specific gotchas

Shopify

Shopify’s built-in GA4 integration has improved, but it still has problems. The biggest one: Shopify’s checkout is on checkout.shopify.com (unless you’re on Shopify Plus with a custom checkout domain). That’s a cross-domain tracking scenario, and many stores don’t configure it.

The order status page (where purchase fires) also has restrictions on custom scripts. Shopify gives you limited access through the “Additional scripts” section, but it behaves differently than a normal page. The dataLayer might not be available in the way you expect.

My recommendation for Shopify: use a dedicated app like Elevar or Analyzify for the heavy lifting. Trying to do it through GTM alone on Shopify is a constant battle against Shopify’s architecture.

Ecommerce tracking broken?I audit GA4 ecommerce setups and fix the gaps most teams miss.

Book a Free Audit →

WooCommerce

WooCommerce is more flexible but more fragile. The GTM4WP plugin by Thomas Geiger is the go-to solution, and it’s genuinely good. But the default configuration doesn’t populate all item parameters.

Common WooCommerce issues I run into:

  • Variable products send the parent product ID instead of the variation ID in item_id
  • Price including/excluding tax mismatch between item price and the value parameter
  • Cached pages serving stale dataLayer content (especially with WP Rocket or similar caching plugins)
  • Mini-cart interactions not firing add_to_cart because the plugin only hooks into the full cart page

Test with caching enabled. I’ve seen implementations that work perfectly in development (no cache) and break completely in production.

Custom builds (React, Next.js, headless)

If you’re building a custom storefront, you have full control but no safety net. The most common mistake I see: treating the dataLayer as optional and firing GA4 events directly through gtag().

Use the dataLayer. Always. It gives you a clean separation between your application code and your tracking, and it makes GTM work properly if you ever need it.

For SPAs (single-page applications), you need to handle the view_item event on route changes, not just initial page loads. React Router or Next.js navigation won’t trigger a new page load, so your view_item event won’t fire unless you explicitly call it on route change.

// React example with useEffect
useEffect(() => {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({ ecommerce: null }); // Clear previous ecommerce data
  window.dataLayer.push({
    event: "view_item",
    ecommerce: {
      currency: "USD",
      value: product.price,
      items: [mapProductToItem(product)]
    }
  });
}, [product.id]); // Fire when product changes

Notice the ecommerce: null push before the actual event. This clears the previous ecommerce object from the dataLayer. Without it, item data from a previous event can bleed into the current one. This is a GA4-specific quirk that catches everyone the first time.

Testing ecommerce events properly

The GA4 DebugView is your primary testing tool, and most people underuse it. If you want a deeper dive into the full debugging toolkit, I cover the complete process in my guide on how to debug tracking like a pro.

Step 1: Enable debug mode. Add the debug_mode parameter to your config tag or install the GA Debugger Chrome extension. Without this, events won’t show in DebugView.

Step 2: Walk through the entire funnel. Don’t just test the purchase event. Start from a product listing page and go through every step. Check that:

  • Each event appears in DebugView in the correct order
  • The items array has all expected parameters at every step
  • The currency is consistent across all events
  • The value on purchase matches what you’d expect
  • The item_list_name carries through from view_item_list to select_item

Step 3: Check parameter limits. GA4 has a 100-character limit on event parameter values and a 500-character limit on item parameter values. Long product names or deeply nested categories can get truncated silently.

Step 4: BigQuery validation. If you have BigQuery export enabled (and you should), wait 24 hours after your test transactions and then run queries against the actual data. The GA4 interface has sampling and processing delays. BigQuery is the source of truth.

SELECT
  event_name,
  event_date,
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') as transaction_id,
  (SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value') as value,
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'currency') as currency
FROM `project.analytics_XXXXXXX.events_*`
WHERE event_name = 'purchase'
  AND _TABLE_SUFFIX BETWEEN '20260501' AND '20260519'
ORDER BY event_timestamp DESC

This will show you exactly what GA4 received. Compare the transaction_id and value against your order management system. If they don’t match, you have a problem.

The refund event nobody implements

I’ve saved this for last because it’s the most neglected part of ecommerce tracking. Almost nobody implements refund events, and it means their GA4 revenue data drifts further from reality every month.

The refund event is simple:

dataLayer.push({
  event: "refund",
  ecommerce: {
    transaction_id: "T_12345",
    value: 129.99,
    currency: "USD",
    items: [{ ... }] // Required for partial refunds
  }
});

For full refunds, you only need the transaction_id. For partial refunds, include the specific items and quantities being refunded.

The challenge is that refunds usually happen in your backend (admin panel, customer service tool, payment provider). The customer isn’t on your website when the refund happens. So you can’t use the dataLayer.

This is where the Measurement Protocol comes in. You fire the refund event server-side when your backend processes the refund. You need your GA4 Measurement Protocol API secret and the client_id from the original transaction (which you should be storing in your order database).

If you’re not tracking refunds, your GA4 revenue will always be higher than your actual revenue. For stores with a 10-15% return rate, that’s a significant discrepancy that compounds over time.

Frequently asked questions

Q: What ecommerce events does GA4 require?

GA4 supports a full chain of ten events: view_item_list, select_item, view_item, add_to_cart, view_cart, begin_checkout, add_shipping_info, add_payment_info, purchase, and refund. At minimum, implement view_item, add_to_cart, begin_checkout, and purchase. The more events you track, the better your funnel analysis and the richer the data for optimization.

Q: Why is my GA4 ecommerce revenue wrong or doubled?

The most common causes are missing transaction ID deduplication (purchase events fire multiple times when users refresh the confirmation page) and incorrect currency codes. GA4 requires ISO 4217 currency codes like “USD” at the event level. Handle deduplication client-side using sessionStorage flags or server-side by tracking fired transaction IDs.

Q: What item parameters should I include in the GA4 items array?

Beyond the minimum item_id and item_name, include item_brand, up to five item_category levels, item_variant, price, quantity, discount, coupon, item_list_id, item_list_name, and index. The category hierarchy should use separate fields (item_category through item_category5), not a slash-separated string, to work correctly with built-in GA4 reports.

Q: How do I track refunds in GA4?

Send a refund event with the transaction_id for full refunds, or include specific items and quantities for partial refunds. Since refunds typically happen in your backend, use the GA4 Measurement Protocol to fire the event server-side. Store the original client_id with each order so you can reference it when sending the refund.

The bottom line

GA4 ecommerce tracking isn’t hard in concept. It’s hard in execution because there are dozens of small details that Google’s docs gloss over, and each one can corrupt your data in subtle ways.

Start with the full event chain. Populate every item parameter you have data for. Deduplicate your transactions. Test with DebugView and validate in BigQuery. And implement refund tracking from day one, not “later when we have time.” Later never comes, and meanwhile your revenue data is wrong.

If you’re already running ecommerce tracking and you’re not sure it’s right, pull up your BigQuery export and run the query above. Check if your purchase count matches your order count. Check if your revenue matches your payment processor. If the numbers don’t line up, something in the chain is broken.

AR

Artem Reiter

Web Analytics Consultant

Related Articles

Need help with your analytics?

Free 30-minute discovery call. I'll look at your setup, tell you what's broken, and whether I can help. No commitment.

Or email directly: artem@reiterweb.com