Skip to content

Ecommerce Tracking

GA4 ecommerce tracking is not difficult — but it is unforgiving. Get the data structure right and the Shopping Behavior report fills itself. Get it wrong and you get silently missing data that you won’t notice until your stakeholders ask why the purchase funnel looks broken.

This guide covers every event in the GA4 ecommerce lifecycle, the exact dataLayer structure each one requires, and the GTM configuration that ties it all together.

GA4’s ecommerce model maps a user’s journey from discovery to purchase. You don’t need to implement every event — but each one you skip is a gap in your funnel visibility.

EventWhen to fireRequired
view_item_listProduct grid, search resultsRecommended
select_itemUser clicks a productRecommended
view_itemProduct detail pageYes
add_to_cartItem added to cartYes
view_cartCart page viewedRecommended
remove_from_cartItem removed from cartRecommended
begin_checkoutCheckout initiatedYes
add_shipping_infoShipping step completedRecommended
add_payment_infoPayment step completedRecommended
purchaseOrder confirmedYes
refundOrder refundedRecommended

Every ecommerce event contains an items array. This is the most important part of the entire implementation — every item must include all available parameters, because truncating it now means you lose reporting capability later.

{
item_id: 'SKU-1234', // Required. Your internal product ID.
item_name: 'Canvas Sneakers', // Required. Human-readable product name.
affiliation: 'Main Store', // Optional. Store name for marketplace tracking.
coupon: 'SUMMER20', // Optional. Coupon applied to this item.
currency: 'USD', // Optional at item level; required at event level.
discount: 5.00, // Optional. Discount amount applied.
index: 0, // Position in the list (0-indexed).
item_brand: 'Stride', // Brand name.
item_category: 'Footwear', // Primary category.
item_category2: 'Sneakers', // Sub-category.
item_category3: 'Canvas', // Further sub-category.
item_category4: 'Low Top', // Even further.
item_list_id: 'homepage_recs', // The list this item came from.
item_list_name: 'Homepage Recommendations',
item_variant: 'White / Size 10',
price: 79.99, // Unit price. Must be a number, not a string.
quantity: 1 // Must be a number, not a string.
}

Before every single ecommerce push, clear the previous ecommerce data:

// Step 1: Always clear first
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ ecommerce: null });
// Step 2: Then push your event
window.dataLayer.push({
event: 'view_item',
ecommerce: {
currency: 'USD',
value: 79.99,
items: [{ /* ... */ }]
}
});

This is non-negotiable. GTM’s dataLayer uses a recursive merge model — without clearing, data from a previous add_to_cart event can contaminate your purchase event. If you’ve ever seen a purchase event with the wrong items, this is why.

Fire this when users see a grid of products — category pages, search results, recommendation carousels.

window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'view_item_list',
ecommerce: {
item_list_id: 'category_womens_tops',
item_list_name: "Women's Tops",
items: [
{
item_id: 'SKU-1001',
item_name: 'Linen Button Shirt',
item_brand: 'Strand',
item_category: 'Clothing',
item_category2: "Women's",
item_category3: 'Tops',
item_list_id: 'category_womens_tops',
item_list_name: "Women's Tops",
index: 0,
price: 89.00,
quantity: 1
},
{
item_id: 'SKU-1002',
item_name: 'Silk Blouse',
item_brand: 'Strand',
item_category: 'Clothing',
item_category2: "Women's",
item_category3: 'Tops',
item_list_id: 'category_womens_tops',
item_list_name: "Women's Tops",
index: 1,
price: 129.00,
quantity: 1
}
]
}
});

Fire on the product detail page. This is the most important single event for funnel analysis.

window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'view_item',
ecommerce: {
currency: 'USD',
value: 89.00,
items: [
{
item_id: 'SKU-1001',
item_name: 'Linen Button Shirt',
item_brand: 'Strand',
item_category: 'Clothing',
item_category2: "Women's",
item_category3: 'Tops',
item_list_id: 'category_womens_tops',
item_list_name: "Women's Tops",
item_variant: 'White / Size M',
price: 89.00,
quantity: 1
}
]
}
});

Fire immediately when the user adds an item — not on page load, not on cart page view.

window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'add_to_cart',
ecommerce: {
currency: 'USD',
value: 89.00, // price × quantity
items: [
{
item_id: 'SKU-1001',
item_name: 'Linen Button Shirt',
item_brand: 'Strand',
item_category: 'Clothing',
item_variant: 'White / Size M',
price: 89.00,
quantity: 1
}
]
}
});

The most critical event. Include every item in the order, and match the transaction_id exactly to your order management system.

