Every e-commerce client we take on shows the same pattern in week one: WooCommerce reports 340 orders for the month, Google Ads reports 190, GA4 reports 220. Three sources of truth, none of them right. The knee-jerk fix everyone reaches for is Enhanced Conversions for Web. Turn it on, enable auto-collection, done.
Enhanced Conversions for Web helps. It does not fix the gap. The reason is that each layer of conversion tracking fails in a different part of the funnel, and only a stack that covers all four gaps produces reliable attribution.
Where Each Layer Actually Loses Conversions
| Layer | Loses conversions when | Typical loss |
|---|---|---|
| gtag conversion snippet | Ad-blocker, iOS ITP, consent denied, JS error | 15–35% |
| Enhanced Conversions for Web | Payment page loads on a third-party domain, or user checked out as guest with no email match | 10–25% |
| GA4 Measurement Protocol | Server never runs (front-end order, no webhook), or session context is missing | Recovers what gtag lost — if wired |
| Enhanced Offline Conversions (EOC) | gclid stored on order + upload runs — closes what everything else misses | Recovers up to 90% of remaining gap |
The mistake most teams make is treating these as alternatives — “we use EOC instead of gtag” or “we use MP instead of EOC.” They're not alternatives. Each one plugs a different hole. Deploying all four is how you get from 65% attribution to 92%.
Layer 1: The gtag Conversion Snippet on Thank-You
This is the baseline. When a purchase completes and lands on the thank-you page, fire a gtag('event', 'conversion', {...}) with the order value, currency, and transaction_id. In WooCommerce, hook it into woocommerce_thankyou. In Shopify, drop it in Additional Scripts on the Order Status page (or the Web Pixels API on Plus).
transaction_id — but only within a specific window and only if the same ID reaches every source. Use the raw order ID from your platform. Don't append prefixes, don't hash it, don't strip zeros. The same string must appear in gtag, Enhanced Conversions payload, and any EOC upload. Mismatch → double-counting or Google Ads rejecting the offline record entirely.This layer catches everyone whose browser loaded gtag and stayed on your domain to the thank-you page. That's the biggest chunk in most accounts — but it's also the layer that suffers most from ad-blockers, third-party payment redirects, and Consent Mode denies.
Layer 2: Enhanced Conversions for Web
Enhanced Conversions for Web adds first-party user data (hashed email, phone, name, address) to the gtag conversion event. Google matches those hashes against logged-in accounts and recovers the click-to-conversion link even when the original cookie was blocked.
The recovery is real but bounded. If the user checked out as a guest and never gave an email that matches a Google account, no recovery happens. If Consent Mode is denied and the entire gtag event never fires, EOC has nothing to enhance — the conversion is lost at layer 1, and enhancement cannot resurrect what never existed.
Turn on the enhancement, not just the setting
Enabling “Enhanced Conversions” in Google Ads UI is not enough. The gtag event on your thank-you page must actually include the user data object. In WooCommerce, read customer_email, billing_first_name, etc., from the order object. In Shopify, pull from Shopify.checkout.customer. Hash client-side with SHA-256 in lowercase, no whitespace.
Layer 3: GA4 Measurement Protocol as Redundancy
Server-to-server events sent via the GA4 Measurement Protocol are the backup line for layer 1. When ad-blockers or Consent Mode kill the client-side gtag, your server (WooCommerce webhook, Shopify order webhook, or your platform's equivalent) can still fire a purchase event with the same transaction_id and value.
Two things matter here. First, this is a GA4 layer, not a Google Ads layer — MP events reach GA4, and Google Ads pulls them via the GA4 link. Second, MP events do not attach session source correctly by default; you'll need the Custom Channel Group workaround covered in the GA4 sessionSource article to keep them out of Direct.
Wire the same client_id
MP events must carry the same client_id that the browser used. Read the _ga cookie server-side (available in every request), extract the client_id, and pass it as client_id in the MP payload. Otherwise GA4 treats the MP event as a new user and de-links attribution entirely.
Layer 4: Enhanced Offline Conversions (The Last-Resort Layer)
Enhanced Offline Conversions upload finalized orders to Google Ads directly, matched against the original gclid stored on the order. This is the layer that recovers whatever the first three missed — guest checkouts, users who never accepted Consent Mode, browsers that blocked everything.
The stack works because EOC has a different failure mode from every other layer. As long as your platform captured gclid at landing and stored it on the order, EOC can upload it days later and Google Ads will still match it. The only requirement is the gclid — no cookie, no gtag fire needed at time of purchase.
On every landing, read ?gclid= and ?_gcl_aw= from the URL. Store in a first-party cookie with 90-day lifetime. On order create, write both to the order's metadata table. Waiting until checkout to capture loses users who browsed for days before buying — the gclid was gone by then.
Google Ads → Conversions → New → Import → From clicks. The upload action must exist before you can send data. Note the ID it returns. Wait 4–6 hours after creation before uploading — the action takes time to become writable.
Run the upload job daily against orders from 24–48 hours ago. The lag lets your gtag/EOC layers claim their share first, so EOC only fills the residuals. Uploading in real-time causes over-counting because gtag also fires the same conversion.
The minimum viable payload. Add first-party user data (hashed email, phone) to make it Enhanced. Set partial_failure=True so one bad row doesn't kill the batch. Log which orders uploaded and which errored, so you can replay.
How to Sequence the Four Layers Without Double-Counting
The single biggest fear teams have with a stack is double-counting. It's avoidable if you configure Google Ads deduplication correctly:
| Layer | Send transaction_id | Include user data |
|---|---|---|
| gtag on thank-you | Yes — the raw order ID | Yes — for Enhanced enhancement |
| MP to GA4 | Yes — same order ID | No (goes to GA4, not Ads) |
| EOC upload | Yes — same order ID as order_id field | Yes — for Enhanced enhancement |
Google Ads deduplicates within an action based on transaction_id. As long as gtag and EOC send the same transaction_id for the same order, only one will count. GA4 is a separate reporting layer entirely — the MP event lives there, doesn't compete with the Ads-side data.
Expected Recovery from Each Layer, Real Numbers
From an EU WooCommerce client where we deployed the full stack over five weeks:
| Baseline | gtag only | + EOC Web | + MP | + EOC upload |
|---|---|---|---|---|
| Conversions attributed vs WooCommerce truth | 62% | 74% | 81% | 93% |
62% to 93% over five weeks, at the cost of one snippet, one CMP toggle, one MP endpoint, and one daily cron. No single layer alone got past 81%. The full stack made the difference.
When to Build the Stack vs. When to Stop at Layer 2
If your monthly ad spend is under $2K and your order volume is under 50/mo, layers 1 and 2 are usually enough — the incremental data from layers 3 and 4 is real but small, and the engineering cost isn't justified. Above $5K/mo or in EU markets with heavy Consent Mode denies, the full stack pays for itself within a month through better Smart Bidding signals and modeled conversion recovery.
We build this stack for every mid-to-large e-commerce account in our Enhanced Conversions service. The full technical playbook is more than fits in a blog post — but the four layers, in this order, are what makes the difference between an attribution rebuild that works and one that plateaus at 75%.