Skilly

ga4-ecommerce-event-auditor

Analytics & tracking v3
@shopmetric Instalace: 237 aktualizováno před 44 dny Licence MIT

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-reconciler or 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:

  1. Event sample, ideally the GA4 BigQuery export for 7 full days (events_YYYYMMDD tables). 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.
  2. 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.
  3. 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)

  1. currency whenever value is sent. Google's event reference marks currency as required if value is set, in 3-letter ISO 4217 format. Without it, the event is not counted as revenue. Check every ecommerce event that carries value, not only purchase.
  2. value is 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 of int_value, double_value or string_value is populated.
  3. transaction_id on purchase and refund is 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.
  4. items array present on item events, and each item has item_id or item_name (one is required). Without items, item reports and item-scoped funnels are empty.

Value semantics (severity: high, because it corrupts revenue quietly)

  1. value = Σ(price × quantity) over the items, and it does not include shipping or tax. Those go into their own parameters on purchase and refund. Test this on every purchase in the sample: abs(value − Σ price×quantity) > 0.01 is 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.
  2. price is the discounted unit price. When a discount applies, set price to the discounted unit price and put the unit discount into the item discount parameter. A common bug is price = line total (unit × quantity). Detect it where quantity > 1 and price equals the backend line total.
  3. quantity defaults to 1 if absent. A missing quantity on multi-unit lines understates item revenue and units.
  4. Consistent item_id across events. The same product must use the same id in view_item, add_to_cart and purchase, and it should match the feed id if 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)

  1. 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, with remove_from_cart, refund, view_promotion/select_promotion where relevant. A step with zero events is untagged, and GA4 funnel and checkout-journey reports will show it as a 100% drop.
  2. 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.
  3. add_shipping_info carries shipping_tier and add_payment_info carries payment_type, with human-readable, stable values ("Zásilkovna pickup", "card"). Report the distinct values, because they are the most useful checkout segmentation available.
  4. Each payment method is covered. Split purchase coverage by payment_method from 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)

  1. 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.
  2. 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_cart events carrying the whole viewed list.
  3. 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)

  1. 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.
  2. 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

  1. The items array 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.
  2. Event-level and item-level coupon are independent. Check that each is filled where the store uses it.

Process

  1. 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.
  2. 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_location patterns).
  3. Order check. Compare consecutive steps at session level. Flag every step where sessions(step n) > sessions(step n−1).
  4. 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
  5. 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 the value sum check from rule 5.
  6. Backend match. Normalise ids (trim, case, strip prefixes), then compute:
    • match_rate = matched backend web orders ÷ backend web orders
    • value_agreement = share of matched orders where |GA4 value − backend comparable| ≤ 0.01
    • GA4-only ids, split by hostname
    • match rate by payment_method and 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.
  7. 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).
  8. Output the table and the fix list, then run the quality checklist.

Pitfalls and edge cases

  • Platform plugins (Shoptet, Upgates, Shopify apps) often send value including 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: currency must 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_info and add_payment_info at 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_id stored as number vs string: order 001234 becomes 1234. Normalise before joining.
  • items[].price of 0 for free gifts is valid. Do not flag it. price missing 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_id or 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_item 48,210 events; add_to_cart 6,930; begin_checkout 2,410; add_shipping_info 1,980; add_payment_info 0; purchase 1,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 have value but no currency.
  • add_to_cart: on 1,247 of 6,930 events (18.0%), quantity > 1 and price equals 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

License

MIT