Skip to content
ceaksan
gtm

GA4 Ecommerce Events: Complete dataLayer Implementation Guide

Learn how to implement all 13 GA4 ecommerce events via dataLayer, clear the ecommerce object properly, and configure GTM for accurate sales tracking.

Apr 2, 2019 7 min read Updated: Apr 7, 2026
TL;DR

GA4 ecommerce tracking uses 13 standard events, all fired via dataLayer.push. Every push must be preceded by ecommerce: null to clear stale data, otherwise GTM's recursive merge bleeds items from previous events into new ones. The items array is shared across all events; item_id or item_name is required per item. Without transaction_id on the purchase event, GA4 cannot deduplicate transactions and page refreshes inflate revenue. From client projects I have seen, the range is 2-4x depending on refresh behavior. On the GTM side, use Data Layer Variable Version 1 for the items array to avoid Version 2's merge contamination.

GA4 ecommerce tracking consists of 13 standard events. Each is fired via dataLayer.push with a shared items array. Pushing ecommerce: null before every event is mandatory. Without transaction_id on the purchase event, duplicate revenue is inevitable.

Quick ReferenceValue
Total events13
Items array limit200 items/event
Required item fielditem_id or item_name
Category depth5 levels (item_category - item_category5)
Custom item parametersMax 27
info

Universal Analytics Note

Universal Analytics (UA) stopped processing data on July 1, 2023. UA 360 properties shut down on October 1, 2023. As of July 1, 2024, all UA properties were permanently deleted, including historical data and API access. The UA Enhanced Ecommerce products array, actionField structure, and numbered checkout steps are no longer valid. This guide covers GA4 ecommerce events only.

dataLayer Prerequisites

Two prerequisites exist for dataLayer ecommerce events.

1. dataLayer initialization: The dataLayer must be defined as an array before the GTM snippet.

<script>
  window.dataLayer = window.dataLayer || [];
</script>
<!-- Google Tag Manager -->

When the GTM snippet loads, it looks for the dataLayer array. If this line is missing and the first push call comes before the snippet, a JavaScript error occurs. If dataLayer has been assigned a non-array value by another script, the || [] fallback won’t work. This is a common issue.

2. Clearing the ecommerce object: Previous ecommerce data must be reset before every dataLayer.push.

// Clear previous data
dataLayer.push({ ecommerce: null });

// Send the new event
dataLayer.push({
  event: "view_item",
  ecommerce: {
    currency: "USD",
    value: 29.99,
    items: [{ item_id: "SKU_001", item_name: "Product Name" }],
  },
});

GTM’s Data Layer Variable mechanism uses recursive merge. Without ecommerce: null, the items array from a previous push persists in memory and bleeds into the next event. The result: an add_to_cart event shows products that were never actually added to the cart. This is especially common on SPA (Single Page Application) sites where the page never reloads and the dataLayer is never reset, so each event pushes new data on top of the previous ecommerce object.

tip

You can request Google Tag Manager support here.

items Array Structure

All GA4 ecommerce events use the same items array structure1. The separate products and impressions array names from UA no longer exist.

FieldTypeStatusDescription
item_idstringOne requiredUnique product ID (SKU)
item_namestringOne requiredProduct name
pricenumberRecommendedUnit price (unquoted numeric, not string)
quantityintegerRecommendedQuantity (integer, not float)
item_brandstringOptionalBrand
item_categorystringOptionalPrimary category
item_category2stringOptionalSubcategory 2
item_category3stringOptionalSubcategory 3
item_category4stringOptionalSubcategory 4
item_category5stringOptionalSubcategory 5
item_variantstringOptionalVariant (color, size)
item_list_idstringOptionalList ID
item_list_namestringOptionalList name
indexnumberOptionalPosition in list
discountnumberOptionalDiscount amount
couponstringOptionalItem-level coupon
affiliationstringOptionalStore/partner name
location_idstringOptionalGoogle Place ID
promotion_idstringOptionalPromotion ID
promotion_namestringOptionalPromotion name
creative_namestringOptionalBanner/creative name
creative_slotstringOptionalDisplay position

