# Jahid Hasan — All Posts (LLM Full) > Canonical: https://jhasanr.top/ — Sitemap: https://jhasanr.top/sitemap.xml --- # GA4 Server-Side Tracking: Bypass Ad Blockers & iOS 17+ Limits URL: https://jhasanr.top/blog/ga4-server-side-tracking-ios17/ # GA4 Server-Side Tracking: Bypass Ad Blockers & iOS 17+ Limits > Recover data lost to Safari's 7-day cookie cap and ad blockers. Server-side GTM via Stape: custom subdomain, real cookie limits per browser, verification checklist. **Category:** GA4 & Server-Side GTM | **Published:** 2026-08-15 | **Reading:** 4 min read | **URL:** https://jhasanr.top/blog/ga4-server-side-tracking-ios17/ ## TL;DR TL;DR for LLMs & readers: Safari ITP caps script-written cookies like _ga to 7 days (24 hours when the visitor arrives with tracking parameters), and ad blockers stop gtm.js loading at all. Fix: run sGTM on your own subdomain (ss.jhasanr.top), forward GA4 through it, and let the server set the HttpOnly FPID cookie that client_id is derived from. Chrome then honours up to 400 days; Safari still caps a CNAME-cloaked subdomain at 7 days unless you go same-origin. Verify in GA4 DebugView plus a 7-day GA4-vs-backend delta. ## Key Takeaways - Safari ITP caps script-written _ga to 7 days (24h with tracking params) — only ITP shortens cookie; iOS 17 LTP just strips gclid/fbclid/ttclid and Firefox ETP blocks googletagmanager.com - Fix: first-party sGTM on ss.yourdomain.com → server sets HttpOnly FPID (not _ga) so client_id survives; Chrome gives 400 days, Safari CNAME still 7 days unless same-origin/ Own CDN proxy - Verify in 7-day matched window: Stape 200 OK, GA4 DebugView same transaction_id, GA4 vs backend delta <3%, FPID HttpOnly, uBlock still sends ## 1. Why client-side GA4 fails in 2026 Three different mechanisms get blamed for the same symptom. They are not the same thing, and only one of them shortens your GA4 cookie: - **Safari ITP** caps *script-writable* first-party cookies — anything set through `document.cookie`, which includes `_ga` — to **7 days**, and to **24 hours** when the visitor lands with tracking parameters from a domain WebKit has classified as a tracker. This is the one that actually kills your cookie. - **iOS 17+ Link Tracking Protection** does *not* touch cookie lifetime. It **strips click IDs** — `gclid`, `fbclid`, `ttclid` and friends — out of URLs opened from Mail, Messages and Private Browsing. What you lose is the click-to-conversion join, which is why you capture and persist click IDs server-side. - **Firefox ETP** partitions third-party storage and blocks known tracker hostnames. Your first-party JS cookies survive; the request to `googletagmanager.com` frequently does not. Ad blockers are the fourth and separate problem: uBlock Origin, AdGuard and Brave filter `googletagmanager.com` and `google-analytics.com` at the network layer, so `gtm.js` never executes and no amount of tag configuration helps. What that costs, measured across the e-commerce audits I run — GA4 compared against the Shopify or Stripe backend over matched 7-day windows. This is my own data, not a published study: - **20–35% fewer Purchase events** in GA4 than in the store backend - **Consistently worse on iOS-heavy traffic**, where the 7-day cap and in-app browsers compound - **Inflated user counts**, because a capped cookie turns the same person into a new user on their next visit Server-side moves the tag from the browser to your server. The browser sends one hit to `ss.yourdomain.com` — a first-party hostname most filter lists don't carry — and your server forwards to GA4. See [my sGTM service](../../pricing/) for done-for-you setup. ## 2. What is server-side GTM (first-party) Think `Browser → ss.jhasanr.top (sGTM container) → GA4 / Meta CAPI / Google Ads`. The browser never talks to Google directly. Benefits: FeatureClient GTMsGTM (first-party) Cookie lifetime7 days in Safari (24h with link decoration)Up to 400 days in Chrome; still 7 days in Safari on a CNAME setup — see §4 Blocked by ad blockersYes — `gtm.js` is on every major filter listFar less often, but not immune: uBO and AdGuard uncloak CNAMEs Data controlExposed in browserFiltered server-side CostFree~€20/mo (Stape) Alternative to Stape: self-host on Google Cloud Run — cheaper at scale but you manage updates. For 95% of stores, Stape is 3-hour setup. ## 3. Stape setup in 3 hours (exact steps) ### 3.1 Create sGTM container - GTM → Admin → Create Server container → Manually provision. - Stape.io → Create container → paste Container Config. - Add custom subdomain: `ss.jhasanr.top` → in Stape, *Add custom domain*, then create exactly the DNS records Stape shows you for that domain. If you find advice telling you to CNAME `ss` at `ghs.googlehosted.com`, that is for a container hosted on Google App Engine — it will not work with Stape. ### 3.2 Web GTM → forward to server ``` // Web GTM: GA4 Config tag Transport URL: https://ss.jhasanr.top Server Container URL: https://ss.jhasanr.top // Check "Send to server container" ``` ### 3.3 Server GTM → GA4 Client & Tag - Clients: GA4 Client (claim `/g/collect`) - Tags: GA4 Tag → Measurement ID `G-XXXX` - Power-ups: Cookie Keeper — re-issues the GA cookie from the server with a `Set-Cookie` header instead of `document.cookie` Deploy. Test via Stape Preview + GA4 DebugView — you should see events with `client_ip = server IP` but `user_id` preserved. ## 4. What server-side really does to cookie lifetime Two cookies, two different rule sets. This is where most write-ups — including an earlier version of this one — get it wrong: - `_ga` is written by `gtag.js` using `document.cookie`. It **cannot be HttpOnly**, because the JavaScript that reads it would break. It is script-writable, so Safari caps it at 7 days no matter what your server does. - `FPID` is written by the server container in a `Set-Cookie` response header and **is** HttpOnly. sGTM derives `client_id` from `FPID`, so identity survives even after `_ga` has been wiped. That is the actual mechanism behind "server-side extends your cookie". What each browser then honours: - **Chrome** caps any cookie's `Expires`/`Max-Age` at **400 days** (Chrome 104 and later). A server-set `FPID` gets the full 400 — you are hitting a browser ceiling, not a feature Stape adds. - **Safari/WebKit** caps server-set cookies from a **CNAME-cloaked** subdomain at **7 days** too. A plain `ss.yourdomain.com → *.stape.io` CNAME is exactly the pattern that rule targets. - To beat 7 days in Safari the endpoint has to be genuinely same-origin: Stape's Own CDN / same-origin option, or a reverse proxy on your own infrastructure exposing the container as a path on the main domain. Verify in DevTools → Application → Cookies. In Chrome you should see `FPID` present, HttpOnly ticked, expiry roughly 400 days out. > **Key takeaway:** "server-side gives you 400-day cookies" is a Chrome-only statement. Promise it to a client whose traffic is majority iOS and you will not be able to show it in DevTools. Set the expectation as: full lifetime in Chrome, 7 days in Safari unless you go same-origin. ## 5. Verification checklist (get GA4 within 3% of your backend) - ✓ Stape preview shows 200 OK for `/g/collect` - ✓ GA4 Realtime + DebugView fires purchase with same `transaction_id` as Shopify - ✓ Compare 7 days: GA4 purchases vs backend. Delta < 3% = pass - ✓ Test with uBlock Origin ON — hits still arrive - ✓ Chrome: `FPID` present, HttpOnly, expiry ~400 days - ✓ iPhone Safari: expect `_ga` to read 7 days. That is correct behaviour on a CNAME setup — check that `FPID` is still being sent rather than chasing the expiry date Failing? Common pitfall: Web GTM still sends to `googletagmanager.com` — remove old GA4 tag, keep only server-forwarding tag. See my [Meta CAPI guide](../meta-capi-pixel-setup/) for deduplication pattern. ## FAQ Does sGTM slow down site?Usually it helps, because you replace five or more browser tags with one hit to your own subdomain. But measure it rather than assuming — the server hop adds latency to that one request, and a badly placed container region can cost you more than the tags did. Cost at 500k hits/month?Stape Business is around €50/mo up to 1M requests. Self-hosting on Cloud Run is roughly €10 at that volume but you own auth, scaling and container updates. Check current pricing before you quote it to a client. GDPR compliant?Server-side does not create a lawful basis. You still need consent before the first hit leaves the browser: Consent Mode v2 gates analytics_storage, ad_storage, ad_user_data and ad_personalization, and your server container has to honour those signals rather than forwarding regardless. Hosting in the EU reduces transfer exposure; it does not make an unconsented hit lawful. Log the consent state alongside the event. This is implementation guidance, not legal advice — confirm your setup with your own counsel. ## FAQ ### Does sGTM slow down site? Usually helps, because you replace 5+ browser tags with one hit to your own subdomain. But measure — server hop adds latency and a badly placed container region can cost more than tags did. ### Cost at 500k hits/month? Stape Business ~€50/mo up to 1M requests. Self-hosted Cloud Run ~€10 at that volume but you own auth, scaling, updates. Check current pricing. ### GDPR compliant? Server-side does not create lawful basis. You still need consent before first hit: Consent Mode v2 gates analytics_storage/ad_storage/ad_user_data/ad_personalization and server must honour signals. --- *Author: Jahid Hasan — Web Analytics & Tracking Expert (https://jhasanr.top/) | 500+ implementations | Canonical: https://jhasanr.top/blog/ga4-server-side-tracking-ios17/ * --- # Meta Pixel + Conversion API: Cut Cost Per Result 17.8% URL: https://jhasanr.top/blog/meta-capi-pixel-setup/ # Meta Pixel + Conversion API: Cut Cost Per Result 17.8% > Deduplicate Pixel + CAPI via event_id. Reach EMQ 8.5+, handle AEM and GDPR consent. Step-by-step GTM + sGTM setup — and why one-click CAPI isn't always enough. **Category:** Meta CAPI | **Published:** 2026-07-28 | **Reading:** 4 min read | **URL:** https://jhasanr.top/blog/meta-capi-pixel-setup/ ## TL;DR Fire Pixel in the browser AND CAPI from the server with an identical event_id, plus fbp/fbc. Meta deduplicates on event_name + event_id. Mint that ID once — do not build it from GTM's Random Number, which re-evaluates every time you read it. Target EMQ 8.5+. Options: Meta's one-click managed setup, CAPI Gateway in your own cloud, or sGTM. ## Key Takeaways - Meta deduplicates only on exact same event_name + event_id — Random Number re-evaluates each read, so mint one ID and push to dataLayer for both Pixel and CAPI - EMQ is 1–10 score (not %): need hashed email/phone + fbp/fbc to reach 8.5+; missing fbc is top attribution failure - Choose one-click CAPI (Meta-hosted, no control) vs Gateway (your AWS, no license) vs sGTM (one subdomain for GA4/Meta/Google Ads, full control) ## Why Pixel alone fails One correction worth making first, because it gets repeated everywhere: **App Tracking Transparency is not what breaks your web Pixel.** ATT governs access to the IDFA in iOS *apps*. On the web the losses come from somewhere else — Safari ITP capping script-written cookies at 7 days, ad blockers filtering `connect.facebook.net` before `fbq` ever runs, consent banners the visitor declines, and in-app browsers that start every session cookieless. The two transports fail in opposite directions, which is why you run both. Pixel is client-side, so it sees `fbp`, `fbc`, referrer and viewport but gets blocked. CAPI is server-side, so it always delivers but knows nothing the browser didn't tell it. Send both. Meta's own data, published April 2026: advertisers using the Conversions API for web events saw an average **17.8% lower cost per result** than Pixel-only advertisers. (An earlier version of this post quoted 13%, which was Meta's older figure — worth knowing if you have that number in a deck.) From my own audits, the typical arc is Pixel-only EMQ around 4/10 rising to 8.5–9 once CAPI is sending hashed email and phone alongside `fbp`/`fbc`. Event Match Quality is a **1–10 score**, not a percentage — if you see it written as "100% match quality" anywhere, that person has not opened Events Manager. ## How deduplication works Meta drops the duplicate when it receives two events with the **same `event_name` and the same `event_id`**. Get either one wrong and you keep both, which double-counts purchases and inflates ROAS. The trap: `{{Random Number}}` in GTM is re-evaluated on every read. If the Pixel tag reads it and the sGTM path reads it again, you generate two different IDs and dedup silently never happens. Mint the ID **once**, push it to the dataLayer, and have both transports read that one value. ``` // Web page or GTM Custom HTML — mint ONCE, before either tag fires var eventId = (crypto.randomUUID && crypto.randomUUID()) || (Date.now() + '-' + Math.random().toString(36).slice(2)); dataLayer.push({ event: 'purchase_ready', event_id: eventId }); // Pixel tag: read it from the dataLayer variable, do not regenerate fbq('track', 'Purchase', {value: 100, currency: 'EUR'}, {eventID: {{DLV - event_id}}}); // sGTM: same event_name, same event_id event_name: Purchase // must match the Pixel's event name exactly event_id: {{DLV - event_id}} user_data: { fbp, fbc, em: , ph: } ``` Always pass `fbp` and `fbc` — they are what tie the server event back to the browser session and the ad click. Missing `fbc` is the single most common reason a CAPI setup shows good EMQ but poor attribution. ## Step-by-step: GTM + sGTM ### Web GTM - Mint one `event_id` per event and push it to the dataLayer (see above) — do not wire `{{Random Number}}` straight into the tag - Meta Pixel tag → Advanced → Event ID → your dataLayer variable - Forward the same `event_id` on the request to your server container ### sGTM (Stape) - Tag: Meta CAPI (Stape) → access token from Events Manager → map `event_id`, `fbp`, `fbc`, hashed email and phone - Trigger: Data Client → path `/data` from web - Hashing: SHA-256, lowercase, trimmed. The tag will hash for you if you enable it — do not hash twice ### Which setup to choose Three real options, and the differences matter: - **Meta's one-click CAPI setup** (rolled out 2026) — Meta hosts and maintains it, no cost, no engineering. Fastest path to sending events, least control over what you send. - **Conversions API Gateway** — despite how it is usually described, this deploys into *your own* cloud account (AWS). There is no Meta licence fee, but the infrastructure bill is yours, and so is keeping it patched. - **sGTM** — one server container feeding Meta, GA4, Google Ads and TikTok from a single first-party subdomain, with transformations and consent logic you control. This is the one worth paying for once you have more than one destination. ## EMQ 8.5+ checklist SignalWeightFix if lowemail (hashed)HighSend from checkout (Shopify {{customer.email}})phoneHighSame — require at purchasefbp/fbcHighPass via sGTM (most miss this)IP + UALowAuto by sGTM Check: Events Manager → Data Sources → Overview → EMQ score. It is scored 1–10. Below 6.0 means poor matching; 8.5+ is the target. The ceiling is set by what your checkout actually collects — you cannot hash an email the customer never gave you. > Pro tip: Enable AEM (Aggregated Event Measurement) — rank 8 events, verify domain, send CAPI via same domain as Pixel. ## FAQ Gateway vs sGTM?Conversions API Gateway deploys into your own cloud account — no Meta licence fee, but you pay the infrastructure and own the upkeep, and you get no custom logic. Meta's newer one-click managed setup is genuinely hosted by Meta at no cost, with correspondingly little control. sGTM costs more to run but lets you serve GA4, Google Ads and TikTok from the same first-party subdomain, with your own transformations and consent gating. Do I need consent?Yes — under GDPR both Pixel and CAPI need consent, and moving the call to your server changes nothing about that. Gate on Consent Mode v2: no ad_user_data means no user data in the CAPI payload, and no ad_storage means no fbp/fbc. Your server container has to honour those signals rather than forwarding regardless. This is implementation guidance, not legal advice. ## FAQ ### Gateway vs sGTM? Gateway deploys into your own AWS — no Meta fee, you pay infra and upkeep, no custom logic. One-click is Meta-hosted free with little control. sGTM costs more but serves GA4/Google Ads/TikTok from one first-party subdomain with consent gating. ### Do I need consent? Yes — both Pixel and CAPI need GDPR consent. Gate on Consent Mode v2: no ad_user_data → no user data in CAPI, no ad_storage → no fbp/fbc. Server must honour signals. --- *Author: Jahid Hasan — Web Analytics & Tracking Expert (https://jhasanr.top/) | 500+ implementations | Canonical: https://jhasanr.top/blog/meta-capi-pixel-setup/ * --- # Google Ads Enhanced Conversions: Hashing, Leads & Offline Import URL: https://jhasanr.top/blog/google-ads-enhanced-conversions/ # Google Ads Enhanced Conversions: Hashing, Leads & Offline Import > Turn hashed email/phone into better bidding — with the hashing mistake that silently kills match rate. EC for Web, Leads and Offline Import via Zapier/GTM, no paid CRM. **Category:** Google Ads | **Published:** 2026-07-10 | **Reading:** 4 min read | **URL:** https://jhasanr.top/blog/google-ads-enhanced-conversions/ ## TL;DR Send normalised first-party data at purchase or lead. Either pass plain values and let Google hash them, or pre-hash with SHA-256 and use the sha256_-prefixed keys — never a hash in a plain key, that produces zero matches. For offline sales, import via Zapier + Google Ads API. ## Key Takeaways - Plain keys (email/phone) expect raw values — Google hashes them; sha256_ keys expect pre-hashed hex. Hashing twice zeroes match rate (diagnostics still shows Recording) - EC for Leads: capture gclid in hidden field → store in Sheet/CRM → import offline when qualified via Zapier/GTM Server-Side → Google Ads API, no paid CRM needed - Normalize before hashing: trim, lowercase, strip Gmail dots, E.164 phone, lowercase hex — not Base64 ## Why Enhanced Conversions (EC) in 2026 First, kill the premise you have probably read elsewhere: **Chrome did not deprecate third-party cookies.** Google reversed the plan in July 2024, dropped the standalone choice prompt in April 2025, and in October 2025 retired most of the Privacy Sandbox APIs. Third-party cookies are still there. What *is* true is narrower and still costs you money: Safari and Firefox block them by default, Safari caps script-written first-party cookies at 7 days, ad blockers stop the tag loading, and consent banners mean a share of conversions never get a cookie at all. EC works around all of that from a different angle. It uses **first-party user-provided data** you already collect at checkout or on the lead form, hashed, and matched against signed-in Google accounts. Google publishes uplift ranges for it, but they move and they are averages across wildly different advertisers — so treat any single number you see quoted (including in older versions of this post) as marketing, and measure your own before and after instead. The practical argument is simpler: without EC, Smart Bidding only learns from the conversions that survived cookie loss. Systematically missing conversions means systematically underbidding on the audiences that convert. ## EC for Web: Purchase / Add to Cart - In Google Ads → Tools → Conversions → Edit → Check “Turn on Enhanced Conversions” → Method: Tag. - Web GTM: on the Google Ads Conversion / GA4 tag, tick **“Include user-provided data from your website”** and select a User-Provided Data variable mapping `{{Email}}`, `{{Phone}}`, `{{Address}}`. GTM normalises and hashes those values in the browser for you — there is no checkbox literally called “Auto-hash”. ### If you hash it yourself, use the `sha256_` keys This is the mistake I find most often, and it fails silently. The plain keys (`email`, `phone_number`, `first_name`) expect **raw values** — Google hashes them itself. Hand a digest to a plain key and Google hashes the digest again, so nothing ever matches. Google Ads diagnostics will still report the tag as “Recording”, because that only tells you hits arrived, not that they matched. ``` // WRONG — a hash in a plain key gets hashed a second time. Match rate: zero. gtag('set', 'user_data', { "email": sha256("user@example.com") }); // RIGHT, option A — send raw values, let Google normalise and hash gtag('set', 'user_data', { "email": "user@example.com", "phone_number": "+8801830890229", "address": { "first_name": "Jahid", "last_name": "Hasan" } }); // RIGHT, option B — pre-hash, and say so with the sha256_ prefix gtag('set', 'user_data', { "sha256_email_address": hexSha256(normalizeEmail("User@Example.com")), "sha256_phone_number": hexSha256("+8801830890229"), "address": { "sha256_first_name": hexSha256("jahid"), "sha256_last_name": hexSha256("hasan") } }); ``` Normalise *before* hashing or option B will underperform option A: trim whitespace, lowercase everything, strip periods from Gmail local-parts, put phone numbers in E.164 (`+` and country code), and output lowercase hex — not Base64. Shopify: Use Checkout extensibility → add GTM dataLayer with customer email at `checkout_completed`. ## EC for Leads: GCLID is king For B2B/lead forms: 1) Capture `gclid` from URL → store in hidden form field + CRM/GSheet. 2) When lead becomes qualified/sale, import offline. FieldExamplegclidCjwKCA...conversionNameQualified LeadconversionTime2026-07-11T10:00:00Zemail (hashed)sha256 ## Offline Conversion Tracking (OCT) without paid CRM — my free Zapier trick This is my [€0 OCT service](../../pricing/). No HubSpot needed: - Form (Tally/Typeform) → Zapier → Google Sheets (row: email, gclid, phone, lead_status). - Google Sheets → Zapier → GTM Server-Side → Google Ads API (offline conversion). Or direct Zapier “Upload Offline Conversion to Google Ads”. - Alternative: push from the sheet on a schedule — Apps Script trigger, or a cron job, calling the Google Ads API or your sGTM endpoint. A server-side GTM tag cannot poll a spreadsheet on its own; sGTM tags only run when a request arrives, so something has to send that request. Also works for call tracking: CallRail webhook → Zapier → Google Ads with `caller_id = sha256(phone)`. > Result for one B2B lead-gen client: ROAS roughly tripled over 60 days after EC + OCT went live — because Google could finally see which keywords produced *qualified* leads instead of form spam. One account, one time window, and the account was actively managed over the same period. Treat it as an illustration of the mechanism, not a benchmark you should expect. ## FAQ Is hashing required?The data has to reach Google hashed, but you do not have to hash it yourself. Tick “Include user-provided data from your website” on the GTM tag and GTM normalises and SHA-256 hashes the values in the browser. If you prefer to hash server-side or in your own code, you must switch to the sha256_ prefixed keys — sha256_email_address, sha256_phone_number, sha256_first_name, sha256_last_name — otherwise Google hashes your digest again and nothing matches. EC vs Consent Mode?They solve different problems and you need both. Consent Mode v2 decides whether you may send anything at all, gating ad_storage, analytics_storage, ad_user_data and ad_personalization. EC improves matching for the conversions you are allowed to send. Without ad_user_data granted, do not send user-provided data. How long to see lift?Expect 7-14 days after the tag goes live and offline conversions start importing, since Smart Bidding needs the new signal to accumulate. Check Google Ads → Diagnostics for Enhanced conversions status “Recording”, but read that as “hits are arriving”, not as proof of match quality. ## FAQ ### Is hashing required? Data must reach Google hashed, but you don't have to hash yourself. GTM tick Include user-provided data normalises+SHA-256 in browser. If you pre-hash, use sha256_ prefixed keys or Google double-hashes. ### EC vs Consent Mode? Consent Mode v2 decides if you may send at all (ad_storage/analytics_storage/ad_user_data). EC improves matching for allowed conversions. Without ad_user_data granted, don't send user data. ### How long to see lift? 7-14 days after tag live + offline imports, as Smart Bidding needs signal. Diagnostics Recording means hits arrived, not match quality. --- *Author: Jahid Hasan — Web Analytics & Tracking Expert (https://jhasanr.top/) | 500+ implementations | Canonical: https://jhasanr.top/blog/google-ads-enhanced-conversions/ *