window.dataLayer.push({ ecommerce: null });
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'ORDER-7829', // Your order ID. Must be unique.
currency: 'USD',
value: 164.00, // Revenue after discounts, before tax/shipping.
tax: 13.12,
shipping: 8.99,
coupon: 'SUMMER20',
items: [
{
item_id: 'SKU-1001',
item_name: 'Linen Button Shirt',
item_brand: 'Strand',
item_category: 'Clothing',
item_variant: 'White / Size M',
price: 89.00,
quantity: 1,
coupon: 'SUMMER20',
discount: 14.00
},
{
item_id: 'SKU-0892',
item_name: 'Canvas Tote Bag',
item_brand: 'Strand',
item_category: 'Accessories',
price: 75.00,
quantity: 1
}
]
}
});

If your team uses TypeScript, type the dataLayer contract and enforce it at compile time:

interface GA4EcommerceItem {
item_id: string;
item_name: string;
affiliation?: string;
coupon?: string;
currency?: string;
discount?: number;
index?: number;
item_brand?: string;
item_category?: string;
item_category2?: string;
item_category3?: string;
item_category4?: string;
item_list_id?: string;
item_list_name?: string;
item_variant?: string;
price?: number;
quantity?: number;
}
interface GA4EcommerceEvent {
event: string;
ecommerce: {
currency?: string;
value?: number;
transaction_id?: string;
tax?: number;
shipping?: number;
coupon?: string;
item_list_id?: string;
item_list_name?: string;
items: GA4EcommerceItem[];
};
}
declare global {
interface Window {
dataLayer: (GA4EcommerceEvent | { ecommerce: null } | Record<string, unknown>)[];
}
}

You need one GA4 Event tag per ecommerce event type, each with the “Send Ecommerce Data” checkbox enabled:

Tag Configuration

GA4 - Ecommerce - purchase

Type
Google Analytics: GA4 Event
Trigger
CE - purchase
Variables
Google Tag - Measurement ID

In the tag configuration:

  1. Set Event Name to purchase (or use {{Event}} to read the event name from the dataLayer)
  2. Under More Settings → Ecommerce, check Send Ecommerce Data
  3. Set Data Source to Data Layer

With this configuration, GTM reads the ecommerce object from the dataLayer automatically and sends it to GA4.

Reusable approach: one tag for all ecommerce events

Section titled “Reusable approach: one tag for all ecommerce events”

Instead of creating a separate tag for each event, use {{Event}} as the event name and set conditions on your trigger:

  1. Create a Custom Event trigger that matches a regex covering all ecommerce events: ^(view_item_list|select_item|view_item|add_to_cart|view_cart|remove_from_cart|begin_checkout|add_shipping_info|add_payment_info|purchase|refund)$

  2. Create one GA4 Event tag with Event Name = {{Event}} and Send Ecommerce Data enabled.

  3. Attach the single trigger. All ecommerce events now fire through one tag.

This keeps your container clean. One tag, one trigger, all ecommerce events handled.

Every practitioner learns this the hard way. Push { ecommerce: null } before every ecommerce push. No exceptions.

// ❌ This will silently fail or produce wrong data in GA4
items: [{ item_id: 'SKU-1', price: '89.00', quantity: '1' }]
// ✅ Numbers only
items: [{ item_id: 'SKU-1', price: 89.00, quantity: 1 }]

GA4 cannot merge ecommerce data across events without item_id. Without it, you lose the ability to trace a product from impression to purchase. item_id and item_name are the two non-negotiable fields.

value at the event level should equal the sum of (price - discount) × quantity for all items in that specific interaction — not the cart total, not the catalog price.

Never send a truncated items array in production. Some teams only send the first item for performance reasons. You lose all multi-item analytics.

Forgetting the Send Ecommerce Data checkbox

Section titled “Forgetting the Send Ecommerce Data checkbox”

The most common GTM configuration mistake. Your dataLayer push can be perfect, but if the GA4 Event tag doesn’t have “Send Ecommerce Data” checked, none of the ecommerce data reaches GA4.

Combining Send Ecommerce Data with manual parameter mapping

Section titled “Combining Send Ecommerce Data with manual parameter mapping”

If you check “Send Ecommerce Data” AND also manually map items as event parameters in the same GA4 Event tag, the manual parameters override the ecommerce struct. This causes confusing behavior — some ecommerce data appears, some doesn’t.

Pick one approach:

  • Send Ecommerce Data (recommended for standard ecommerce) — let GTM read the ecommerce object from the dataLayer automatically
  • Manual parameter mapping — explicitly map each parameter in the tag configuration

Never combine both in the same tag. If you need custom parameters alongside ecommerce data, add them as separate event parameters while still using “Send Ecommerce Data” for the ecommerce object.

  1. GA4 DebugView — Log in to GA4, go to Configure → DebugView. Trigger events on your site with GTM Preview mode active. You should see each event with its ecommerce parameters within seconds.

  2. GTM Preview → Data Layer tab — After each user action, click on the event in the Tag Assistant panel and check the Data Layer tab. Confirm the ecommerce object contains the expected structure.

  3. Network tab — Filter for collect requests to www.google-analytics.com or analytics.google.com. Inspect the ep.items and pr parameters in the request payload.