A single event supports up to 200 items in the array. At least one of item_id or item_name must be provided; using both is recommended. When item_name is missing, reports show (not set).

In UA, category hierarchy was pipe-separated in a single field (Apparel/Men/T-Shirts). In GA4, each level is a separate field: item_category for the primary category, item_category2 through item_category5 for subcategories.

GA4 Ecommerce Events

The GA4 ecommerce funnel consists of 13 events2. Below is the structure and dataLayer code for each event in funnel order.

warning

These 13 events are GA4’s predefined (recommended) ecommerce events. Do not create them manually in GA4 Admin > Events. Events sent via dataLayer are automatically recognized by GA4. Creating events with the same name in the GA4 UI causes duplicate data and corrupts ecommerce reports.

Discovery Stage

view_item_list

Fires when a user views a product list (category page, search results, recommended products).

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "view_item_list",
  ecommerce: {
    item_list_id: "category_mens_shirts",
    item_list_name: "Men's Shirts",
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        item_brand: "Brand A",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "Blue / M",
        index: 0,
        quantity: 1,
      },
      {
        item_id: "SKU_002",
        item_name: "Linen Shirt",
        price: 39.9,
        item_brand: "Brand B",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "White / L",
        index: 1,
        quantity: 1,
      },
    ],
  },
});

select_item

Fires when a user clicks a product from a list. Associating the clicked product with its source list via item_list_name is critical for funnel analysis.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "select_item",
  ecommerce: {
    item_list_id: "category_mens_shirts",
    item_list_name: "Men's Shirts",
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        item_brand: "Brand A",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "Blue / M",
        index: 0,
        quantity: 1,
      },
    ],
  },
});

view_item

Fires when a user views a product detail page. currency and value are required here.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "view_item",
  ecommerce: {
    currency: "USD",
    value: 49.9,
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        item_brand: "Brand A",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "Blue / M",
        quantity: 1,
      },
    ],
  },
});

Promotion Events

view_promotion

Fires when a promotion banner or campaign area enters the user’s viewport.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "view_promotion",
  ecommerce: {
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        promotion_id: "summer_sale_2026",
        promotion_name: "Summer Sale 30% Off",
        creative_name: "hero_banner_summer",
        creative_slot: "homepage_slot_1",
        price: 49.9,
        discount: 14.97,
        quantity: 1,
      },
    ],
  },
});

select_promotion

Fires when a user clicks a promotion banner.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "select_promotion",
  ecommerce: {
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        promotion_id: "summer_sale_2026",
        promotion_name: "Summer Sale 30% Off",
        creative_name: "hero_banner_summer",
        creative_slot: "homepage_slot_1",
        price: 49.9,
        discount: 14.97,
        quantity: 1,
      },
    ],
  },
});

Cart Stage

add_to_cart

Fires when a product is added to the cart. currency and value are required.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "add_to_cart",
  ecommerce: {
    currency: "USD",
    value: 49.9,
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        item_brand: "Brand A",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "Blue / M",
        quantity: 1,
      },
    ],
  },
});

remove_from_cart

Fires when a product is removed from the cart.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "remove_from_cart",
  ecommerce: {
    currency: "USD",
    value: 49.9,
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        item_brand: "Brand A",
        item_category: "Apparel",
        item_variant: "Blue / M",
        quantity: 1,
      },
    ],
  },
});

view_cart

Fires when a user views the cart page. This event had no equivalent in UA; it was added in GA4.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "view_cart",
  ecommerce: {
    currency: "USD",
    value: 89.8,
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        quantity: 1,
      },
      {
        item_id: "SKU_002",
        item_name: "Linen Shirt",
        price: 39.9,
        quantity: 1,
      },
    ],
  },
});

Checkout Stage

In UA, the checkout process was tracked with numbered steps (checkout_step: 1, 2, 3). In GA4, each step is a separate event.

begin_checkout

Fires when a user initiates the checkout process.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "begin_checkout",
  ecommerce: {
    currency: "USD",
    value: 89.8,
    coupon: "SUMMER2026",
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        quantity: 1,
      },
      {
        item_id: "SKU_002",
        item_name: "Linen Shirt",
        price: 39.9,
        quantity: 1,
      },
    ],
  },
});

