Self-host the tracker (anti-blocking)
Chrome 121+, uBlock Origin, Brave Shields, AdGuard and most ad blockers now block by default any request to a third-party domain labelled "analytics", even when the service does no advertising tracking (which is our case).
Without action, you currently lose 5–20% of sessions, and that share will rise to 30–50% within 12–24 months as Chrome rolls _Tracking Protection_ out to all sessions (not just incognito mode).
The solution: serve the tracker and the API from your own domain. The browser then sees a "1st-party" request, indistinguishable from an image or stylesheet load. No blocker blocks that.
This page documents how to do it with first-party configurations that fit a European infrastructure approach. Choose the host and execution region according to your compliance, performance, and support needs.
How it works — high-level diagram
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ BEFORE (3rd-party, blocked) │
│ ─────────────────────── │
│ │
│ browser ──► <script src="https://snorklee.com/w.js"> │
│ ❌ ERR_BLOCKED_BY_CLIENT │
│ │
│ browser ──► POST https://snorklee.com/api/event │
│ ❌ ERR_BLOCKED_BY_CLIENT │
│ │
└──────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ AFTER (1st-party via your proxy, never blocked) │
│ ─────────────────────────────────────────── │
│ │
│ browser ──► <script src="/js/flow.js"> on yoursite.com │
│ ✅ served by your proxy from snorklee.com │
│ │
│ browser ──► POST yoursite.com/api/event │
│ ✅ relayed by your proxy to snorklee.com │
│ │
└──────────────────────────────────────────────────────────────────────────┘
You proxy the script and 4 API endpoints from your domain to snorklee.com:
| Path on your domain | Target on snorklee | Role |
|---|---|---|
/js/flow.js | snorklee.com/w.js | Tracker script |
/api/event | snorklee.com/api/event | Pageviews + custom events |
/api/ping | snorklee.com/api/ping | "Live" heartbeat every 30s |
/api/zone | snorklee.com/api/zone | Scroll attention zones |
/api/click | snorklee.com/api/click | Aggregated click heatmap |
The snippet you put in <head> becomes:
<script defer src="/js/flow.js" data-site="yoursite.com"></script>
All paths are 1st-party; nothing leaves to a third-party domain from the browser's perspective. Server-side, your proxy bridges to snorklee.com transparently.
Important — preserve the visitor IP: your proxy must forward X-Forwarded-For correctly, otherwise snorklee's geolocation will resolve the proxy's IP instead of the visitor's. Each recipe below includes this explicitly.
Recipe 1 — Nginx (most universal)
Works on: any Linux VPS (OVH, Scaleway, Hetzner, Clever Cloud with Nginx runtime, IONOS, Infomaniak…).
Sovereignty: neutral (depends on your hosting provider — choose EU).
Add this block inside the server { ... } that serves your site (typically /etc/nginx/sites-available/yoursite.conf):
# snorklee — 1st-party proxy (anti-blocking)
location = /js/flow.js {
proxy_pass https://snorklee.com/w.js;
proxy_set_header Host snorklee.com;
proxy_ssl_server_name on;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Accept-Language $http_accept_language;
proxy_hide_header Set-Cookie;
proxy_read_timeout 10s;
}
location ~ ^/api/(event|ping|zone|click)$ {
proxy_pass https://snorklee.com$request_uri;
proxy_set_header Host snorklee.com;
proxy_ssl_server_name on;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Accept-Language $http_accept_language;
proxy_set_header Origin $http_origin;
proxy_hide_header Set-Cookie;
proxy_read_timeout 10s;
}
Then:
sudo nginx -t && sudo systemctl reload nginx
Verification: open https://yoursite.com/js/flow.js in your browser — you should see the minified tracker code. If you get a 502, check that proxy_ssl_server_name on; is set (required for SNI to snorklee.com).
Recipe 2 — Caddy (simplest)
Works on: any host running Caddy (auto-TLS included).
Sovereignty: neutral.
In your Caddyfile, inside your site block:
yoursite.com {
# ... your existing config ...
# snorklee — 1st-party proxy (anti-blocking)
@snorkleeApi path /api/event /api/ping /api/zone /api/click
handle_path /js/flow.js {
rewrite * /w.js
reverse_proxy https://snorklee.com {
header_up Host snorklee.com
header_down -Set-Cookie
}
}
handle @snorkleeApi {
reverse_proxy https://snorklee.com {
header_up Host snorklee.com
header_down -Set-Cookie
}
}
}
Then:
caddy reload --config /etc/caddy/Caddyfile
Caddy 2.6+ syntax: use a named matcher (@snorkleeApi path …) to applyhandleto multiple paths. The formhandle /a /b /c { … }is not accepted by the parser and failscaddy validate.
Caddy injectsX-Forwarded-ForandX-Real-IPautomatically.
Recipe 3 — Apache (mod_proxy)
Works on: shared hosting (OVH, Infomaniak, IONOS) and any Apache server with mod_proxy enabled. Useful for WordPress on cPanel without root access.
Sovereignty: neutral.
In your .htaccess (site root) or <VirtualHost>:
# snorklee — 1st-party proxy (anti-blocking)
SSLProxyEngine On
# Tracker script
RewriteEngine On
RewriteRule ^js/flow\.js$ https://snorklee.com/w.js [P,L]
# API
RewriteRule ^api/(event|ping|zone|click)$ https://snorklee.com/api/$1 [P,L]
# Preserve visitor IP (setifempty avoids overwriting an upstream XFF
# if you sit behind another LB/reverse-proxy that already sets one)
ProxyPreserveHost Off
RequestHeader setifempty X-Forwarded-For "%{REMOTE_ADDR}s"
RequestHeader setifempty X-Real-IP "%{REMOTE_ADDR}s"
# Never forward visitor-side session cookies
Header always unset Set-Cookie
Required modules (typically already enabled on serious EU shared hosts): mod_proxy, mod_proxy_http, mod_ssl, mod_rewrite, mod_headers. On OVH shared, ask support to enable if missing — it's free and standard.
Recipe 4 — Bunny.net Edge Scripting
For whom: high-traffic sites that want a CDN edge closer to the visitor (latency win + origin offload).
European footprint: Bunny.net is operated by BunnyWay d.o.o. in Slovenia. If you want routing to stay close to Europe, limit the pricing zones to Europe during setup.
Pricing: ~€0.01 per million Edge Script requests + ~€0.005 per GB CDN bandwidth. For 1M pageviews/month, count ~€5 total. No minimum, pay-as-you-go.
Steps:
- Create an account on bunny.net (credit card, ~€5 free initial credit).
- Create a "Pull Zone":
- _CDN_ tab → _Add Pull Zone_
- Name:
mysite-snorklee(free choice) - Origin URL:
https://snorklee.com - Pricing tier: Standard
- Pricing zones: you can keep only Europe if you want to limit routing and costs to that zone
- Connect your domain via CNAME:
- _Hostnames_ tab → _Add Hostname_ →
flow.yoursite.com - At your DNS registrar (Gandi, OVH, etc.): add
CNAME flow.yoursite.com → mysite-snorklee.b-cdn.net - Wait 5 min, return to Bunny → click _Generate Free SSL Certificate_ (auto Let's Encrypt)
- Map the path:
- _Edge Rules_ tab → _Add Edge Rule_
- Action: _Override URL_
- Match:
Request URL contains "/js/flow.js" - Override URL:
https://snorklee.com/w.js
- Final snippet:
<script defer src="https://flow.yoursite.com/js/flow.js" data-site="yoursite.com"></script>
All /api/* paths automatically pass through to snorklee.com/api/* via the Pull Zone.
IP note: Bunny adds X-Forwarded-For correctly by default. Verify in the snorklee dashboard, Analytics tab (Audience section, Countries card), that countries resolve correctly within 1–2 hours.
Recipe 5 — Next.js / Nuxt rewrites
For whom: modern JavaScript-stack sites whose host supports rewrites or server routes.
Hosting: check that your platform preserves the useful headers (X-Forwarded-For, HTTP method, User-Agent, Accept-Language) and that the chosen execution region matches your privacy and performance needs.
Possible options include classic Node/SSR hosting, serverless with an explicit region, a Docker container, or a frontend platform that exposes server-side rewrites.
Next.js — in next.config.js:
module.exports = {
async rewrites() {
return [
{ source: '/js/flow.js', destination: 'https://snorklee.com/w.js' },
{ source: '/api/event', destination: 'https://snorklee.com/api/event' },
{ source: '/api/ping', destination: 'https://snorklee.com/api/ping' },
{ source: '/api/zone', destination: 'https://snorklee.com/api/zone' },
{ source: '/api/click', destination: 'https://snorklee.com/api/click' },
];
},
};
Nuxt 3 — in nuxt.config.ts:
export default defineNuxtConfig({
routeRules: {
'/js/flow.js': { proxy: 'https://snorklee.com/w.js' },
'/api/event': { proxy: 'https://snorklee.com/api/event' },
'/api/ping': { proxy: 'https://snorklee.com/api/ping' },
'/api/zone': { proxy: 'https://snorklee.com/api/zone' },
'/api/click': { proxy: 'https://snorklee.com/api/click' },
},
});
Next/Nuxt rewrites preserve X-Forwarded-For and HTTP method automatically. POST events go through correctly.
Choosing your host
The first-party proxy works with many hosts. Before choosing a platform, check:
- the execution region actually used by the proxy;
- the host-side log retention period;
- correct forwarding of
X-Forwarded-For; - the ability to remove unnecessary cookies or headers;
- the contractual commitments you need for your own privacy policy.
Verify it works
- Script loads — open
https://yoursite.com/js/flow.jsin your browser, you should see the minified tracker code (starts with!function()...). - Events leave — open DevTools (F12) → _Network_ tab, click around your site. You should see
POST /api/eventreturning204 No Content. - Geolocation works — connect to the snorklee dashboard, wait 1–2 minutes, check the _Countries_ card in the _Analytics_ tab (Audience section). If all visitors resolve from your proxy's country instead of their real one,
X-Forwarded-Foris not being forwarded. Review your proxy config. - Tracker tab in dashboard — the HTTP probe "Test installation" auto-detects self-host mode and shows _"1st-party proxy detected"_ in the result.
FAQ
Does the proxy slow down my site? No, or marginally. The tracker flow.js is ≈ 2 KB gzip and stays browser-cached. Events leave via non-blocking sendBeacon, so invisible to visitors even if the proxy adds 50 ms of latency.
What if my proxy goes down? Events are lost during the outage (no offline queue, by doctrine §25 TDDDG / GDPR minimization). Your site keeps working normally — only audience measurement stops. Same as any other third-party service.
Can I proxy only the script and not the API? Yes, with the data-api attribute:
<script
defer
src="/js/flow.js"
data-site="yoursite.com"
data-api="https://snorklee.com"
></script>
The script is served 1st-party (bypasses filename-based blockers) but events go to snorklee.com directly (re-blocked by domain-based blockers). Less effective than the full recipe; documented here for edge cases only.
What if I switch analytics provider later? The proxy structure stays the same, just change the targets. The <script src="/js/flow.js" data-site="..."> snippet is universal.
Need help?
- 🛠️ Dashboard _Integration_ tab → _Test installation_ button (auto HTTP probe)
- 📧 DPO / support contact: see _Compliance_ tab in the dashboard
- 📚 Full docs: /docs