All posts
5 min readga4, ecommerce, datalayer

The GA4 items array: event-scoped vs item-scoped data, explained

Ecommerce reporting lives or dies on the items array. Get its structure and scoping wrong and your product reports are empty even though "tracking works." Here is the anatomy.


The items array is where GA4 keeps product-level detail, and it behaves differently from ordinary event parameters in ways that quietly break reports. Item-scoped fields describe one product within an event; event-scoped fields describe the event as a whole. Mix them up — put the revenue inside an item, or the category outside the array — and your ecommerce reports populate partially, with no error to tell you why.

The two scopes, and why the distinction bites

An ecommerce event carries both kinds of data at once:

window.dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: 'T-10432',   // event-scoped
    value: 178.00,               // event-scoped
    currency: 'USD',             // event-scoped
    tax: 14.00,                  // event-scoped
    shipping: 5.00,              // event-scoped
    items: [
      {
        item_id: 'SKU-1189',     // item-scoped
        item_name: 'Merino Crew',
        item_brand: 'Northlight',
        item_category: 'Knitwear',
        item_variant: 'Navy / M',
        price: 49.00,            // per unit, item-scoped
        quantity: 2
      },
      {
        item_id: 'SKU-2240',
        item_name: 'Trail Runner',
        item_category: 'Footwear',
        price: 79.00,
        quantity: 1
      }
    ]
  }
});

Event-scoped fields answer "what was this transaction worth?". Item-scoped fields answer "what was in it?".

The rule that prevents most mistakes: value belongs to the event, price belongs to the item. GA4 does not compute one from the other, and it does not warn you when they disagree.

The fields that matter

FieldScopeNotes
transaction_idEventMust be unique and stable; refunds key on it
valueEventTotal revenue for the transaction
currencyEventISO code; required for revenue to report
item_idItemYour SKU. At least one of id or name is required
item_nameItemHuman-readable
priceItemPer unit, not line total
quantityItemUnits of that item
item_categoryItemUp to five levels, category through category5
item_variantItemSize, colour, configuration
indexItemPosition in a list, for list reporting

The five mistakes that break ecommerce reports

1. price as a line total rather than a per-unit price. Two units at £49 means price: 49, quantity: 2 — not price: 98. Getting this wrong inflates average order value and product-level revenue while the transaction total stays correct, which makes it hard to spot.

2. Missing currency. Without it, GA4 cannot report revenue at all. The event arrives, the value is present, and the Monetisation reports stay empty.

3. Values sent as strings. "£49.00" instead of 49.00. Currency symbols, thousands separators, and quoted numbers are all rejected or coerced unpredictably.

4. The ecommerce object not being cleared between pushes. GTM's dataLayer persists, so a previous event's items can leak into the next one. Push a null first:

    window.dataLayer.push({ ecommerce: null });
    window.dataLayer.push({ event: 'purchase', ecommerce: { /* ... */ } });

This is one of the most common causes of a purchase event carrying the items from an earlier add-to-cart.

5. Inconsistent item_id between events. The same product must carry the same item_id in view_item, add_to_cart, and purchase, or GA4 cannot connect the journey and your product funnel breaks.

Item-scoped custom dimensions

You can add your own item-level fields — a supplier, a margin band, a fulfilment type — and register them as item-scoped custom dimensions. The quota is small (ten on a standard property), so spend it deliberately.

The same cardinality rules apply as everywhere else: bounded, normalised values only. Custom dimensions covers the trap.

Lists, positions, and where merchandising data comes from

The array is also how GA4 learns about product lists, and this is the part most implementations skip entirely.

view_item_list with item_list_name and index tells you which list a product appeared in and at what position. select_item records the click. Together they answer merchandising questions nothing else can: does position three on the category page get clicked, does the recommended-products carousel earn its space, does the search results ordering work?

window.dataLayer.push({
  event: 'view_item_list',
  ecommerce: {
    item_list_id: 'category_knitwear',
    item_list_name: 'Knitwear',
    items: [
      { item_id: 'SKU-1189', item_name: 'Merino Crew', index: 1, price: 49.00 },
      { item_id: 'SKU-2240', item_name: 'Trail Runner', index: 2, price: 79.00 }
    ]
  }
});

Two practical notes. Send the items actually visible rather than the whole result set — a 200-product category page does not need 200 items in one event, and large payloads get truncated. And keep item_list_name values bounded and normalised, since they become a dimension subject to the same cardinality limits as everything else.

Which events need the array

Every ecommerce event carries it: view_item_list, select_item, view_item, add_to_cart, remove_from_cart, view_cart, begin_checkout, add_payment_info, add_shipping_info, purchase, and refund.

Two get overlooked and are worth the effort: view_item_list with index populated tells you which list positions get clicked, and refund with items is the only way partial refunds correct product-level revenue. Refunds and returns covers that.

Verifying it

  1. Check DebugView for a real transaction and confirm the items array is present and populated, not empty.
  2. Confirm price times quantity sums sensibly against value, accounting for tax and shipping treatment.
  3. Check the Monetisation reports after data accrues — item-level revenue populating means the array is being read.
  4. Trace one product through view, add, and purchase, confirming the item_id is identical at each stage.
  5. Reconcile a day of revenue against your order system.

FAQ

Should price in the items array be the unit price or the line total?

Unit price. Quantity carries the multiplier. Sending a line total inflates average order value and product-level revenue while leaving the transaction total correct.

Why is my GA4 ecommerce revenue empty despite sending purchase events?

Most often a missing currency parameter, or values sent as strings with currency symbols. Both cause revenue to fail while the event itself records fine.

Do I need to clear the ecommerce object between events?

Yes, when using GTM. Push ecommerce: null before the next ecommerce event, or items from a previous push can leak into it.

What is the difference between event-scoped and item-scoped data?

Event-scoped describes the whole transaction — total value, transaction ID, currency. Item-scoped describes one product within it — SKU, unit price, category.

How many item-scoped custom dimensions can I register?

Ten on a standard property, which is few enough that each one should be a deliberate decision with a documented purpose.

Before debugging the items array, confirm the tag is firing at all: the free tracking audit checks GA4 presence and duplicates on any URL.


See where your tracking stands

Run the same 13-check audit referenced in this post against any URL. No signup, results in seconds.

Run a free audit