E-commerce Data Layer Structure
Your purchase event isn't pushing a dataLayer object that conversion tags can actually read. Either `value`, `currency`, `transaction_id`, or the `items` array is missing, or the whole thing is shaped wrong. Tags still fire on the confirmation page, but the value they ship is zero or undefined. Google Ads counts the conversion. It just thinks every order is worth $0.
Why It Matters
GA4, Google Ads conversion tracking, Meta CAPI, TikTok Events, and most server-side pipelines all read from the same dataLayer object. Each tag subscribes to specific keys inside an `ecommerce` block. The GA4 spec is the canonical shape: an event named `purchase`, a nested `ecommerce` object, and inside it `transaction_id`, `value`, `currency`, and an `items` array of `{ item_id, item_name, price, quantity }` rows. Real implementations break this shape in predictable ways. Sometimes the developer flattens `value` to the top level instead of nesting it inside `ecommerce`. Sometimes `currency` is missing entirely, so Google Ads has no idea whether the $129 was USD or JPY. Sometimes the dataLayer pushes a different shape on different pages — the product-detail-page push uses `ecommerce.items[].id` while the order-confirmation push uses `ecommerce.items[].item_id`, and each tag picks a different one and they silently disagree. Everything keeps working at the surface. Tags fire. Conversions count. Dashboards populate with mostly-zero values and the occasional non-zero spike. Smart Bidding optimises against that noise. ROAS reports diverge from the e-commerce backend by an amount nobody can explain. The team eventually files a ticket asking why reported ROAS doesn't match what the bank shows.
How To Fix It
- Find the code that pushes the `purchase` event. It is typically in the order-confirmation page template or a checkout-success hook.
- Replace whatever shape it currently pushes with the GA4 canonical structure. The example below is verbatim what you want.
- Make sure the push happens before any GTM tag reads from the dataLayer for that event. On most stacks that means rendering the script inline at the top of the confirmation page body, before the GTM snippet.
- In GTM Preview, run a real test purchase. Step through the Variables tab at the `purchase` event. Every variable a conversion tag uses (`{{DLV - ecommerce.value}}`, `{{DLV - ecommerce.currency}}`, etc.) should resolve to a non-empty value.
- Repeat for the refund and partial-cancellation paths if the site supports them. Same shape, different event names (`refund`, `cancellation`).
Example
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'T-12345',
value: 129.99,
currency: 'USD',
items: [
{ item_id: 'SKU-1', item_name: 'Walking Shoes', price: 129.99, quantity: 1 }
]
}
});This GTM container expects a `purchase` dataLayer event but the pushed payload does not match the GA4 recommended e-commerce shape. Per Google's GA4 documentation, conversion tags require a nested `ecommerce` object containing `transaction_id`, `value`, `currency`, and an `items` array. Deviations cause downstream Google Ads conversion-value, GA4 e-commerce, and remarketing-audience scoping to silently degrade. The conversion is still counted; the value is not. Fix: standardise the `purchase` push to the GA4 canonical shape before any GTM tag fires, and verify variable resolution in GTM Preview. Source: developers.google.com/analytics/devguides/collection/ga4/ecommerce.
Drop this paragraph into your client deliverable. Sources back to the canonical platform documentation linked below.
References
Audit your own files for this check
AdLint runs this check (and 177 others) against your GTM, Google Ads, Meta, TikTok, LinkedIn, Pinterest, Twitter/X, and Snapchat exports. Everything stays in your browser. No uploads, no accounts.
Run a free audit