LW IT Solutions
« Blog Overview /Digital Analytics/Tutorials / Tutorial: Migrating E-Commerce Analytics from Client-Side to...
Read this article in other languages:

Tutorial: Migrating E-Commerce Analytics from Client-Side to Server-Side GTM

Tutorial: Migrating E-Commerce Analytics from Client-Side to Server-Side GTM
Contents
  1. Step 1: Run Both, Cut Over Later
  2. Step 2: Provisioning the Tagging Server
  3. Step 3: The GA4 Client
  4. Step 4: Pointing the Web Container at the Server
  5. Step 5: Moving the Purchase Off the Browser
  6. Step 6: Comparing Before Switching Anything Off
  7. Staying Inside the Guardrails
  8. Questions and answers
  9. Sources
  10. Practical check: reconcile orders with the shop export
  11. Preparing the Analytics CSV from BigQuery

Every shop with client-side tracking reports fewer purchases than it processes. The gap is not a bug to be found: requests to well-known analytics hostnames are on filter lists, tracking protection shortens the life of the identifying cookie, and a visitor who closes the tab before the confirmation page finishes rendering never sends the event at all. A server-side container changes where the data travels, which addresses part of that — and moving the purchase event off the browser addresses the rest.

The following walkthrough treats this as a migration rather than a rebuild: the existing client-side setup stays live throughout, the two run side by side long enough to be compared, and the switch happens only once the numbers justify it.

Three delivery paths against one blocker: the client-side hit stopped before it leaves the device, the hit routed via an own subdomain to the server container passing through, and the purchase sent from the shop backend never meeting the barrier at all
Moving the container is what makes the data controllable; moving the purchase off the browser is what makes it complete.

Step 1: Run Both, Cut Over Later

The safest sequence sends data to the server container while the client-side setup keeps reporting into the same property or a parallel one. Nothing is deleted, no tags are paused, and the comparison runs on real traffic rather than on preview mode.

Using a second GA4 property as the migration target keeps the production numbers clean during the overlap — a duplicated dataset in the live property is harder to unpick afterwards than a temporary second property is to throw away. Two to four weeks of overlap covers weekday and weekend patterns and at least one full purchase cycle.

Step 2: Provisioning the Tagging Server

In the GTM interface, a server container is created as its own container type, and its settings offer automatic provisioning into Google Cloud Run. The automatic path is convenient and produces a working deployment; a manual deployment of the same public image gives more control over region and scaling:

gcloud run deploy sgtm \
  --image=gcr.io/cloud-tagging-10302018/gtm-cloud-image:stable \
  --region=europe-central2 \
  --platform=managed \
  --allow-unauthenticated \
  --set-env-vars=CONTAINER_CONFIG='<config string from the container settings>'

The default provisioned settings are sized for testing rather than production traffic, and the instance and memory flags are the part worth revisiting once real volume arrives. Any container runtime works equally well — Cloud Run is the documented default, not a requirement.

The part that is not optional is the hostname. A tagging server reachable only at its generated cloud URL is a third-party host like any other. The benefit of first-party context appears only when it answers on a subdomain of the shop’s own domain, such as sgtm.shop.example.com, with DNS pointing at the service and a certificate mapped to it.

Step 3: The GA4 Client

A server container does nothing until a client claims incoming requests. The built-in GA4 client listens on the paths a Google tag uses, parses the incoming hit, and turns it into an event that server-side tags can read — without it the container receives requests and discards them.

Two settings deserve a decision rather than a default. The client can take over issuing the identifying cookie instead of leaving it to JavaScript, which is what makes the cookie durable against browser restrictions. Switching it on without the accompanying migration option resets the identity of every returning visitor, so it belongs in its own change with its own verification. The second is path handling: leaving the default paths in place is what allows the web container to send hits without any custom endpoint configuration.

Step 4: Pointing the Web Container at the Server

On the browser side a single field redirects the traffic. In a Google tag inside the web container, the server container URL is set as a configuration parameter; in a plain gtag.js implementation, the equivalent is transport_url:

// Google tag configuration parameter (web container)
server_container_url: 'https://sgtm.shop.example.com'

// Equivalent in a plain gtag.js implementation
gtag('config', 'G-XXXXXXXXXX', {
  server_container_url: 'https://sgtm.shop.example.com'
});

From that point the browser addresses the shop’s own subdomain, and the server container forwards to GA4. The server container has its own preview mode, which shows the incoming request, the client that claimed it, and every tag that fired from it — the place where a misrouted hit becomes visible immediately rather than a day later in reporting.

Step 5: Moving the Purchase Off the Browser

Routing through an own subdomain reduces blocking; it does not end it, and it does nothing about the visitor who never reaches the confirmation page. Only an event sent by the backend, after the order is confirmed in the shop system, is independent of the browser entirely.

That request needs the identifiers the browser holds. The client id from the analytics cookie has to be read at checkout and stored on the order, along with the session id, or the purchase arrives without a session to attach to and lands in reporting as traffic with no source:

POST https://sgtm.shop.example.com/g/collect

v=2
&tid=G-XXXXXXXXXX
&cid=1234567890.1690000000     // read from the analytics cookie at checkout
&sid=1789000000                // the session id from the same visit
&en=purchase
&ep.transaction_id=ORD-10432
&epn.value=149.00
&ep.currency=PLN

Sending the same purchase from both the browser and the backend needs the transaction identifier to be consistent, so the duplicate can be recognised and removed downstream — the alternative is a shop whose revenue figures are inflated by exactly the events the migration was meant to recover.

Step 6: Comparing Before Switching Anything Off

The comparison that decides the cutover is not client-side against server-side; it is each of them against the shop’s own order count. A server-side setup reporting more purchases than before is only an improvement if the additional purchases correspond to real orders, and the order table is the reference that settles it.