add_shipping_info

Fires when a user selects a shipping option. shipping_tier is required.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "add_shipping_info",
  ecommerce: {
    currency: "USD",
    value: 89.8,
    coupon: "SUMMER2026",
    shipping_tier: "Standard Shipping",
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        quantity: 1,
      },
      {
        item_id: "SKU_002",
        item_name: "Linen Shirt",
        price: 39.9,
        quantity: 1,
      },
    ],
  },
});

add_payment_info

Fires when a user selects a payment method.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "add_payment_info",
  ecommerce: {
    currency: "USD",
    value: 89.8,
    coupon: "SUMMER2026",
    payment_type: "Credit Card",
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        quantity: 1,
      },
      {
        item_id: "SKU_002",
        item_name: "Linen Shirt",
        price: 39.9,
        quantity: 1,
      },
    ],
  },
});

Post-Purchase

purchase

Fires when an order is completed. transaction_id, currency, and value are required. Without transaction_id, page refreshes cause duplicate revenue.

dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "purchase",
  ecommerce: {
    transaction_id: "ORD-20260407-001",
    value: 89.8,
    tax: 7.18,
    shipping: 5.99,
    currency: "USD",
    coupon: "SUMMER2026",
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        item_brand: "Brand A",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "Blue / M",
        quantity: 1,
      },
      {
        item_id: "SKU_002",
        item_name: "Linen Shirt",
        price: 39.9,
        item_brand: "Brand B",
        item_category: "Apparel",
        item_category2: "Men",
        item_category3: "Shirts",
        item_variant: "White / L",
        quantity: 1,
      },
    ],
  },
});
warning

Be consistent about whether value includes tax and shipping or represents the net amount. GA4 Monetization reports calculate total revenue as value + tax + shipping. If tax is already included in value and you also send the tax parameter separately, revenue gets double-counted.

refund

Fires when a full or partial refund is issued. For full refunds, transaction_id is sufficient. For partial refunds, specify the returned items in the items array.

// Full refund
dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "refund",
  ecommerce: {
    currency: "USD",
    transaction_id: "ORD-20260407-001",
  },
});

// Partial refund
dataLayer.push({ ecommerce: null });
dataLayer.push({
  event: "refund",
  ecommerce: {
    currency: "USD",
    transaction_id: "ORD-20260407-001",
    value: 49.9,
    items: [
      {
        item_id: "SKU_001",
        item_name: "Oxford Shirt",
        price: 49.9,
        quantity: 1,
      },
    ],
  },
});

Event Parameters Reference Table

EventRequired ParametersOptional Parameters
view_item_list-item_list_id, item_list_name
select_item-item_list_id, item_list_name
view_itemcurrency, value-
view_promotion-promotion_id, promotion_name, creative_name, creative_slot
select_promotion-promotion_id, promotion_name, creative_name, creative_slot
add_to_cartcurrency, value-
remove_from_cartcurrency, value-
view_cartcurrency, value-
begin_checkoutcurrency, valuecoupon
add_shipping_infocurrency, value, shipping_tiercoupon
add_payment_infocurrency, valuepayment_type, coupon
purchasecurrency, value, transaction_idtax, shipping, coupon, affiliation
refundcurrency, transaction_idvalue, tax, shipping, coupon

All events require the items array. At least one of item_id or item_name must be provided per item.

Event Size Limits

GA4 events have size and parameter limits. Data exceeding these limits is silently truncated or dropped; no HTTP error is returned3.

LimitStandardGA4 360
Event name length40 characters40 characters
Parameters per event25100
Parameter name length40 characters40 characters
Parameter value length (string)100 characters500 characters
Items array limit200 items200 items
Event-scoped custom dimensions50125
Item-scoped custom dimensions1025
User-scoped custom dimensions25100
Measurement Protocol payload130 KB130 KB

What happens when limits are exceeded:

  • Invalid/too-long event name: Event is dropped entirely, a firebase_error event is logged
  • Invalid/too-long parameter name: Parameter is replaced with firebase_error
  • Parameter value too long: Value is silently truncated, no error raised
  • More than 25 parameters: Extra parameters are dropped
  • More than 200 items: Array is truncated to 200
  • More than 27 custom item parameters: Extras are dropped

