11. First-Party Domains & Cookies
transport_url, what genuinely counts as first-party (A records vs CNAME cloaking), FPID server-managed cookies, serving gtm.js through your own domain, custom loaders, and why proxying the tracking subdomain breaks everything.
Chapter 10's architecture runs anywhere — Google Cloud, a managed host, a VPS. But almost all of its value depends on a single decision that looks like a deployment detail and isn't: what hostname the browser talks to. The two prizes of server-side tagging — unblockable requests and durable cookies — are both properties of genuine first-partyness, and this chapter is about earning the word "genuine".
transport_url: re-pointing the browser
The browser side of the migration is almost embarrassingly small. The GA4 web tag (or Google tag) has a transport/server-container setting; set it to your tagging server's origin:
before: POST https://www.google-analytics.com/g/collect?v=2&en=purchase…
└── third-party, blockable, no cookie powers
after: transport_url = https://pulse.shop.example
POST https://pulse.shop.example/g/collect?v=2&en=purchase…
└── first-party: your DNS, your server, your responseSame tag, same data layer, same events — different destination. Everything from Part 2 carries over untouched, which is the practical meaning of Chapter 10's "the GA4 web tag became a universal transport": you don't re-instrument the site, you re-point it.
Note what this does not re-point: other vendors' web pixels. A Meta pixel
in the browser still calls facebook.com/tr — transport_url is a GA4-tag
setting, not a site-wide switch. The standard endgame is that the GA4 stream
into your server becomes the only stream that needs to leave the browser,
and Meta/TikTok/LinkedIn are fed server-side from it (Chapters 13–14, with
the browser pixel kept or dropped per platform dedup strategy).
What actually counts as first-party
First-party means same registrable domain (eTLD+1): pulse.shop.example
is first-party to shop.example. A subdomain is the standard choice. But
the DNS record behind the subdomain decides whether browsers agree with
your labeling:
how pulse.shop.example resolves verdict
──────────────────────────────── ──────────────────────────────────
A/AAAA → infrastructure serving genuinely first-party — full cookie
YOUR container (your VPS, your powers, no ITP penalty. The gold
load balancer, dedicated IPs) standard.
CNAME → a vendor's shared domain "CNAME cloaking" — Safari resolves
(tracker.vendor.example) the chain (Ch. 3), sees cross-domain
delegation, and caps even HTTP-set
cookies at 7 days. Also: the vendor
terminates TLS *as you* — a trust
decision hiding in a DNS record.
CNAME → dedicated infra operated the grey zone. Works, common among
for you (your own LB hostname, managed hosts — but Safari's rule
per-client dedicated endpoints) keys on the cross-domain *DNS shape*,
so dedicated IPs + A records remain
the maximal-durability setup; serious
providers offer them for exactly this
reason.The takeaway is blunt: if Safari durability is the goal, the chain should end at an A/AAAA record to infrastructure dedicated to you. This is also why "first-party" claims from vendors deserve one question: what does the DNS look like?
Server-managed cookies: FPID
Now the response path from Chapter 10 ("the claiming client owns the HTTP
response") pays out. The GA4 client has a server-managed client ID option:
instead of trusting the JS-set _ga cookie as the source of identity, the
server sets its own:
Set-Cookie: FPID=…; Domain=.shop.example; Max-Age=34560000;
Secure; HttpOnly; SameSite=Lax; Path=/Walk the attributes, because each one is doing real work:
- HTTP-set, genuine first-party — outside ITP's JS-cookie caps. Two years means two years (well — see Max-Age below).
HttpOnly— page JavaScript cannot read it. That's not a limitation, it's the feature twice over: scripts can't kill it, and a compromised third-party script can't steal it.Max-Age=34560000— 400 days, and that exact number isn't arbitrary: it's the maximum cookie lifetime Chrome enforces (per the updated cookie spec). Ask for more and Chrome clamps to 400 days anyway, so 400 is the honest ceiling. (It's also why our own stack pins precisely this value — set the cap, get the cap.)- The GA4 client handles the translation transparently: incoming requests
carry
FPID(the browser attaches it automatically — same domain), outgoing Measurement Protocol hits carry a client ID derived from it. GA4 reports don't know or care.
Mechanically, identity now lives in a place where the browser's anti-tracking machinery has agreed not to look — because HTTP-set first-party cookies are what login sessions are made of, and that's a hostage browsers can't shoot.
Limits, stated plainly: still one browser on one device (no cross-device magic), still gone if the user clears site data, still subject to consent (a cookie is a cookie — Chapter 17). Durable ≠ omniscient.
Serving the container itself first-party
One leak remains: the page still loads gtm.js from
googletagmanager.com — a hostname firmly on every blocklist (Chapter 3),
and if the container never loads, there's nothing to transport. The server
container closes this too: stock clients can serve the Google scripts
through your domain — the snippet's src becomes
https://pulse.shop.example/gtm.js?id=GTM-XXXXXXXand your tagging server fetches the script from Google, then serves it as a
first-party asset — the GTM Web Container client handles this (and since
sGTM v3.2.0 it's the one place all Google JS serving lives, gtag.js
included). Hostname problem solved.
Custom loaders: the second half of the trick
Blocklists don't only match hostnames — they match paths. /gtm.js and
/g/collect are themselves on filter lists, so a first-party domain with
stock paths still loses requests to path rules. The countermeasure is the
custom loader: serve the loader script and collection endpoints under
non-standard, per-site paths —
https://pulse.shop.example/cdn/a7f3k2.js ← the loader
https://pulse.shop.example/a7f3k2/data ← the event endpoint— so neither hostname nor path matches a list. This is precisely what Stape's "Custom Loader" power-up sells (Chapter 16), and in our own stack it's pixel-agent's job: each client's edge serves the web container under client-specific stealth paths, so the loader survives list-based blocking.
Honesty paragraph, as always: this is an arms race. Filter lists update; heuristic blockers profile script behavior, not just URLs. First-party serving plus custom loading recovers a large, real share of blocked measurement — and nothing in this space is permanent. Build for graceful degradation, not for "100% capture", and re-audit quarterly.
Google's own flavor: Tag Gateway
Worth knowing the official cousin: Google tag gateway for advertisers (generally available since May 2025) gets first-party serving without an sGTM container — your CDN (Cloudflare at launch; Akamai, Fastly, CloudFront since) or a GCP load balancer intercepts Google-tag requests on your own domain and relays them to Google. It's the serving benefit productized for Google tags specifically — useful on its own, but note what it doesn't give you: no checkpoint, no server-set cookies under your control, no multi-vendor fan-out. A complement to a server container, not a replacement.
The proxy trap: why Cloudflare breaks it
The most common self-inflicted wound in production sGTM, and our support inbox's old friend: someone "protects" the tracking subdomain behind a proxying CDN (Cloudflare orange-cloud), and three things degrade at once:
- Preview/debug dies. The console's server preview is a live stream from the preview server (Chapter 10) over long-held connections. Buffering proxies hold and batch those responses — the debug UI hangs or shows nothing. If server preview "just spins", check the DNS cloud color before anything else.
- The client IP disappears. Your server now sees the proxy's IP unless it's configured to trust forwarding headers. Geo reporting and platform match quality (IP is a match signal — Chapter 13) quietly degrade.
- Caching/bot rules eat events. A CDN that caches
200s or "challenges" suspicious POSTs is making per-request decisions about your measurement pipeline.
Hence the rule (it's Critical Rule #5 in our own runbook): tracking subdomains are DNS-only. No proxy, no cache, no bot-management — TLS and a straight line to the tagging server. A CDN in front of your site is fine; in front of your collector, it's a man in the middle you hired yourself.
The healthy-setup checklist
- Tracking host is a subdomain of the site's own registrable domain
- DNS terminates at an A/AAAA record to dedicated infrastructure (or you've consciously accepted the CNAME grey zone)
- No proxying/caching/bot-layer on the tracking host — DNS-only
-
transport_urlset on the GA4 web tag; events arriving at/g/collect(verify in server preview) - Container scripts served first-party — ideally via custom loader paths
- Server-managed client ID (FPID) enabled;
Set-Cookievisible in the response withHttpOnly+Max-Age=34560000 - Client IP and User-Agent correctly forwarded to the container (check event data in preview — geo should be the visitor, not the datacenter)
With the pipe built and the cookies durable, it's time to watch one event travel the whole road and learn to debug every hop: Chapter 12 — GA4 End to End.