Typical findings during the overlap: purchase counts rise by a margin that varies heavily by audience and device mix, sessions gain duration because the identifying cookie survives longer, and a handful of tags turn out to depend on browser-only variables that the server container does not have. Those tags are the actual migration work; the container itself is the easy part.

Staying Inside the Guardrails

A server-side container moves the processing, not the obligation. Consent state has to travel with the request and be evaluated before any tag fires, and it has to be stored with the order so the backend event can check it too — a request sent from a server is not exempt because the browser is no longer involved. The container also becomes a system with running costs and an uptime that reporting now depends on, and the identifying cookie it issues is still personal data under the same rules as before, set from a different place.

Questions and answers

Does the tagging server have to be redeployed every time the server container is published?

No. The config string in CONTAINER_CONFIG ties the service to the container, not to a particular version, and the tagging server fetches the currently published version on its own. A redeployment is only needed when a newer image is meant to run: at deploy time Cloud Run resolves gtm-cloud-image:stable to one fixed image version, and a newer one only goes into service with the next gcloud run deploy.

What does the server container’s preview mode need in a manual deployment?

A service of its own. A manual deployment first creates a preview server from the same image, with RUN_AS_PREVIEW_SERVER=true and at most one instance, and then the tagging server, which learns the preview server’s address through PREVIEW_SERVER_URL. Both receive the same CONTAINER_CONFIG.

The command in the article creates only the tagging server. It accepts requests, but the preview, where according to the article a misrouted hit becomes visible immediately, has nowhere to run. Automatic provisioning creates both services by itself; choosing the manual path means taking over that part.

Is the shop’s own subdomain enough to stop Safari from shortening the cookie set by the server container?

Not in every case. For cookies from a Set-Cookie header, Safari also checks where a subdomain actually points. If it is mapped via CNAME to a foreign host, as is common when a custom domain is mapped to Cloud Run, or if it answers from an IP address outside the website’s own address range, Safari caps these cookies at seven days as well.

The subdomain remains the precondition the article names: without it there is no first-party context at all. For the full lifetime in Safari, though, the tagging server has to be reachable through the same infrastructure as the shop, for instance through a path on the shop domain that a load balancer or CDN passes on to the server container.

Practical check: reconcile orders with the shop export

The Shop vs. GA4 Revenue Reconciliation tool compares two CSV files locally. The order ID is the join key. No personal customer data is needed: an order ID, amount and currency are enough.

  1. Prepare both exports for the same period and timezone, with the same treatment of tax, shipping and refunds. Aggregate line items and refunds to one agreed amount per order before comparison.
  2. Select each file’s separator and decimal mark, then load it. Map the order ID, amount and currency columns. Identifiers with leading zeros remain text.
  3. Select the decimal precision and an explicit per-order tolerance. Currency conversion is not performed.
  4. Review the separate groups: missing on either side, duplicate ID, different currency and amount outside tolerance.
  5. Export the result and investigate individual orders in the source systems.

Duplicate IDs are flagged rather than added together: a repeated purchase event, an item-level export and a refund row require different interpretations. Negative amounts are accepted only as numbers; the business definition still has to agree between files. A missing Analytics order can reflect consent, timing, filtering or collection failures and is not by itself proof of a faulty tag. A zero difference also does not establish that all orders were exported. The tool never combines totals across currencies.

Definitions should follow the chosen export and the GA4 ecommerce documentation. The Data API schema distinguishes transaction IDs and revenue metrics; a generic total-revenue column is not automatically equivalent to a shop’s order amount.

Preparing the Analytics CSV from BigQuery

The export helper in the revenue reconciliation tool generates a purchase query for an existing daily GA4 export. Project, dataset, dates and rounding precision are entered locally. The page does not run the SQL or request BigQuery access.

The resulting columns transaction_id, amount and currency match the comparison fields. Each recorded purchase remains a row. An additional purchase-event count helps identify repeated transaction IDs; these are not silently summed or discarded. A missing amount is not replaced with zero.

  1. Agree on the purchase-amount definition and reporting-day boundaries before creating the shop file. This query excludes refunds and does not add tax or shipping separately.
  2. Generate SQL, validate it in BigQuery and inspect the cost estimate. Set a maximum bytes billed before execution.
  3. Save the complete results as a local CSV. Load the file with comma-separated fields and decimal dot, using the same precision as the query.
  4. Review repeated IDs and missing fields before interpreting amount differences. The comparison accepts up to 20,000 data rows per file. Keep identifiers as text when using spreadsheet software.

Two downloadable invented CSV files provide a reproducible first test, with one case in each of the six result groups. They are independent of the SQL and use two decimals with zero tolerance.

References: GA4 export schema, query validation and cost controls, and saving query results as CSV.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Write a comment

Differing figures from other accounts and questions about the setup are welcome here.

The email address is not published. Required fields are marked with an asterisk.

Articles & categories

CCTV

Follow this category by RSS

Cloud & AI

All 11 articles in this category Follow this category by RSS

Data Privacy

All 18 articles in this category Follow this category by RSS

Digital Analytics

All 58 articles in this category Follow this category by RSS

Digital Marketing

All 37 articles in this category Follow this category by RSS

IT & Networks

All 17 articles in this category Follow this category by RSS

Music Production

All 16 articles in this category Follow this category by RSS

Raspberry PI

Follow this category by RSS

SaaS & Internet Earning

Articles coming soon

Smart Home

All 18 articles in this category Follow this category by RSS

Web Development

All 11 articles in this category Follow this category by RSS

WordPress Plugins & Tricks

All 13 articles in this category Follow this category by RSS