DEV Community

Cover image for Shopify to GA4 without GTM: native events plus a server-side Measurement Protocol fallback
Luke Sandelands
Luke Sandelands

Posted on Originally published at stackarchitect.xyz

Shopify to GA4 without GTM: native events plus a server-side Measurement Protocol fallback

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
Enter fullscreen mode Exit fullscreen mode

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}}
          }
        ]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Two gotchas worth flagging:

  • value, tax, shipping, quantity and price are 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, /debug prefix) while you're building; it returns a validationMessages array.
  • client_id determines 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 _ga cookie 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)