If you run a Shopify store, the GA4 setup question usually gets answered with "install Google Tag Manager." You don't need it. Shopify's Google & YouTube sales channel emits the whole GA4 ecommerce event set natively, and it takes about five minutes.
The part nobody covers is what happens after: your GA4 purchase count drifts below your Shopify order count and stays there. That gap is browser-side event loss — ITP, ad blockers, iOS — and no amount of tag configuration fixes it, because the event never leaves the client. The fix is a server-side purchase event hitting GA4's Measurement Protocol from a webhook.
This walks through both layers.
What the native integration actually gives you
Once the Google & YouTube channel is connected, Shopify emits these to GA4 with zero further config:
| Event | Fires on |
|---|---|
page_view |
every page load |
view_item |
product page view |
add_to_cart |
cart addition |
view_cart |
cart page view |
begin_checkout |
checkout started |
add_shipping_info |
shipping details entered |
add_payment_info |
payment details entered |
purchase |
order completed, with revenue, tax and shipping |
That's the complete funnel. What it does not give you:
- Profit per order. GA4 tracks revenue. COGS, payment fees and net margin live elsewhere.
-
Ad-click attribution. GA4 is analytics, not conversion tracking. Google Ads Enhanced Conversions is a separate system with separate plumbing (it needs the
gclid). - Cross-order LTV. Needs a GA4 User ID implementation or a CRM.
Step 1 — Create the GA4 property
Skip if you already have one wired up.
analytics.google.com → Admin → Create → Property. Set the reporting timezone and currency to match your store (GBP if you're UK-based — a currency mismatch here quietly corrupts every revenue figure downstream). Business category: Ecommerce.
Then Admin → Data Streams → Add stream → Web. Enter your store URL without the scheme. Copy the Measurement ID — G-XXXXXXXXXX. You need it twice.
Check Enhanced Measurement is toggled on in the stream settings. That's scrolls, outbound clicks, site search, video engagement and file downloads for free.
Step 2 — Connect Shopify
Shopify Admin → Sales channels → Google & YouTube. Install it if it isn't there.
Connect the Google account that owns the GA4 property — this is the step people get wrong. If the channel is authed to a different Google account than the one holding the property, the dropdown in the next step will be empty and the error message won't tell you why.
Then: Settings → Measurement → Google Analytics 4 → select your property → Connect. Your G- ID should appear next to a "Connected" status.
Step 3 — Verify before you build anything else
Realtime: GA4 → Reports → Realtime. Open your storefront in another tab, click around. You should appear as an active user inside 30 seconds with your page path visible.
Purchase event: place a test order using a 100% discount code. Watch for purchase in Realtime within 30–60 seconds.
DebugView: for event-level detail, Admin → DebugView, then load your store with ?gtm_debug=x appended. Events stream in near-real-time with their full parameter payloads, which is the fastest way to spot a malformed items array.
If nothing arrives: check the channel shows Connected, check the account ownership issue above, and check the store has at least one active product.
Step 4 — Three config changes worth making immediately
Data retention → 14 months. Admin → Data Settings → Data Retention → Event data retention. The default is 2 months, which makes year-over-year comparison impossible. 14 is the free-tier max. Do this on day one — it isn't retroactive.
Google Signals. Admin → Data Settings → Data Collection. Enables cross-device and demographic reporting from aggregated signed-in Google users.
Search Console link. Admin → Property Settings → Product Links → Search Console. Pulls organic query data into GA4 reports.
Mark purchase as a key event. Admin → Events → Mark as key event. Required if you ever want to import it into Google Ads.
Step 5 — The server-side layer
Here's the actual engineering problem. The native integration fires from the customer's browser. Safari ITP, ad blockers and iOS tracking restrictions mean a meaningful share of purchase events never reach Google's collection endpoint. Shopify recorded the order; GA4 didn't hear about it.
Measurement Protocol solves this because it's a plain HTTPS POST from a server. No cookies, no client, no browser state to block.
Architecture: Shopify orders/paid webhook → Make.com (or any webhook runner — n8n, a Lambda, a Cloudflare Worker) → POST https://www.google-analytics.com/mp/collect.
You need an API secret: GA4 → Admin → Data Streams → your stream → Measurement Protocol API secrets → Create. Copy the value; it's only shown once.
Endpoint:
POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
Content-Type: application/json
Payload:
{
"client_id": "{{webhook.customer.id}}",
"events": [
{
"name": "purchase",
"params": {
"transaction_id": "{{webhook.order_number}}",
"value": {{webhook.total_price}},
"currency": "{{webhook.currency}}",
"tax": {{webhook.total_tax}},
"shipping": {{webhook.total_shipping_price_set.shop_money.amount}},
"items": [
{
"item_id": "{{webhook.line_items[].variant_id}}",
"item_name": "{{webhook.line_items[].title}}",
"quantity": {{webhook.line_items[].quantity}},
"price": {{webhook.line_items[].price}}
}
]
}
}
]
}
Two gotchas worth flagging:
-
value,tax,shipping,quantityandpriceare unquoted. They're numbers. Quote them and GA4 accepts the request with a 2xx and silently discards the event — the Measurement Protocol does not return validation errors on the collect endpoint. Use/debug/mp/collect(same URL,/debugprefix) while you're building; it returns avalidationMessagesarray. -
client_iddetermines session stitching. Using the Shopify customer ID means the server-side event won't join up with the browser-side session for that user, so attribution reports treat it as direct. If you care about stitching, capture the_gacookie value client-side at checkout and pass that through instead. If you only care about accurate order and revenue counts, the customer ID is fine.
Also worth knowing: if both layers fire for the same order you'll get duplicates. GA4 does not deduplicate on transaction_id the way Meta CAPI deduplicates on event_id. In practice the native pixel drops enough events that most stores accept a small overcount, but if that's unacceptable, the clean approach is to disable GA4 in the Google & YouTube channel and run purchase entirely server-side, keeping the native layer only for the upper-funnel events.
Test it: place a test order, run the scenario, watch Realtime for the purchase event carrying your order details.
Step 6 — Google Ads link (optional)
GA4 → Admin → Product Links → Google Ads Links → Link. Enable personalised advertising if you want remarketing audiences.
Then in Google Ads: Goals → Conversions → Import → Google Analytics 4 properties, and import the purchase key event. Campaigns now optimise against GA4's purchase data, which — with the server-side layer running — is more complete than the browser-only version.
The reports that are actually useful
- Acquisition → Traffic acquisition — sessions by channel. Sort by Sessions for volume, then by Purchases to see which channels actually convert rather than just arrive.
- Engagement → Pages and screens — find product pages with high views and low add-to-cart. That ratio is your product page optimisation backlog.
- Monetisation → Checkout journey — drop-off at the payment step usually means friction or trust; drop-off at address entry usually means UX.
-
Explore → Funnel exploration — build
view_item → add_to_cart → begin_checkout → purchase. Compare each step against your own prior months. The step whose rate falls is where to look, not the step with the lowest absolute rate.
Sanity checks after a week
- Sessions should roughly track Shopify Analytics visitors. Variance is normal — different bot filtering, different session definitions.
- Purchases vs Shopify orders. Some gap is expected browser-side. After deploying the server-side layer, measure the change against your own pre-deployment baseline rather than against someone else's headline percentage — the size of the gap depends entirely on your traffic's iOS and ad-blocker mix.
- Average session duration under 30s across the board almost always means bot traffic or a filter misconfiguration, not a bad site.
- Engaged sessions (>10s, or a conversion, or 2+ page views) is your bounce-rate equivalent.
Still short after the server-side layer is live? Check, in order: API secret correct, Measurement ID pointing at the live stream and not a test property, and the scenario actually executing — check the run history for silent failures on the JSON payload.
Same webhook fans out to more than GA4, incidentally: one orders/paid payload can feed Measurement Protocol, Meta CAPI, Google Enhanced Conversions and a P&L sheet in parallel. I've written up the Meta CAPI side and the Google Ads Enhanced Conversions side separately over on stackarchitect.xyz if that's the next thing you're wiring up.
Happy to answer setup questions in the comments — particularly on the client_id stitching problem, which has more nuance than fits here.
Top comments (0)