Skip to content
← ALL WRITING

2026-04-23 / 9 MIN READ

GA4 migration playbook for DTC: what actually breaks

A tutorial walkthrough on the GA4 migration for DTC brands. The specific events, props, and joins that fail during the move and the fixes that hold up.

Universal Analytics shut down in mid-2024 and most DTC brands still have not cleanly finished the GA4 migration. They have a GA4 property, they have enhanced ecommerce events firing, and they have a dashboard that kind of works. What they do not have is confidence that the numbers match anything else in the stack.

This is the playbook I run through on every DTC GA4 migration that needs a second pass. It is specifically about what actually breaks in the move, not a reprint of Google's documentation. It assumes you have a Shopify store, a working browser pixel, and a sense that something is off in the numbers.

This walkthrough fits into the warehouse-first analytics rebuild hub at stage 3, where GA4 becomes a downstream consumer of your warehouse instead of the source of truth.

ga4 event name check
1 / 8
event name
purchase
canonical · slotted into funnel report

canonical

GA4 matches event names exactly. Drift costs you the funnel report.

The five things that break in the migration

I keep seeing the same five failures, in roughly the same order.

  1. Enhanced ecommerce event names do not match GA4's expectations
  2. purchase event lacks transaction_id or fires twice
  3. items[] array has the wrong shape for GA4 ecommerce reports
  4. Consent mode v2 is missing or gated wrong
  5. Cross-domain measurement breaks when you move to a custom checkout

Each one looks like a minor config issue from the outside and each one silently destroys a report you care about. I will walk through each, with the actual config that works.

Event name mismatches: begin_checkout versus initiate_checkout

GA4 expects specific event names for enhanced ecommerce. Universal Analytics let you name events anything. Shopify's default integrations still sometimes fire initiate_checkout instead of begin_checkout, and GA4 ignores it silently. Your funnel report shows a massive drop between add_to_cart and purchase because begin_checkout is missing entirely.

The canonical event names GA4 expects are documented at developers.google.com/analytics/devguides/collection/ga4/ecommerce. The ones DTC brands need to get right:

view_item_list, select_item, view_item, add_to_cart,
view_cart, remove_from_cart, begin_checkout,
add_shipping_info, add_payment_info, purchase, refund

Any deviation from these names and GA4 reports them as custom events instead of slotting them into the ecommerce funnel. Custom events are fine for brand-specific actions. Rewriting purchase as order_completed is not fine.

The double-fire purchase problem

The most common single issue I see in a GA4 migration is that purchase is firing twice. Once from Shopify's default integration and once from a GTM tag someone added during the UA-to-GA4 cutover and never removed.

The symptoms: GA4 purchase count is roughly 2x Shopify. Revenue is 2x. ROAS calculations are half of what they should be. The team notices, blames the tracking pixel, files a ticket with Meta, and spends two weeks investigating the wrong thing.

The fix is to pick exactly one source for the purchase event and kill the other. My default is to fire from a server-side ingestion service via Measurement Protocol, but Shopify's native Google Tag integration is also fine if you are staying fully Shopify. Just do not run both.

To verify, open Realtime in GA4, place a test order (refund it after), and watch how many purchase events land. If the count is anything other than one, you have a duplicate firing somewhere.

The items array shape, and why GA4 cares

GA4's items array has a specific shape. If you are off by a field name, the items show up blank in the report and the item_id dimension is useless.

The shape GA4 wants for each item:

{
  item_id: "SKU-001",             // required
  item_name: "Product Title",     // required
  item_brand: "Brand",            // optional but used in reports
  item_category: "Category",      // up to item_category5 supported
  item_variant: "Size: Large",    // optional
  price: 49.00,                   // recommended
  quantity: 2,                    // required for purchase
  discount: 5.00,                 // optional
  currency: "USD"                 // required if not on event
}

The four common drifts I see: item_id arriving as sku (GA4 ignores), price arriving as a string ("49.00" instead of 49.00), quantity missing, and currency set per-event instead of per-item for multi-currency stores.

A warehouse-first rebuild fixes this at the schema layer in the event schema design for DTC pattern library, where the canonical item shape is defined once and every downstream destination reads from it.

Consent mode v2 is the legal gating layer that sits in front of GA4. If you do not have it configured, you are exposing the brand to EU regulator enforcement. If you have it too restrictive, you are invisible in GA4 for 30 to 60 percent of your European traffic.

