GA4 ecommerce event auditor
Audits a store's GA4 ecommerce implementation event by event and parameter by parameter against Google's documented recommended-event schema. It then checks the purchase stream against backend orders and returns a pass/fail table with a prioritised fix list for the developer. The naive audit opens DebugView, sees purchase arrive and calls it done. That misses the failures that actually corrupt reports: a value without currency, which GA4 does not report as revenue; an empty transaction_id, which makes GA4 deduplicate every such purchase into one; line totals sent as unit prices; and events that fire twice on some templates and never on others. These show up only in a sample of real events and a join to the order book.
When to use
- After any change to the theme, checkout, payment gateway, consent banner, GTM container or shop-platform GA4 plugin.
- As the gate before
ga4-funnel-analyst,revenue-discrepancy-reconcileror any revenue-by-channel analysis. - When GA4 revenue or purchase counts step-change without a business reason.
- Before GA4 key events are imported into Google Ads for value-based bidding.
When not to use
- The question is only "why do the three tools disagree on revenue?" and events are known to be clean. Use
revenue-discrepancy-reconciler. - App (Firebase) implementations. Most of the schema applies, but deduplication and debugging differ. Say so and limit the audit to what the sample shows.
- Designing a tracking plan from scratch. This skill checks an existing implementation against the schema. It does not write tags.
Inputs
Required:
- Event sample, ideally the GA4 BigQuery export for 7 full days (
events_YYYYMMDDtables). Needed columns:event_name,event_date,event_timestamp,user_pseudo_id,event_params(ga_session_id,page_location,value,currency,transaction_id,coupon,shipping_tier,payment_type),ecommerce.*(transaction_id,purchase_revenue,tax_value,shipping_value,total_item_quantity),items[](all fields),device.category,privacy_info.*,stream_id. Fallback: GTM Preview or GA4 DebugView captures of one complete journey per template (category, product, cart, each checkout step, each payment method), plus Reports > Monetization > Ecommerce purchases for counts. State that a capture-based audit covers only the journeys captured. - Backend orders for the same days, in the GA4 property time zone:
order_id,created_at,channel,payment_method, merchandise ex VAT after discounts,tax,shipping,currency. - Implementation facts: gtag.js, GTM or a platform plugin; server-side tagging or Measurement Protocol in use (yes/no); consent mode type (none, basic, advanced); GA4 property currency and time zone.
Optional: the tracking plan or dataLayer spec, CMP consent-rate report, list of staging hosts.
Missing inputs: without backend orders, skip the match section and say so. Without BigQuery, run the schema checks on captures and mark the counts-based checks (ordering, duplicates) as "not assessed".
Best practices
Schema rules that break reporting (severity: critical)
currencywhenevervalueis sent. Google's event reference markscurrencyas required ifvalueis set, in 3-letter ISO 4217 format. Without it, the event is not counted as revenue. Check every ecommerce event that carriesvalue, not onlypurchase.valueis a number, not a string, with a dot decimal separator and no currency symbol or thousands separator."1 299,00 Kč"is a string that GA4 cannot sum. In BigQuery, check which ofint_value,double_valueorstring_valueis populated.transaction_idonpurchaseandrefundis required. It must be unique per order, not empty, not a constant (undefined,0,null,(not set)), and free of personal data such as e-mail addresses. Google warns that all purchase events with an empty transaction ID are deduplicated together, and that reusing one ID for different transactions can significantly undercount.itemsarray present on item events, and each item hasitem_idoritem_name(one is required). Without items, item reports and item-scoped funnels are empty.
Value semantics (severity: high, because it corrupts revenue quietly)
value= Σ(price×quantity) over the items, and it does not includeshippingortax. Those go into their own parameters onpurchaseandrefund. Test this on every purchase in the sample:abs(value − Σ price×quantity) > 0.01is a finding. If the store deliberately sends a different basis (for example after the order-level coupon, or incl. VAT), write down the basis and check that it is consistent, because downstream reconciliation depends on it.priceis the discounted unit price. When a discount applies, setpriceto the discounted unit price and put the unit discount into the itemdiscountparameter. A common bug isprice= line total (unit × quantity). Detect it wherequantity > 1andpriceequals the backend line total.quantitydefaults to 1 if absent. A missing quantity on multi-unit lines understates item revenue and units.- Consistent
item_idacross events. The same product must use the same id inview_item,add_to_cartandpurchase, and it should match the feedidif Merchant Center or Ads use the data. Parent vs variant id mixing breaks item-level funnels. Report the match rate of purchase item ids to view_item item ids.
Coverage and order (severity: high)
- Every step exists. The core sequence is
view_item_list/select_item→view_item→add_to_cart→view_cart→begin_checkout→add_shipping_info→add_payment_info→purchase, withremove_from_cart,refund,view_promotion/select_promotionwhere relevant. A step with zero events is untagged, and GA4 funnel and checkout-journey reports will show it as a 100% drop. - Counts fall down the funnel at session level. Count distinct sessions (
user_pseudo_id+ga_session_id) per step, not events. If a later step has more sessions than the step before it, the earlier one is under-tagged on some template, or the later one double-fires. add_shipping_infocarriesshipping_tierandadd_payment_infocarriespayment_type, with human-readable, stable values ("Zásilkovna pickup", "card"). Report the distinct values, because they are the most useful checkout segmentation available.- Each payment method is covered. Split purchase coverage by
payment_methodfrom the backend. Off-site gateways (bank buttons, 3-D Secure, buy now pay later) often lose the return to the thank-you page.
Duplicates and the dataLayer (severity: high)
- Deduplication scope: GA4 deduplicates purchases with the same transaction ID from the same user, and only on web streams. Duplicates across different users (a thank-you page opened on another device, server-side and browser events with different client IDs) stay in the data. Measure
COUNT(*) − COUNT(DISTINCT transaction_id)in BigQuery. - Clear the ecommerce object in GTM. Google's ecommerce guide shows
dataLayer.push({ ecommerce: null })before each ecommerce push, so items from a previous event are not merged into the next one. Symptom:add_to_cartevents carrying the whole viewed list. - One purchase source. If both a browser tag and a server-side or Measurement Protocol purchase are active, confirm that only one sends
purchase, or that both share the same client ID and transaction ID.
Consent and completeness (severity: context)
- The backend match rate cannot reach 100% under consent mode. With basic consent mode, nothing is sent for users who deny. With advanced mode, cookieless pings are sent, but they are not linked to a user. Compare the match rate with the CMP's analytics-consent rate among buyers when available, instead of a fixed threshold. A match rate well below the consent rate, or a drop against the previous audit, points to tagging.
- Check the GA4 purchase events for staging or other hostnames (
page_location). A test site sending to the production property inflates revenue.
Limits worth knowing
- The
itemsarray can hold up to 200 elements, and up to 27 custom item-scoped parameters in addition to the prescribed ones. Carts beyond that are truncated. - Event-level and item-level
couponare independent. Check that each is filled where the store uses it.
Process
- Scope. Record the property, stream(s), dates, sample source and size, implementation method and consent mode type. Filter out internal and staging hostnames and state how many events were removed.
- Coverage table. For each ecommerce event, give the event count, session count, share of sessions with that event, and the templates or pages where it fires (top
page_locationpatterns). - Order check. Compare consecutive steps at session level. Flag every step where
sessions(step n) > sessions(step n−1). - Schema checks per event. For each rule 1–8 and 11, compute the failing share = failing events ÷ events. Use this severity:
- critical: rules 1, 2, 3, 4 (revenue or items missing)
- high: rules 5, 6, 8, 9, 13, 15
- medium: rules 7, 11, 19
- Purchase integrity. Compute
events,distinct transaction_ids,empty or constant ids,repeat events on the same id(split by same user vs different user), and thevaluesum check from rule 5. - Backend match. Normalise ids (trim, case, strip prefixes), then compute:
match_rate = matched backend web orders ÷ backend web ordersvalue_agreement = share of matched orders where |GA4 value − backend comparable| ≤ 0.01GA4-only ids, split by hostname- match rate by
payment_methodand device. A method more than a few points below the others is a finding. Name it, but do not invent a threshold for the user.
- Prioritise. Critical first, then by revenue affected (failing share × revenue of the event). Give each fix as: what to change, where, how to verify (DebugView check or BigQuery query).
- Output the table and the fix list, then run the quality checklist.
Pitfalls and edge cases
- Platform plugins (Shoptet, Upgates, Shopify apps) often send
valueincluding VAT or shipping. That is not wrong for reporting consistency, but it must be known and not "fixed" silently, because it would break year-over-year comparisons. Propose the change with its impact on history. - Multi-currency shops:
currencymust be the currency of the transaction, not the property's. GA4 converts at the previous day's rate. Check that the currency switcher updates the dataLayer. - Single-page checkouts fire
add_shipping_infoandadd_payment_infoat the moment the options are chosen. Some themes fire them on page load with default values, which makes the step look 100% completed. - Thank-you page as a GTM trigger on URL: reloads and back-button visits re-fire it. The transaction ID limits the damage for the same user only.
- Numeric
transaction_idstored as number vs string: order001234becomes1234. Normalise before joining. items[].priceof 0 for free gifts is valid. Do not flag it.pricemissing is a finding.- DebugView shows only debug devices: absence in DebugView is not absence in production.
- BigQuery daily tables can be updated after the day ends, for late hits from apps and Measurement Protocol. Audit days that are at least 3 days old, or state that recent days may change.
- Consent denied with advanced mode: events may arrive without
user_pseudo_id. Count them separately and do not label them as a tagging defect.
Rules
- Read-only. No changes to GTM, gtag, the theme, plugins or GA4 settings. Output is a fix list for a developer, who deploys after a human approves.
- Report only what the sample shows, and state the sample source, dates and size. Never infer that an event exists because "it usually does".
- Every finding has a count and a denominator ("23 of 1,102 purchase events").
- Do not include personal data from the order export or from event parameters in the output. If a
transaction_idor any parameter contains an e-mail address or name, report the pattern and count, not the values, and flag it as a privacy issue. - If a check cannot be run on the available input, write "not assessed" with the reason.
Output format
GA4 ecommerce audit — <property / stream> · <dates> (<time zone>)
Sample: <BigQuery | captures> · <n events> · implementation <GTM|gtag|plugin> · consent mode <none|basic|advanced>
Excluded: <n events from hosts ...>
## Coverage
| event | events | sessions | % of prev step (sessions) | status |
|---|---|---|---|---|
| view_item | n | n | | ok / finding |
...
## Findings (priority order)
| # | severity | event | rule | affected | fix | verify by |
|---|---|---|---|---|---|---|
| 1 | critical | purchase | transaction_id empty | 23 of 1 102 (2.1%) | ... | ... |
## Purchase vs backend
Backend web orders: n · matched: n (<x%>) · value agreement: <x%> of matched
By payment method: <method x%> · <method y%>
GA4-only ids: n (<hosts>)
Value basis actually sent: <formula>
Not assessed: <checks + reason>
Worked example
Illustrative 7-day sample, 7–13 September 2026, from BigQuery. The implementation is a GTM web container with advanced consent mode, in CZK, Europe/Prague.
- Coverage:
view_item48,210 events;add_to_cart6,930;begin_checkout2,410;add_shipping_info1,980;add_payment_info0;purchase1,102. - Purchase integrity: 23 events have an empty
transaction_id. The other 1,079 events carry 1,066 distinct ids, and 13 are repeats (9 from the same user, which GA4 deduplicates in reports, and 4 from different users, which it does not). 4 events havevaluebut nocurrency. add_to_cart: on 1,247 of 6,930 events (18.0%),quantity > 1andpriceequals the line total.- Backend: 1,188 web orders; 1,041 matched (87.6%); 25 GA4-only ids, all from
staging.host. That gives 1,041 + 25 = 1,066 ids, which closes the arithmetic. By payment method, card is 91.2% matched and one bank-button gateway 64.0%.
| # | severity | event | rule | affected | fix | verify by |
| 1 | critical | purchase | transaction_id empty | 23 of 1 102 (2.1%) | read order id from dataLayer on the thank-you template used by bank-button returns | BigQuery: count empty ids = 0 for 3 days |
| 2 | critical | purchase | currency missing with value | 4 of 1 102 | set currency from order, not from session | DebugView on a EUR order |
| 3 | high | add_payment_info | event missing | 0 events | fire on payment method selection with payment_type | step sessions ≤ add_shipping_info |
| 4 | high | purchase | GA4-only ids from staging | 25 ids | separate measurement ID for staging | no staging host in export |
| 5 | high | add_to_cart | price = line total | 1 247 of 6 930 (18.0%) | send unit price; quantity separately | price × quantity = line total |
Backend match 87.6%; bank-button gateway 64.0% vs card 91.2% → check return URL of that gateway before attributing the gap to consent.
Quality checklist
- Sample source, dates, size, time zone and exclusions are stated.
- Every ecommerce event in scope has a row, including zero-count events.
- Every finding has a count, a denominator, a severity, a fix and a verification step.
- The step order was checked on sessions, not on event counts.
- The purchase id arithmetic closes: distinct ids = matched + GA4-only, and events = distinct ids + repeats + empty.
- The consent context is stated, and the match rate is not judged against an invented benchmark.
- No personal data appears in the output. Anything not assessed is listed.
Sources
- GA4 recommended events reference: https://developers.google.com/analytics/devguides/collection/ga4/reference/events
- GA4 measure ecommerce (items limits, refunds, clearing the ecommerce object): https://developers.google.com/analytics/devguides/collection/ga4/ecommerce
- [GA4] Minimize duplicate key events with transaction IDs: https://support.google.com/analytics/answer/12313109
- [GA4] Set up ecommerce events: https://support.google.com/analytics/answer/12200568
- [GA4] Currency reference: https://support.google.com/analytics/answer/9796179
- [GA4] Fix missing revenue data: https://support.google.com/analytics/answer/13800978
- [GA4] Behavioral modeling for consent mode: https://support.google.com/analytics/answer/11161109
- GA4 BigQuery export schema: https://support.google.com/analytics/answer/7029846
License
MIT