Ad blockers
What ad blockers look at in an analytics request, why a same-origin proxy gets past most of them, and what they can still stop.
Visitors with an ad blocker or strict tracking protection can stop events before they reach the API. This page explains what those tools look at, what the SDK's requests look like to them, and what you lose with and without the /_ra proxy.
What blockers look at
Brave Shields, uBlock Origin and Firefox's strict tracking protection compare each request's URL with filter lists such as EasyPrivacy. Those lists name known tracker domains, block third-party hosts that serve tracking, and match words in paths such as analytics, track, collect or pixel.
The SDK is installed from npm and bundled into your own JavaScript, so there is no separate tracker script from another host for a list to name. What a blocker can see is the request that carries the events.
What the SDK's request looks like
Each batch is a POST to the client's endpoint, /_ra by default, with the public key as a key query parameter and a text/plain JSON body. The client sets no custom headers, so the browser sends the request without a CORS preflight.
| Setup | Request goes to | Seen by a blocker as |
|---|---|---|
| Proxy | /_ra on your own domain | A first-party request to a path with no tracking words |
| Direct | https://api.example.com/v2/events | A request to another host, with events in the path |
A direct setup is the one filter lists are built to catch: a separate host that receives data from many sites. The proxy moves the request onto your own domain, where a list would need a rule for your site in particular.
What the proxy changes
The proxy from createProxy runs on your server, receives the batch on the same origin and forwards it to POST /v2/events. It adds the project's secret key and the visitor's IP and user agent, forwards the page's Origin and the admin session cookie, and returns the API's answer unchanged. See Proxy.
This has effects beyond blockers:
- The public key is no longer checked against the project's allowed origins, because the API trusts the secret key.
- The per-IP-hash rate limit of 100 requests a minute does not apply, since it covers public key callers only.
- The proxy refuses methods other than
POST, requests the browser marks as cross-site, and bodies over 60 KB, so other sites cannot send events through it.
Pick any path you like, as long as the client's endpoint matches it. The Astro and SvelteKit setups use /ra; Astro does not route a file whose name starts with _.
What can still be blocked
The proxy avoids the rules filter lists already have. It does not make the request impossible to block.
- A list can add a rule for your domain and path, or a visitor can add one by hand.
- A blocker that stops all script on the page stops the SDK with it.
- Opt-out, consent, Do Not Track and Global Privacy Control are checked in the browser before anything is queued, and the proxy does not change that. A visitor who opted out sends nothing, through any path.
When the request is blocked, the browser reports a network error. The client retries after 1, 4 and 16 seconds, then saves the events in localStorage and tries again on the next page load, where the blocker stops them again. At most 100 saved events are kept, so a blocked visitor never builds up more than that. See Duplicates and retries.
Measuring what is blocked
createPageCounter from /proxy runs in your server middleware and records each HTML page load as a page_request event. It counts a GET whose Sec-Fetch-Dest is document, or that accepts text/html when that header is missing, and skips prefetches. A blocker in the browser cannot stop a request the server makes, so these events arrive for every page load.
The gap between page_request events and pageviews estimates how many page loads the client did not report. Three things affect that estimate:
- Client-side navigations in a single-page app send pageviews without a page request, so compare page requests with the first pageview of each page load, not with all pageviews.
- Crawlers load HTML too. The page counter forwards the visitor's user agent, so known crawlers score as bots and the default
traffic=humanfilter leaves them out. - Visitors who opted out, did not give required consent, or have Do Not Track or Global Privacy Control on also send no pageviews, and count as a gap the same way a blocked visitor does.