The right configuration for a mid-market DTC brand:

gtag('consent', 'default', {
  ad_storage: 'denied',
  ad_user_data: 'denied',
  ad_personalization: 'denied',
  analytics_storage: 'denied',
  functionality_storage: 'granted',
  security_storage: 'granted',
  wait_for_update: 500
});

This sets everything except functionality and security to denied by default, with a 500ms window for the consent banner to update the state before GA4 fires. Update the state when the user accepts via your consent management platform (CookieYes, OneTrust, Klaro, etc.).

Two gotchas. First, wait_for_update needs to be long enough for the CMP to load but short enough that analytics_storage resolves before the user navigates away. 500ms is my default and I have not seen a CMP that misses it. Second, consent mode v2 defaults to EU-only enforcement. If your consent banner is showing in the US too, check the geo-targeting in your CMP config.

Cross-domain measurement for custom checkouts

If you moved from Shopify's hosted checkout to a custom checkout (common with headless Hydrogen or a custom post-purchase upsell), cross-domain measurement needs explicit config. Otherwise GA4 treats the checkout domain as a new session and the whole funnel breaks.

In GA4 Admin → Data Streams → Configure tag settings → Configure your domains, add every domain in your checkout path. If you are on brand.comcheckout.brand.comupsell.brand.com, all three need to be in the list. GA4 then passes the client_id in the URL across the boundary and the session stays intact.

To verify, watch the Realtime report while placing a test order across the full funnel. The same active user should persist from brand.com through checkout and back. If a new user appears at the checkout boundary, cross-domain is broken.

The migration order that works

After running this on a dozen stores, the order that minimizes pain:

  1. Audit what exists today. Export the GTM container as JSON. Grep for event names that are not in GA4's canonical list.
  2. Deploy a parallel GA4 tag alongside the existing one with correct event names. Do not delete the old tag yet.
  3. Run both in parallel for 14 days. Compare counts daily. Expect the new tag to land slightly lower than the old (correct behavior; the old was probably double-firing somewhere).
  4. Verify consent mode v2 is configured and the CMP is updating state correctly.
  5. Kill the old tag. Watch GA4 for anomalies for 7 days.
  6. Move to server-side GA4 via Measurement Protocol (see the tutorial).

This is slower than the "rip-and-replace" approach most teams try. It is also the only version I have seen work without a reconciliation-of-death afterwards.

The GA4 migration fails quietly. The numbers look plausible, the reports load, and nobody notices that the funnel is missing a stage until the quarterly review.

FAQ

My GA4 migration shipped a year ago. Should I re-audit?

Yes. Run the five-check audit above. About 70 percent of the migrations I see from 2024-2025 have at least one of the five issues still live. The double-fire purchase is the most common and the cheapest to fix.

Do I need server-side GA4 if my browser pixel works?

You need it if you care about operator-level numbers. Browser-only loses 30 to 60 percent of traffic to ITP, consent gating, and ad blockers. Server-side via Measurement Protocol recovers most of that. See the Measurement Protocol tutorial.

Should I migrate off GA4 entirely?

No, not yet. GA4 is still the de facto standard for Google Ads optimization and a lot of the ad platform algorithms lean on it. Treat it as a downstream consumer fed by your warehouse, not as your source of truth.

What about Universal Analytics historical data?

Export to BigQuery while you still can. Google has been tightening the UA-to-BigQuery pipeline; as of April 2026, you can still pull historical but it requires the GA360 paid tier for the full export. For most mid-market brands, a one-time CSV export of the key reports is enough.

Is consent mode v2 mandatory in the US?

Not federally, but state-level gating (Colorado, California via CCPA/CPRA) is trending toward the same kind of opt-in regime. Most mid-market DTC brands I work with enable consent mode v2 globally as a hedge, with the CMP surfacing the banner based on geo.

What to try this week

Open GA4 Realtime. Place a test order. Count how many purchase events fire. If the count is anything other than one, you have a duplicate firing somewhere, and fixing it will move your ROAS number meaningfully without changing spend.

If you suspect a deeper set of issues than a double-fire, a DTC Stack Audit runs this exact check against your live store along with 23 others across tracking, data quality, and attribution.

Sources and specifics

// related

DTC Stack Audit

If this resonated, the audit covers your tracking layer end-to-end. Server-side CAPI, dedup logic, and attribution gaps - all mapped to your stack.

>See what is covered