Skip to content

GA4 & tracking

GA4 ecommerce tracking setup: events, parameters and QA

A step-by-step GA4 ecommerce setup covering the recommended events from view_item to purchase and refund, required parameters, the dataLayer, and QA in DebugView and BigQuery.

DATUMPANELPublished Updated 7 min read

Cover for the GA4 ecommerce tracking article

A good store dashboard depends on correct GA4 ecommerce events. Most audits I run turn up the same problems: purchase fires twice, transaction_id is missing, the price arrives as a string with a currency symbol in it, and items is empty. This article shows how to set things up so the data is actually usable.

The events you need

Google documents the recommended events in its ecommerce guide and in the recommended events list. The minimum working set:

EventWhenPriority
view_item_listproduct list shown (category, search)medium
select_itemproduct clicked in a listmedium
view_itemproduct pagehigh
add_to_cartadded to carthigh
remove_from_cartremoved from cartlow
view_cartcart viewedmedium
begin_checkoutcheckout startedhigh
add_shipping_infoshipping chosenmedium
add_payment_infopayment method chosenmedium
purchaseorder placed/paidcritical
refundrefund issuedrecommended

Required parameters

For purchase:

ParameterExampleGotcha
transaction_id"100245"same ID as your backend; without it you get duplicates
value158.00a number, no currency symbol
currency"USD"ISO code; without it value is ignored
shipping7optional
tax12.64optional
coupon"AUTUMN10"for promo analysis
itemsarray of productsat least item_id or item_name

Each item should carry item_id, item_name, price and quantity, and ideally item_brand, item_category and item_variant too.

Example dataLayer push

dataLayer.push({ ecommerce: null }); // clear the previous object
dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: '100245',
    value: 158.00,
    currency: 'USD',
    shipping: 7,
    coupon: 'AUTUMN10',
    items: [
      { item_id: 'SKU-881', item_name: 'Travel mug 450 ml', item_category: 'Kitchen', price: 75.50, quantity: 2 }
    ]
  }
});

Where to fire purchase

The usual culprit is the thank-you page. When purchase fires there:

  • it fires again on reloads and when the user returns from the confirmation email;
  • it never fires if payment completed on a hosted page and the user closed the tab before the redirect;
  • ad blockers stop it.

My setup:

  1. Fire purchase on the frontend when the order is created, with transaction_id, and guard against repeats (a flag keyed by order ID in localStorage or a cookie).
  2. For paid online orders, send a server-side confirmation through the Measurement Protocol after the payment webhook (server-side tracking). GA4 de-duplicates on transaction_id within a property.
  3. Send refunds from the backend as refund events.

Platforms

  • Shopify: the native Google channel sends ecommerce events. Make sure GTM isn't firing the same events a second time.
  • WooCommerce: plugins vary a lot in quality, so check items and transaction_id.
  • Custom builds: dataLayer + GTM, as shown above.

QA

  1. GTM Preview + GA4 DebugView: walk from category page to payment and check every event and parameter.
  2. Monetisation → Ecommerce purchases report: are item names populated?
  3. BigQuery: query for duplicate transaction_ids and empty items (GA4 export).
  4. Weekly reconciliation with the backend on order count and revenue. A gap of a few percent is normal. Anything bigger needs a root cause (GA4 vs CRM discrepancies).
SELECT event_date,
       COUNTIF(ecommerce.transaction_id IS NULL) AS no_tid,
       COUNTIF(ARRAY_LENGTH(items) = 0) AS no_items,
       COUNT(*) AS purchases
FROM `project.analytics_123456789.events_*`
WHERE _TABLE_SUFFIX >= '20261001' AND event_name = 'purchase'
GROUP BY 1 ORDER BY 1;

Common mistakes

  • value including tax on some orders and excluding it on others.
  • Different item_ids for the same product (SKU in one place, internal ID in another).
  • Not clearing ecommerce before a new push, so parameters leak between events.
  • Missing currency, so GA4 drops the amounts.
  • Test orders landing in the production property.

Checklist

  • Every funnel event from view_item to purchase is in place
  • transaction_id = the backend order ID
  • value is numeric and currency is an ISO code
  • items uses consistent item_ids across events
  • Repeat purchase firing is blocked
  • Paid orders are confirmed server-side
  • Refunds are sent as refund
  • Weekly reconciliation with the backend

Step by step: implementing purchase via the dataLayer

The cleanest setup for most stores is a dataLayer push from the order confirmation page (or better, from the code that confirms payment) and a GA4 event tag in Google Tag Manager that reads it. The structure follows Google's ecommerce documentation:

window.dataLayer = window.dataLayer || [];
dataLayer.push({ ecommerce: null });          // clear the previous ecommerce object
dataLayer.push({
  event: "purchase",
  ecommerce: {
    transaction_id: "100245",                  // same ID your backend stores
    value: 1840.00,                            // decide: with or without shipping/VAT, and document it
    currency: "UAH",
    shipping: 90.00,
    tax: 0,
    coupon: "AUTUMN10",
    items: [
      { item_id: "SKU-118", item_name: "Linen shirt", item_category: "Shirts",
        price: 875.00, quantity: 2, discount: 0 },
      { item_id: "SKU-402", item_name: "Canvas belt", item_category: "Accessories",
        price: 90.00, quantity: 1 }
    ]
  }
});

