To serve analytics from your own domain, add two routes to your site: one that fetches the vendor’s script, and one that forwards events to the vendor’s collection endpoint along with each visitor’s IP address and user agent. Then point the tracker at those two paths. Blocklists such as EasyPrivacy match analytics mostly by the vendor’s domain, so a neutral path on your own domain is not caught by those rules.
A proxy changes where a request goes, not what you may collect. Global Privacy Control, Do Not Track and consent rules still apply.
Below: what the lists block (we ran 39 analytics URLs through them on November 3, 2026), how to measure your own gap, tested setups for Next.js, Cloudflare Workers, nginx and Vercel, and the pitfalls that quietly break counts.
Why do ad blockers block analytics scripts?
Most ad blockers block analytics because they subscribe to EasyPrivacy, the tracking list maintained in the EasyList repository. uBlock Origin enables it by default, next to EasyList and its own lists (uBlock Origin’s assets.json). The version we downloaded on November 3, 2026 (202611031204) runs to 57,235 lines.
EasyPrivacy matches analytics in two ways: by domain and by path. These rules are in that version:
||plausible.io^$third-party
||cdn.usefathom.com^
||simpleanalyticscdn.com^$third-party
/_vercel/insights/script.js
/umami/script.js
/cdn-cgi/rum?||plausible.io^ matches plausible.io and every subdomain, and $third-party limits it to pages on other sites. A rule that starts with / matches that path on any domain, including yours. That is why first-party is no guarantee: Vercel’s analytics script and Cloudflare’s /cdn-cgi/rum endpoint are served from your own domain and still blocked.
The list also catches individual proxies once someone reports them. easyprivacy_specific.txt contains ||ft.com/ingest? and ||mixpanel-proxy.ted.com^, rules written for two particular sites’ tracking routes.
Privacy-focused tools are not exempt. Plausible’s docs say “some blocklist maintainers block all analytics regardless of privacy practices”, and Fathom ended custom domains in March 2023 because they were “blocked by uBlock and a few others”.
Which analytics requests do ad blockers block?
Every analytics vendor domain we tested is blocked by EasyPrivacy, by uBlock Origin’s default lists and by AdGuard Tracking Protection, with one exception covered below. First-party paths are blocked only when a rule names them, and several vendors’ default paths are named.
| Request from a page on www.example.com | EasyPrivacy | uBlock Origin defaults | AdGuard Tracking Protection |
|---|---|---|---|
plausible.io/js/pa-XXXXX.js |
Blocked | Blocked | Blocked |
cdn.usefathom.com/script.js, cloud.umami.is/script.js |
Blocked | Blocked | Blocked |
us-assets.i.posthog.com/static/array.js |
Blocked | Blocked | Blocked |
static.cloudflareinsights.com/beacon.min.js |
Blocked | Blocked | Blocked |
api.invisit.dev/invisit.js |
Passes | Passes | Passes |
api.invisit.dev/v1/events, sent as a beacon |
Blocked | Blocked | Passes |
Your domain: /_vercel/insights/script.js, /cdn-cgi/rum |
Blocked | Blocked | Blocked |
Your domain: /matomo.js, /plausible.js |
Blocked | Blocked | Blocked |
Your domain: /umami/script.js, /stats/api/event |
Blocked | Blocked | Passes |
Your domain: /js/script.js, /api/event |
Passes | Passes | Passes |
Your domain: /ingest/static/array.js, /ingest/e/ |
Passes | Passes | Passes |
Your domain: /_i/invisit.js, /_i/v1/events |
Passes | Passes | Passes |
We loaded the lists into Ghostery’s open-source adblocker engine (@ghostery/adblocker 2.20.3) and matched scripts as scripts and event endpoints as both fetch and beacon. “uBlock Origin defaults” means the nine lists it enables out of the box. Real extensions do more than an engine, such as CNAME uncloaking, covered under pitfalls.
invisit, the tool we build, is the exception: no rule names it yet, because it is new. One generic rule already hits every vendor, ours included: EasyPrivacy’s *$ping,third-party blocks any cross-site sendBeacon. invisit’s tracker sends a beacon when the page is hidden or closed, carrying engagement time, web vitals and anything still queued, so without a proxy those beacons are blocked. Through a first-party proxy they are same-site and pass.
EasyPrivacy’s header says it expires after 4 days, so treat the table as a snapshot.
How much traffic do ad blockers hide from analytics?
We found no independent, current measurement to cite, only vendor ranges. Plausible says sites without a proxy miss “typically between 5% and 25% depending on your audience” (Plausible, updated October 31, 2026). PostHog says a reverse proxy “typically increases event capture by 10-30%” (PostHog). Both come from vendors that sell proxying, and your number depends on your audience and their browsers.
How to measure your own analytics gap
Compare what your server served with what your analytics counted, for the same pages on the same day.
- Pick one full day and pages that load in full, such as landing pages; single-page app navigations never reach your server.
- Count human page loads on the server. From an nginx access log in the default combined format:
awk -F'"' '$1 ~ /\[02\/Nov\/2026/ && $2 ~ /^GET \/[^ .]* HTTP/ && $3 ~ /^ 200 / && tolower($6) !~ /bot|crawl|spider|curl|python|headless/' /var/log/nginx/access.log | wc -l- Take page views for the same day, in the same time zone, from your analytics.
- The share you are missing is 1 minus analytics page views divided by server page loads.
- After you switch to a proxy, repeat on the same weekday and compare.
The one-liner keeps successful GETs for paths without a file extension and drops self-declared bots. Bots posing as browsers inflate the server count, and pages served from a CDN cache never reach your origin, so use CDN logs if it caches HTML.
Does proxying analytics get around consent or privacy settings?
No, and it should not. A proxy changes which host receives a request. It does not change what the tracker collects or whom it is allowed to count.
- Global Privacy Control. Browsers send it as the
Sec-GPC: 1header andnavigator.globalPrivacyControl. California’s Attorney General lists the right to opt out of sale or sharing “including via the GPC” (CCPA). Your proxy must forwardSec-GPCfor vendors that check it on the server. - Do Not Track. A W3C proposal closed as a Working Group Note on January 17, 2019. Honoring it is voluntary, but tools that do must still receive the
DNTheader. - Consent. Article 5(3) of the ePrivacy Directive concerns storing or reading information on the visitor’s device, whichever domain the request goes to. A setup that needed consent still needs it; our cookie banner guide covers the EU, UK and US rules.
Some visitors install a blocker precisely to avoid analytics. A proxy overrides a blocklist’s guess, not a visitor’s stated choice, so the signals above must keep working. The goal is an accurate count of what your site is allowed to count.
How does a first-party analytics proxy work?
A first-party analytics proxy is two routes on your domain:
- Script route. A GET that returns the vendor’s script. It can be cached.
- Event route. Usually a POST that forwards the body, the user agent, the visitor’s IP address and the
Sec-GPCandDNTheaders to the vendor, drops your cookies, and is never cached.
The examples below proxy Plausible, whose endpoint is strict about the visitor’s address and has a debug header to check it. Replace pa-XXXXX.js with your site’s script from Plausible’s settings, and replace /_hx with a name of your own: a path copied by many sites is exactly what earns a generic rule. Then load the tracker from your paths:
<script async src="/_hx/script.js"></script>
<script>
;((window.plausible =
window.plausible ||
function () {
;(plausible.q = plausible.q || []).push(arguments)
}),
(plausible.init =
plausible.init ||
function (i) {
plausible.o = i || {}
}))
plausible.init({ endpoint: '/_hx/event' })
</script>Next.js rewrites
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{ source: '/_hx/script.js', destination: 'https://plausible.io/js/pa-XXXXX.js' },
{ source: '/_hx/event', destination: 'https://plausible.io/api/event' },
]
},
}
export default nextConfigWe ran this on Next.js 16.4.4 with next start. It forwarded the visitor’s cookies to the vendor, passed the vendor’s Set-Cookie back, and set X-Forwarded-For to the connecting address only when the request had none; an existing one went through unchanged. Behind a load balancer that overwrites the header, that is correct. On a server exposed directly, a visitor can write any address there. On Vercel, the platform sets it.
If your app has a proxy.ts or middleware.ts, exclude the proxy path from its matcher: PostHog’s Next.js guide warns that a catch-all matcher can swallow event routes while the script still loads. PostHog’s paths end in a slash, so that guide also sets skipTrailingSlashRedirect: true.
Cloudflare Worker
const SCRIPT = 'https://plausible.io/js/pa-XXXXX.js'
const EVENT = 'https://plausible.io/api/event'
export default {
async fetch(request) {
const { pathname } = new URL(request.url)
if (pathname === '/_hx/script.js') {
return fetch(SCRIPT, { cf: { cacheTtl: 3600 } })
}
if (pathname === '/_hx/event') {
const upstream = new Request(EVENT, request)
upstream.headers.delete('Cookie')
upstream.headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP') ?? '')
return fetch(upstream)
}
return fetch(request)
},
} satisfies ExportedHandlerAdd a route such as *example.com/_hx/* so the Worker sees only those requests. We ran it locally with wrangler 4.157.0: the cookie was dropped, and the user agent, body, Sec-GPC and DNT arrived with the visitor’s address. The script is cached for an hour at Cloudflare’s edge.
nginx
location = /_hx/script.js {
proxy_pass https://plausible.io/js/pa-XXXXX.js;
proxy_set_header Host plausible.io;
proxy_ssl_server_name on;
}
location = /_hx/event {
proxy_pass https://plausible.io/api/event;
proxy_set_header Host plausible.io;
proxy_ssl_server_name on;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Cookie "";
}We tested this on nginx 1.31.8 against Plausible itself with its X-Debug-Request: true header, which returns the IP address Plausible would count and records nothing. It returned the address nginx forwarded. Keep proxy_ssl_server_name on: it is off by default, and without it our proxy to Cloudflare-hosted cloud.umami.is failed with a 502.
$remote_addr is the visitor only when nginx faces the internet; behind a CDN or load balancer, restore it first with set_real_ip_from and real_ip_header. Plausible’s nginx guide uses $proxy_add_x_forwarded_for, which appends to whatever the visitor sent, while Plausible reads the first valid address in the list.
Vercel rewrites
{
"rewrites": [
{ "source": "/_hx/script.js", "destination": "https://plausible.io/js/pa-XXXXX.js" },
{ "source": "/_hx/event", "destination": "https://plausible.io/api/event" }
]
}This works for any framework deployed on Vercel. Vercel’s rewrites docs say external rewrites forward the visitor’s public IP in x-forwarded-for and x-real-ip, and that projects created on or after April 6, 2026 cache external responses according to the upstream Cache-Control. We validated and compiled this file with @vercel/routing-utils 6.6.1, the package Vercel uses for routes, but did not deploy it.
What does each analytics vendor document for proxying?
As of November 3, 2026, every tool below except Fathom documents a way to serve tracking from your domain. PostHog hosts one for free; Plausible, Matomo and Simple Analytics do on Enterprise.
| Tool | What it documents | Source |
|---|---|---|
| Plausible | Guides for Cloudflare, Vercel, Netlify and nginx, a WordPress toggle, a managed proxy on Enterprise | Proxy guides |
| Umami | Proxy the script and /api/send; self-hosters can rename both with TRACKER_SCRIPT_NAME and COLLECT_API_ENDPOINT |
Docs |
| PostHog | A managed reverse proxy on your subdomain, free on PostHog Cloud and routed through Cloudflare, or your own | Docs |
| Matomo | tracker-proxy, a PHP script (PHP 7.2 or newer) you host; a custom domain on Cloud Enterprise | tracker-proxy, pricing |
| Fathom | Custom domains ended in March 2023; earlier ones keep working | Changelog |
| Simple Analytics | Proxy guides for NGINX, Caddy, Netlify and Vercel; a CNAME subdomain on Enterprise | Docs, pricing |
| Google Analytics 4 | Google tag gateway, through your CDN, load balancer or web server | Docs |
| Vercel Web Analytics | First-party on /_vercel/insights/*; package version 2 (March 11, 2026) discovers its endpoints instead of one predictable path |
Changelog |
| Cloudflare Web Analytics | Automatic setup on proxied sites reports to your own /cdn-cgi/rum; Cloudflare says the beacon is blocked |
FAQ |
| invisit | @invisitdev/server serves the script and every tracker endpoint under /_i |
Server SDK |
Vercel’s troubleshooting page recommends against any reverse proxy in front of Vercel, because visitor identification hashes the incoming request.
How to serve invisit from your own domain
invisit’s proxy is part of its server SDK, @invisitdev/server, rather than a rewrite you write yourself. A plain rewrite to invisit’s collector works, but every visitor then arrives with your server’s address: visitors on the same browser and device type merge into one, and countries show where your server is. The collector accepts a forwarded visitor address only from a proxy that proves itself with your site’s ingest-scoped API key, so nobody else can spoof one.
Create the client with an API key that has only the ingest scope, stored as a server-side variable:
import { createInvisit } from '@invisitdev/server'
export const invisit = createInvisit({ key: process.env.INVISIT_API_KEY! })In Next.js 16 it goes in proxy.ts (before 16, middleware.ts with export async function middleware):
import { NextResponse, type NextFetchEvent, type NextRequest } from 'next/server'
import { invisit } from './lib/invisit'
export async function proxy(request: NextRequest, event: NextFetchEvent) {
const response = await invisit.handle(request, { waitUntil: (p) => event.waitUntil(p) })
return response ?? NextResponse.next()
}
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }Then load the tracker from your domain:
<script defer src="/_i/invisit.js" data-site="<site id>" data-collector="/_i"></script>With the npm package, use init({ site: '<site id>', baseUrl: '/_i' }); the tracker then ships in your own bundle and only its collector calls use the proxy. To rename /_i, pass a prefix such as { prefix: '/_m' } to handle.
The proxy forwards only what the collector reads and keeps cookies, Authorization and Host on your side. We ran this proxy.ts on Next.js 16.4.4 against a stand-in collector: cookies and Set-Cookie were stripped, and the visitor’s address went out as a signed header. The same call records crawler reads, which no browser script sees. The server frameworks guide covers Hono, Express, SvelteKit, Astro, Workers and more.
Analytics proxy pitfalls
Most broken analytics proxies fail in one of four ways: the vendor gets the wrong IP address, the event route gets cached, the path is already on a blocklist, or cookies leak to the vendor.
Forwarding the visitor’s IP address and user agent
The vendor sees your proxy’s address unless you pass the visitor’s, and each vendor reads it differently.
| Tool | What its endpoint expects |
|---|---|
| Plausible | CF-Connecting-IP if present, else the first valid address in X-Forwarded-For; a User-Agent is required (Events API) |
| Umami | Its proxy example sets X-Forwarded-For and User-Agent (docs) |
| Matomo | tracker-proxy sends the IP with a token_auth from a user with write or admin access (README) |
| Simple Analytics | Collects no IP addresses; its Caddy guide removes X-Forwarded-For on purpose (docs) |
| invisit | A signed address header that only @invisitdev/server sends (SDK) |
Get this wrong with Plausible and events are dropped while the API still answers 202. Fathom’s bot detection docs warn that proxied visitors can look like bots.
Cloudflare adds its own trap: in a Worker request to another Cloudflare zone, CF-Connecting-IP is set to 2a06:98c0:3600::103 “for security reasons” (Cloudflare), so a vendor reading that header sees one address for everyone. On November 3, 2026, plausible.io answered from BunnyCDN; cloud.umami.is and api.invisit.dev answered from Cloudflare.
Caching the script but not the event endpoint
Cache the script, never the event route. Plausible’s script came back with Cache-Control: public, max-age=86400, must-revalidate and invisit’s with public, max-age=3600 plus an ETag; our Next.js, Worker and nginx tests passed the vendor’s header through. Make sure no cache-everything rule, or one that overrides origin headers, covers the event route, especially for trackers that send events as GET requests.
Path names that blocklists already know
Avoid the blocked paths in the table above, including vendor defaults: /_vercel/insights/, /cdn-cgi/rum, /umami/script.js, /matomo.js and /api/datafast/ are all on EasyPrivacy. Plausible and PostHog advise against words like analytics, tracking, telemetry and stats. Path rules are specific, so /analytics/script.js passed all three lists, but /analytics/collect? is a rule.
A subdomain that is a CNAME to the vendor is weaker than a path. uBlock Origin on Firefox looks up the canonical name and checks it against its lists, on by default. A subdomain pointing at simpleanalyticsexternal.com matches EasyPrivacy’s ||simpleanalyticsexternal.com^$third-party once uncloaked.
Cookies going the wrong way
Rewrites copy headers both ways. Our Next.js test sent the visitor’s session cookie to the vendor and returned the vendor’s Set-Cookie on your domain. Strip Cookie on the way out, as the Worker and nginx examples do; where config rewrites cannot change headers, proxy from middleware or a Worker instead.
How to check that your analytics proxy works
Load your site with a blocker on and confirm that both requests go to your own domain and succeed.
- Open your site in a browser with uBlock Origin on its default lists.
- Open DevTools, select the Network tab, reload, and filter by your proxy path.
- The script request should come from your domain with status 200, and the event request should return the vendor’s success status, such as 202.
- Clear the filter and check that no analytics request still goes to the vendor’s domain. Requests a blocker stopped fail with
net::ERR_BLOCKED_BY_CLIENTin Chrome. - Check the vendor’s realtime view: your own visit should show your real country, not your server’s.
- For Plausible, send one request with
X-Debug-Request: truethrough the proxy and confirm the IP it returns is yours.
Before you ship the proxy
- The script path and event path use a name of your own, not a vendor default.
- The event route forwards the visitor’s IP, user agent,
Sec-GPCandDNT, and drops cookies. - The script is cached; the event route is not.
- Any middleware matcher skips the proxy path.
- The vendor’s realtime view shows your real country.
- You measured the gap before the switch, so you can compare after.
