How big should the gap be?
GA4 will never count every Shopify order. Some orders never happen in a browser, and some visitors decline tracking. The goal isn’t a perfect match. It’s a gap you can explain line by line.
The quickest sanity check: compare the gap with your cookie banner’s decline rate. If about 20% of visitors decline and GA4 misses about 20% of online orders, your setup is probably fine. If GA4 misses 40%, something is broken.
Only compare Shopify orders from the Online Store channel, excluding test orders, over a full week or month, in the same timezone as your GA4 property.
The eight causes
Most gaps come from a combination of these. The first three are the most common in audits.
| Cause | Effect | How to spot it |
|---|---|---|
| No purchase event from checkout | Fewer | Large gap that started after old Additional Scripts or checkout.liquid code stopped running |
| Consent declined or pending | Fewer | Gap close to your banner’s decline rate, or tags that wait for consent that never comes |
| Ad blockers and browser protections | Fewer | Gap higher on desktop Safari and Firefox than on mobile apps |
| Orders outside the browser | Fewer | POS, draft, manual and subscription renewal orders in the Shopify Source column |
| Purchase sent without currency | Revenue missing | Purchases counted in GA4 but revenue shows zero |
| Timezone mismatch | Shifts by day | Daily totals disagree but weekly totals match |
| Test, cancelled and refunded orders | Either way | Orders tagged as test or cancelled still counted on one side |
| Duplicate purchase events | More | The same transaction_id appears two or more times in GA4 |
Reconcile by order ID
Totals hide problems. Matching orders one by one shows exactly which ones are missing and what they have in common.
- In Shopify, export last month’s orders. Keep the
Id,Name,Source,TotalandCreated atcolumns. - In GA4, list every
transaction_idfor the same dates, with an Exploration or the BigQuery export. - Match on the field your pixel sends. Some setups send the order ID, others the order name like
#1042. - Group the unmatched orders by source, device, payment method and date. The pattern points to the cause.
With the BigQuery export, this query returns one row per tracked order and flags duplicates:
SELECT
ecommerce.transaction_id,
COUNT(*) AS purchase_events,
MIN(TIMESTAMP_MICROS(event_timestamp)) AS first_seen
FROM `your-project.analytics_123456789.events_*`
WHERE event_name = 'purchase'
AND _TABLE_SUFFIX BETWEEN '20261001' AND '20261031'
GROUP BY 1
ORDER BY purchase_events DESC
Rather have it fixed for you?
Shopify packages from 1300€ rebuild your tracking in four weeks, with GA4, Google Ads and Meta aligned on the same orders.
Fix the checkout event
Since Shopify moved checkout tracking to Customer Events, the reliable way to send purchases is a custom pixel subscribed to checkout_completed. Add it in Settings → Customer events → Add custom pixel.
analytics.subscribe("checkout_completed", (event) => {
const c = event.data.checkout;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "purchase",
ecommerce: {
transaction_id: c.order?.id,
value: c.totalPrice?.amount,
tax: c.totalTax?.amount,
shipping: c.shippingLine?.price?.amount,
currency: c.currencyCode,
items: c.lineItems.map((li) => ({
item_id: li.variant?.sku || li.variant?.id,
item_name: li.title,
item_variant: li.variant?.title,
price: li.variant?.price?.amount,
quantity: li.quantity
}))
}
});
});
This pushes to a dataLayer inside the pixel sandbox. Load GTM inside the same custom pixel so it can read it, and connect consent through the Customer Privacy API before any tag fires.
If the Google & YouTube app already sends purchases to the same GA4 property, adding this pixel will double-count. Keep one source per destination.
What to do next
- Run the reconciliation on last month’s orders.
- Fix missing or duplicate purchase events before touching anything else.
- Then connect consent, so the remaining gap matches your decline rate.
- Recheck after every theme update or new checkout app.