Rules that save hours later:

  1. Push ecommerce: null first. Without it, items from a previous event can merge into the next one on single-page sites.
  2. Fire purchase once. Guard against reloads of the thank-you page, for example by storing the transaction ID in sessionStorage and skipping the push if it's already there. GA4 also de-duplicates by transaction_id, but don't rely on that alone.
  3. Keep value consistent with your definition. If value includes shipping on the site but your CRM revenue doesn't, the gap will be permanent. Write the choice down.
  4. Use the same item_id as your product catalogue, so product reports join cleanly with costs and stock.

The full funnel in practice

StepEventWhere it firesTypical pitfall
Product list viewview_item_listcategory/search pagesfiring for every scroll, inflating counts
Product viewview_itemproduct pagevariants missing item_variant
Add to cartadd_to_cartbutton click or cart API successfiring on click even when the add fails
Begin checkoutbegin_checkoutfirst checkout stepmissing on one-page checkouts
Shipping/payment infoadd_shipping_info, add_payment_infocheckout stepsskipped entirely, so you can't see where people drop
Purchasepurchaseconfirmed paymentfired before payment, counting failed payments
Refundrefundbackend, when processednever sent, so revenue in GA4 stays inflated

Fire add_to_cart on the cart API's success response, not the button click, and purchase on payment confirmation, not on "order placed." Both mistakes inflate the funnel in ways that look like real behaviour.

QA: a 20-minute test before every release

  • Open GA4 DebugView and enable the debug mode (Tag Assistant or the debug_mode parameter)
  • Go through the whole funnel with a real test order
  • Check every event fires once, with currency, value and a non-empty items array
  • Confirm transaction_id matches the order number in the admin
  • Reload the thank-you page: no second purchase
  • Pay with a method that redirects to an external page, and check purchase still fires (or comes from the server; see server-side tracking)
  • Refund the test order and check the refund event arrives

Validating with SQL after launch

If the BigQuery export is on, compare GA4 purchases with backend orders daily:

WITH ga AS (
  SELECT ecommerce.transaction_id AS order_id, MIN(event_date) AS day
  FROM `project.analytics_123456.events_*`
  WHERE _TABLE_SUFFIX BETWEEN @from AND @to AND event_name = 'purchase'
  GROUP BY 1
)
SELECT o.order_date,
       COUNT(*) AS backend_orders,
       COUNTIF(ga.order_id IS NOT NULL) AS seen_in_ga4,
       SAFE_DIVIDE(COUNTIF(ga.order_id IS NOT NULL), COUNT(*)) AS coverage
FROM clean.orders o
LEFT JOIN ga ON ga.order_id = o.order_id
WHERE o.status = 'paid' AND o.order_date BETWEEN @from_date AND @to_date
GROUP BY 1 ORDER BY 1;

Coverage below 100% is normal because of consent and blockers. A sudden drop on a specific day usually means a release broke the tag. That's why this query belongs in daily monitoring.

Keeping it working after launch

Tracking rarely breaks on launch day. It breaks three months later, when someone redesigns the checkout, adds a payment method or migrates the theme. Three habits prevent most of that:

  • Add the QA list above to the release checklist for any change touching product pages, cart or checkout.
  • Monitor coverage daily with the SQL above, and alert when it drops more than ~10 points below its usual level.
  • Keep a tracking spec: one table listing each event, where it fires, its parameters and an example payload. Developers can then change the site without guessing what the analytics depends on.

FAQ

Can I set up GA4 ecommerce without a developer?

Partly, through platform integrations. Reliable data (dataLayer, de-duplication, server-side events) usually takes a few developer hours working from an analyst's spec.

GTM or gtag.js?

GTM is easier to manage and test without site releases. gtag.js is simpler when you have few events and your own developers.

Why does GA4 show fewer purchases than my store?

Ad blockers, declined cookies, hosted payment pages with no return to the site, and phone orders. There's a separate article on this.

Should I send cancellations?

Send refunds as refund. Orders cancelled before payment are easier to handle in the backend than in GA4.

Tags: GA4ecommercetrackingGTM

Author

DATUMPANEL — Business data analytics

DATUMPANEL is an independent data analyst based in Ukraine. I build dashboards for business owners, set up GA4 and the BigQuery export, and run data pipelines and Telegram auto-reports for e-commerce, service businesses, SaaS and agencies, from data audit to decisions.

Message on Telegram[email protected]Open the demo

Keep reading

  • GA4 & tracking11 min read

    GA4 BigQuery export: a practical guide for business owners

    Why you should turn on the GA4 BigQuery export today, how the events_ tables and nested event_params work, what it costs, and the first SQL queries worth running.

    Pillar guide

  • GA4 & tracking7 min read

    Analytics audit: a 40-point checklist

    A step-by-step audit of business analytics covering GA4, tags, UTMs, conversions, consent, CRM, ads, the warehouse and dashboards, with a 40-point checklist, fix priorities and a report template.

  • GA4 & tracking7 min read

    Server-side tracking and the GA4 Measurement Protocol: when you need it

    What server-side tracking is, how server-side GTM differs from the GA4 Measurement Protocol, which events belong on the backend (payments, refunds, offline sales), and how to respect consent and avoid duplicates.

Want this kind of analytics for your business?

Tell me what data you have and which questions it can’t answer yet. I reply the same day with a plan, a timeline and the quick wins.

Or reach me directly:

Open the demo →

Send a request

I reply within one business day.

What are you interested in
Industry
A few more details · optional
Approximate budget · optional
When to start · optional
Data sources you have · optional

By submitting, you agree to the Privacy policy — your contact details are used only to reply to this request.