The Problem
A client migrated to server-side Google Tag Manager last weekend to reduce client-side script weight and stop AdBlock from eating their analytics. The Tag Assistant preview looked perfect. The Realtime report in GA4 showed users. By Monday morning the conversions dashboard had cratered: a 92% drop in purchases overnight, and the few that came through were attributed to "(direct) / (none)" with no campaign data and no gclid.
The actual orders were still happening on the WooCommerce side. Stripe was processing payments. The conversion events were firing on the thank-you page — I could see them in the browser network tab leaving for the tagging server. They just never showed up as conversions in GA4 with the right attribution.
The two specific symptoms: the client_id on the server-forwarded events was different from the client_id the website cookie was using, and the user_agent and IP on the GA4 hit showed up as the tagging server's, not the visitor's. Attribution was being calculated against the wrong user and the wrong source.
Why It Happens
Server-side GTM does not magically forward request context. When the browser sends a Google Analytics request to your tagging server domain, that request lands as a normal HTTPS POST from the visitor's browser. The GA4 client in the tagging container reads parameters from the body and forwards them to Google's collection endpoint. That second request is made by the tagging server, not the browser.
By default the GA4 client maps over the event parameters and the obvious user data, but the IP address Google sees is the tagging server's IP, and the user agent is whatever the server-side fetch library defaults to. Without explicit configuration the client_id can also get regenerated because the GA4 client falls back to a fresh ID when it cannot find the _ga cookie in the incoming request, which happens whenever your client-side script is pointed at a tagging domain on a different eTLD+1 from the site.
The combination kills attribution. Google geolocates the wrong country, the user agent looks like a bot, and the client_id mismatch means GA4 cannot stitch the conversion to the session that started two pages earlier with a gclid on it. Conversions appear, but with no source, no medium, and no campaign, which is why they all bucket into direct.
The server-side tagging request claiming guide covers the cookie domain and the GA4 client config, but the IP and user agent forwarding are easy to miss until conversions disappear.
The Fix
Three things have to be right at the same time: the client_id cookie must reach the tagging server, the GA4 client must claim the request, and the IP and user agent must be forwarded to Google.
Step 1: Put the tagging server on the same root domain as the site. If your site is example.com, the tagging server should be on something like tag.example.com. A custom domain on Cloud Run takes about ten minutes. With both endpoints on .example.com, the _ga cookie set by gtag is sent on every request to the tagging server, which is how the GA4 client recovers the existing client_id.
In your gtag config on the client, point transport_url at the tagging server and write the cookie at the root domain:
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || []
function gtag(){dataLayer.push(arguments)}
gtag('js', new Date())
gtag('config', 'G-XXXXXXX', {
transport_url: 'https://tag.example.com',
first_party_collection: true,
cookie_domain: 'example.com',
cookie_flags: 'SameSite=None;Secure',
})
</script>
The cookie_domain is the critical line. Without it the cookie scopes to the host and the tagging server never sees it.
Step 2: Configure the GA4 client to forward IP and user agent. In server-side GTM open the GA4 client (the one claiming the requests, not the tag) and enable the two relevant settings:
- Use HTTP Header for IP Override →
X-Forwarded-For - Use HTTP Header for User-Agent Override →
User-Agent
Then in the GA4 tag, under More Settings → Fields to Set, add:
ip_override → {{Client IP Address}}
user_agent → {{Client User-Agent}}
The built-in variables {{Client IP Address}} and {{Client User-Agent}} read from the headers your tagging server received. If you are running behind Cloudflare or another proxy, override the IP variable to read CF-Connecting-IP instead, or you will end up with Cloudflare edge IPs on every event.
Step 3: Forward the gclid and other campaign parameters. The GA4 client picks these up from the incoming request automatically, but only if your client-side gtag config actually sent them. Confirm that gtag('config', ...) is firing on every page and that you are not stripping query parameters at a CDN or middleware layer. A common cause of missing campaign data is a Next.js middleware that rewrites away ?gclid= before the page script runs.
Step 4: Verify the hit Google receives. Open the DebugView in GA4 with a test session, then trigger a purchase on the thank-you page. The event should appear in DebugView within a few seconds with:
- A
client_idmatching the value in the browser's_gacookie - A
user_pseudo_idthat is stable across the session - Geolocation matching your test location, not your tagging server's region
- A
traffic_sourcepayload withmedium,source, andcampaignif you arrived with agclidor UTM parameters
If the client_id is changing on every event, your cookie is not being forwarded. Go back to step 1. If geolocation is wrong, the IP override is misconfigured. Go back to step 2.
The Lesson
Server-side tagging only preserves attribution if you tell it which headers and cookies carry the user's identity. Put the tagging server on the same root domain so the _ga cookie travels, forward the real IP and user agent on the GA4 tag, and verify in DebugView before you trust the conversions dashboard. The migration is a footgun for any team that treats it as a drop-in.
If your conversions cratered after a server-side GTM migration and you need somebody to read the headers and fix it without burning a week, that is what I do. See my services. For a related GA4 attribution headache, read GA4 Consent Mode v2 missing conversions.
Conversions dashboard lying since your sGTM cutover? I can fix it.