Pay attention to the 100-character limit on string fields like item_name and item_variant. Long product names are silently truncated and appear incomplete in reports. Measurement Protocol requests exceeding the 130 KB payload limit are rejected4.

Currency and Reporting

GA4 has two distinct currency concepts: the currency parameter sent with events and the property-level reporting currency.

Reporting currency is set in GA4 Admin > Property Settings > Currency. When the sent currency differs from the reporting currency, GA4 performs automatic conversion. This conversion uses Google Finance rates and is updated once daily, not in real time.

Key considerations:

  • Sending the same currency code across all events is important for consistency. Sending TRY in one event and USD in another produces misleading results in funnel reports.
  • When the sent currency does not match the GA4 property reporting currency, small discrepancies from exchange rate conversion may occur.
  • value sent without a currency parameter will not appear in Monetization reports.
  • For multi-currency ecommerce sites, sending the correct currency code with each event and setting the GA4 property reporting currency to the primary currency is the safest approach.

Common Mistakes

1. Not clearing the ecommerce object. See dataLayer Prerequisites above for details. Skipping ecommerce: null causes GTM’s recursive merge to bleed items from the previous event into the next one1.

2. Using strings for price and value. price: "$49.90" or value: "89.80 USD" silently fails. GA4 expects these fields as numeric types1. Correct: price: 49.90 (unquoted) and currency: "USD" (separate field).

3. Omitting the currency parameter. See Currency and Reporting above for details. value sent without currency will not appear in Monetization reports1.

4. Missing transaction_id. Without transaction_id, GA4 cannot perform duplicate transaction detection2. When a user refreshes the thank-you page, the purchase event fires again and revenue data multiplies; the range I have observed is 2-4x depending on refresh behavior. Every order must have a unique transaction_id.

5. Using float values for quantity. quantity: 1.5 breaks revenue calculations in GA4 Monetization reports. quantity must always be an integer1.

6. Timing mismatch. If a GTM tag fires on a DOM Ready trigger but ecommerce data is pushed later, the tag captures empty data. Data must always be in the dataLayer before the trigger fires.

7. Using Data Layer Variable Version 2 in GTM. Version 2 applies recursive merge, causing items from a previous push to merge with the next one. Use Version 1 for ecommerce items. The Knowit Experience DLV v1 template from the GTM Template Gallery handles this automatically.

8. Not initializing dataLayer before the GTM snippet. The window.dataLayer = window.dataLayer || [] line must appear in <head> before the GTM container code5. Otherwise, pushes made before GTM loads are lost.

9. Creating events in the GA4 UI. These 13 events are GA4’s recommended events and are automatically recognized when sent via dataLayer2. Creating events with the same name manually in GA4 Admin > Events causes duplicate data.

GTM Configuration

Three components are needed to send GA4 ecommerce events through GTM: tags, triggers, and variables.

Tag

  • Type: Google Analytics: GA4 Event
  • Event name: Exact GA4 event name (purchase, add_to_cart, etc.)
  • Parameters: Each parameter mapped via Data Layer Variables

In UA Enhanced Ecommerce, GTM had a “Use Data Layer” checkbox that automatically read the ecommerce object. GA4 has no such checkbox. Every parameter (items, currency, value, transaction_id) must be manually mapped in the tag’s parameter table.

Trigger

  • Type: Custom Event
  • Event name: Must exactly match the event key in the dataLayer push (e.g., purchase)

Variables

A separate Data Layer Variable must be created for each ecommerce parameter:

Variable NameData Layer Variable Path
DLV - ecommerce.itemsecommerce.items
DLV - ecommerce.currencyecommerce.currency
DLV - ecommerce.valueecommerce.value
DLV - ecommerce.transaction_idecommerce.transaction_id
DLV - ecommerce.taxecommerce.tax
DLV - ecommerce.shippingecommerce.shipping
DLV - ecommerce.couponecommerce.coupon
DLV - ecommerce.shipping_tierecommerce.shipping_tier
DLV - ecommerce.payment_typeecommerce.payment_type

