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

Contents
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.

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.