Ad blockers and Safari's privacy engine are silently erasing up to 40% of your analytics data. Standard GA4 never sees these visitors — so your ROAS is wrong, your audiences are undersized, and your bidding algorithms are training on bad numbers. This guide explains exactly what gets blocked, why it happens, and how to fix it using a server-side GTM architecture on WordPress.
Ad blockers don't just hide ads. They intercept outbound HTTP requests at the network layer — before the browser even sends them. When a visitor lands on your site, your GA4 tag tries to fire a request to www.google-analytics.com/g/collect. An ad blocker sees that URL, matches it against a filter list, and kills the request before it leaves the browser.
The major blockers each work a bit differently. Understanding the difference matters for how you fix the problem.
/g/collect endpoint (GA4 data)gtag.js and analytics.jswww.googletagmanager.comThe key insight: all of these tools allow requests that look genuinely first-party. That is the mechanism server-side tracking exploits. More on that in section 04.
Safari's Intelligent Tracking Prevention (ITP) runs without any extension. It's built into every iPhone, iPad, and Mac. It doesn't block GA4 outright — it's more destructive than that. It lets the script fire, then deletes the cookies shortly after.
Here's the specific rule that breaks WooCommerce attribution: any first-party cookie set via JavaScript (document.cookie) gets a hard 7-day expiration. Your _ga cookie — the one that ties a user's session history together — gets wiped after one week of inactivity.
Apple's Safari 26 (shipping mid-2025) adds Advanced Fingerprinting Protection. It restricts script access to canvas and audio APIs, and blocks marketing scripts from using local storage to create persistent identifiers. Client-side tracking on Safari is getting harder — not easier — over time.
This is not a minor data quality issue. Missing 15–40% of your traffic distorts every downstream decision: which channels get budget, how Google's Smart Bidding allocates spend, and how confidently you can trust your attribution model.
_ga cookie is deleted before a purchase, the transaction gets credited to Direct/None. The paid channel that drove the visit loses credit entirely.GA4 includes Behavioral Modeling for Consent Mode. It estimates the behavior of users who blocked tracking, using machine learning trained on your consented users. This sounds helpful — but it has two hard limits.
First, it only activates if you consistently have at least 1,000 events per day with analytics storage denied and 1,000 daily users with analytics storage granted. Most mid-market WooCommerce stores don't hit this threshold reliably. Second, modeled data is a statistical estimate — it can't produce the individual-level attribution data needed for accurate ROAS, CRM identity resolution, or financial reconciliation.
Modeling is a fallback, not a fix.
Running the same problem on Meta? Facebook's Pixel is blocked by the same tools. The fix is Facebook's Conversions API — same architecture, different platform.
Meta CAPI Fix →The root problem is simple: ad blockers block third-party requests. Your browser sending data to google-analytics.com is a third-party request. The fix is equally simple in concept: make the tracking request look first-party.
Server-side GTM (sGTM) does this by putting a relay server — your server, on your domain — between the browser and Google's servers.
Your sGTM container must run on a subdomain you own — like metrics.yourstore.com. This does two things. It makes the browser request first-party (same registrable domain), which most blockers allow. And it lets your server write cookies via HTTP headers instead of JavaScript, which extends cookie lifespan past Safari's 7-day JavaScript limit.
Firefox provides a browser.dns API that lets advanced blockers resolve DNS records in real time. If your subdomain's IP address resolves to a known Google Cloud Run or Cloudflare IP block, Firefox can detect the CNAME alias and block the request anyway. This is why true same-origin path routing (e.g., yourstore.com/metrics) is more resilient than a subdomain — but also significantly harder to configure.
Your sGTM container needs a server to run on. Two options dominate for WordPress and WooCommerce stores. The right choice depends on your team's DevOps capacity and traffic volume.
For most WooCommerce stores — especially those without dedicated DevOps capacity — Stape is the faster and more cost-predictable choice. Google Cloud Run makes sense when you already have GCP infrastructure or need to handle enterprise-scale traffic with custom configuration.
A full comparison of costs and architecture tradeoffs is covered in the Stape vs Google Cloud for GTM guide →
Setting up server-side GTM on WordPress involves four distinct layers: DNS, the GTM containers, your WordPress snippet, and your WooCommerce dataLayer. Each layer must work correctly for the others to function.
Create a server container in GTM, then deploy it to Stape or GCP. Add a subdomain to your DNS — typically an A record or CNAME pointing metrics.yourdomain.com to your container's IP or hostname. SSL must be fully provisioned before any tracking can work. Missing SSL causes hard ERR_CONNECTION_CLOSED failures.
Your standard GTM snippet loads the library from www.googletagmanager.com/gtm.js. Change the src to metrics.yourdomain.com/gtm.js. This makes even the initial script load first-party — so the library itself can't be blocked by hostname-based rules. Most stores manage this via GTM4WP plugin or hardcoded in header.php.
Inside the web GTM container, open your GA4 Configuration Tag. Add the server_container_url parameter and set it to your subdomain. This tells gtag.js to stop sending data to Google directly and route everything through your relay server instead.
The server container needs a GA4 Client to recognize incoming requests. This client listens on the /g/collect path, parses the payload, and forwards it to Google Analytics. If this client isn't configured and published, every incoming request hits a "No Client Claimed the Request" error and is silently dropped.
Plugins like Google Site Kit and MonsterInsights inject their own GA4 configuration tags independently of GTM. If one of these is active, it will send data directly to google-analytics.com — bypassing your server container entirely. This creates duplicate events and leaves that data exposed to ad blockers. One analytics path only.
Cloudflare's Rocket Loader feature rewrites <script> tags to type="text/rocketscript" to defer JavaScript execution. This breaks GTM initialization. The web container snippet never runs properly, which means no data reaches your server container — even though everything else is configured correctly. You must create a Cloudflare Page Rule to bypass Rocket Loader for your GTM script path.
<!-- Standard GTM snippet — BEFORE (loads from Google's servers) --> <script> (function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX'); // loads from: www.googletagmanager.com/gtm.js (blockable) </script><!-- Server-side GTM snippet — AFTER (loads from YOUR subdomain) --> <script> (function(w,d,s,l,i){
w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'}); var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
j.async=true; // ↓ CHANGED: points to your subdomain, not Google j.src='https://metrics.yourstore.com/gtm.js?id='+i+dl;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX'); </script>Need this set up correctly? We handle the full sGTM build — DNS, container config, WooCommerce dataLayer, and ITP cookie restoration — as part of our tracking integrity service.
View Tracking Services →WP Rocket, Varnish, and Cloudflare page caching store static HTML snapshots of your pages. If your WooCommerce theme or a plugin generates dataLayer variables dynamically via PHP — like cart contents or user status — a cached page serves the stale PHP output. The dataLayer arrives empty or with wrong values. Your server container receives garbage data and forwards garbage to GA4.
The fix: configure your caching layer to exclude dynamic variables from caching, or use cookie-based exclusion rules for logged-in users and active cart sessions. Most WooCommerce stores need caching bypassed for any page with dynamic cart state.
Logged-in customers have a WordPress User ID. You can pass this as the GA4 user_id parameter, which enables cross-device attribution and long-term behavioral analysis. Guest checkouts don't have this — until after they complete the purchase.
The solution is session-based capture. When a visitor arrives from a tracked ad, immediately store their _ga client ID and any URL click parameters in a PHP session variable. When the guest submits their billing email during checkout, hash it (SHA-256) and use it as a stable fallback identifier. This bridges the anonymity gap and allows the server to stitch together the full funnel even without a WordPress user account.
A common misconception: moving tracking to the server removes GDPR obligations. It doesn't. Whether data is collected in a browser or on a server, collecting PII or setting persistent tracking identifiers requires a lawful basis — which, for advertising, means explicit consent in the EU.
What server-side does improve is your ability to enforce that consent. With client-side scripts, a third-party vendor (Meta, Google) loads directly in the browser and collects whatever ambient data it wants. With sGTM, all data flows through your server first. You can configure Transformation rules to strip IP addresses, redact sensitive parameters, and enforce SHA-256 hashing before anything leaves your infrastructure. You control what third parties receive.
Your CMP (Consent Management Platform) must gate both the browser pixel and the server-side trigger independently. If a user declines, the server container must not fire — not just the browser pixel.
When you first compare your new server-side GA4 property to your legacy client-side one, you'll almost always see the server-side property reporting fewer sessions. This is confusing, because the whole point was to capture more data. The discrepancy is real — but it's not a failure. It's almost always a configuration gap, not an infrastructure gap.
The most common cause: the web container is not passing URL query parameters — gclid, fbclid, UTM parameters — to the server container. Without these, every session arrives at the server container without attribution context. GA4 still records the session, but the source/medium defaults to (not set) or direct/none. Reconcile by auditing what the web container sends in GTM Preview mode and ensuring all attribution parameters are explicitly packaged into the outbound payload.
metrics.yourstore.com returning HTTP 200 means data is reaching your server. Requests to google-analytics.com returning blocked:other confirm the old client-side path is blocked and no longer active.client_id or session parameters here explains most attribution failures./g/collect requests going to metrics.yourstore.com, not google-analytics.comclient_id, session_id, and UTM parameters all present in incoming payloadshttps://metrics.yourdomain.com/healthz)googletagmanager.comserver_container_url set to custom subdomain/g/collect)google-analytics.com from browserReady to stop losing data to ad blockers? We audit your current setup, identify exactly what's being dropped, and implement the full server-side architecture — GTM container, subdomain proxy, caching exclusions, and consent mode.
Get in Touch →