Use Version 1 for the ecommerce.items variable. Version 2 is safe for other fields.

Debugging

To verify events fire correctly:

  • GTM Preview mode: Shows which tags fired, which triggers activated, and what variable values were sent
  • GA4 DebugView: Under GA4 Admin > DebugView, monitor events in real time. Activate by adding debug_mode: true to your gtag config or automatically when GTM Preview mode is active
  • dataslayer extension: Monitors dataLayer pushes in real time via Chrome Console6

Data Accuracy Expectations

100% accuracy is impossible with client-side analytics. Ad blockers, browser privacy features, and consent rejection all contribute to data loss. The actual loss rate varies by site, geography, and consent implementation. The gap between backend order data and GA4 ecommerce data serves as the primary benchmark for evaluating implementation accuracy.

Server-side tagging (sGTM) can reduce this loss7. When GA4 events are sent server-side via sGTM, client-side blockers are bypassed. However, this requires separate infrastructure.

GA4 Ecommerce dataLayer Implementation Support

ecommerce: null discipline, transaction_id, items array validation, and GTM tag/trigger/variable wiring. Store-specific implementation delivered with DebugView verification.

Get in Touch

Footnotes

  1. Measure ecommerce. Google Developers 2 3 4 5
  2. GA4 Recommended Events. Google Analytics Help 2 3
  3. GA4 Collection Limits. Google Analytics Help
  4. Measurement Protocol Limitations. Google Developers
  5. Set up data collection for websites. Google Developers
  6. dataslayer. Chrome Web Store
  7. Server-side tagging. Google Tag Manager Help
Key Takeaways
  • 01 Push ecommerce: null before every dataLayer.push, GTM recursive merge causes stale data bleed without it
  • 02 At least one of item_id or item_name is required in the items array
  • 03 Missing transaction_id on purchase inflates revenue on page refreshes; the range I have observed is 2-4x
  • 04 price and value must be unquoted numbers, not strings
  • 05 Use Data Layer Variable Version 1 in GTM for ecommerce items, Version 2 causes merge issues
  • 06 Without the currency parameter, data won't appear in GA4 Monetization reports
  • 07 Do not create these events manually in GA4 UI, it causes duplicate data with dataLayer events
Frequently Asked Questions (FAQ)
+ Does GA4 still have Enhanced Ecommerce?

No. Enhanced Ecommerce was a separate feature in Universal Analytics that needed to be enabled. In GA4, ecommerce tracking is part of the standard event model with no separate toggle. GA4 uses 13 predefined event names from view_item_list to purchase.

+ Why is ecommerce: null required before every push?

GTM's Data Layer Variable mechanism uses recursive merge. The items array from a previous push persists in memory and contaminates the next event. Pushing ecommerce: null resets the previous data and prevents incorrect product data from being reported.

+ What happens without transaction_id?

GA4 uses transaction_id for duplicate transaction detection. Without it, every time a user refreshes the thank-you page, the purchase event fires again and revenue inflates. From client projects I have seen, the range is 2-4x depending on refresh behavior.

+ Why is the currency parameter required?

Without currency, ecommerce data will not appear in GA4 Monetization reports. It must always be sent alongside the value parameter in ISO 4217 format: USD, EUR, TRY.

+ What is the difference between Data Layer Variable Version 1 and Version 2 in GTM?

Version 2 uses recursive merge on dataLayer pushes, causing the items array from a previous push to merge with the next one. Version 1 reads only the most recent push, making it safer for ecommerce items.

+ How many items can be sent in a single GA4 ecommerce event?

A single event supports up to 200 items in the items array. If you have more products, split them across multiple events.

+ What happens when the sent currency differs from the GA4 reporting currency?

GA4 sets the reporting currency in Admin > Property Settings > Currency. If the sent currency differs, GA4 performs automatic conversion using Google Finance rates, updated once daily. For consistency, send the same currency across all events and match it with the GA4 property setting.

+ Do I need to create these events in the GA4 UI as well?

No. These 13 events are GA4's predefined recommended events. When sent via dataLayer, GA4 recognizes them automatically. Creating events with the same name manually in GA4 Admin > Events causes duplicate data and corrupts ecommerce reports.