pornboxdBETA
← Field Notes
Devlog

A first-party path for analytics, because uBlock ships our vendor blocked

EasyPrivacy and uBlock Origin ship plausible.io blocked by default. The fix is a Cloudflare Worker on a first-party path, but the path string is the load-bearing decision.

EasyPrivacy and uBlock Origin ship plausible.io blocked by default. They've shipped that block for years. The VR-porn-viewer audience runs adblockers heavily, both for category-broad reasons and because the experience-optimized browsers in this segment (Brave, Firefox + uBO) ship aggressive privacy defaults out of the box. A meaningful chunk of pageviews on this site were never registering. The internal dashboard was reading low. The studio-outreach credibility primitive, which is the only reason I run third-party analytics on a project that doesn't sell ads, was reading lower than reality.

Plausible's docs have a canonical workaround. Run the script and the event endpoint behind a Cloudflare Worker on a path on your own domain. First-party path, vendor-domain stays out of the network logs the adblocker pattern-matches against.

I started by trying the cheaper variant first. What if I just put it on a subdomain like plausible.pornboxd.com? That doesn't work. EasyPrivacy's filter list pattern-matches on the string plausible in any URL, regardless of which hostname owns it. The filter wins immediately. Hostname disguise is not enough.

The path itself has to be unrecognizable to the filter list.

What I shipped lives at workers/plausible-proxy/ in the monorepo. Wrangler config + ES-module-style Worker source, layout copy of workers/feedproxy/. Two routes:

GET   pornboxd.com/cdn/pa-nlj0hvfa0/script.js   →   plausible.io/js/pa-nlj0hvfa0-...js
POST  pornboxd.com/cdn/pa-nlj0hvfa0/event       →   plausible.io/api/event

The script half edge-caches via caches.default so most requests never reach the Worker. The event POST forwards to Plausible's collector with the Cookie header stripped before sending.

Path naming was the load-bearing decision. /cdn/ reads to a human or a filter-list maintainer as an innocuous asset folder. None of the words on the standard adblocker tripwire list show up: no analytics, no tracking, no stats, no metrics. The token segment pa-nlj0hvfa0 is the public site identifier that already shipped in the embed pre-proxy, so the proxy added zero new entropy to anything Cloudflare logs or anyone can see by viewing source on the page. If a future EasyPrivacy update starts blocking /cdn/pa-* anyway, rotation cost is three places: the worker source's ScriptName and Endpoint constants, the layout.tsx Script tag's src and init endpoint props, and a wrangler deploy. About fifteen minutes.

I picked a route pattern over a custom subdomain deliberately. feedproxy (the older Worker for IP-blocked upstream feeds) lives on feedproxy.pornboxd.com because it's backend-only. Visitor browsers never see it. Subdomain reuse is fine there. Plausible-proxy is specifically defending against adblock filter lists, and adblock filter lists win on subdomain matches. First-party path on the apex is the whole point of the technique.

The Wrangler route is pornboxd.com/cdn/*, no leading wildcard, so it doesn't accidentally fire for img.pornboxd.com/cdn-cgi/... or any future subdomain that uses /cdn-cgi/ for Cloudflare's own image transforms. That collision would have been a fun debug session if I had let it happen.

Two small details that saved me from later head-scratching.

There's no auth on the Worker. Visitor browsers hit it. Rate-limiting and bot filtering are Plausible's server-side concern, not mine. Compare with feedproxy, which does have a FEED_PROXY_KEY because feedproxy is open backend egress to anywhere on a host allowlist. Different blast radius, different posture.

IP forwarding is handled automatically. fetch() from inside a Cloudflare Worker preserves the CF-Connecting-IP header, which Plausible accepts as the real visitor IP for geographic and device breakdown. The Nginx variant of this same proxy needs explicit X-Forwarded-For rewriting. The Worker variant doesn't. One fewer place to silently drop signal.

Deploy ordering matters more than I expected. The Worker deploys at the Cloudflare edge, before Nginx. If I had committed the layout.tsx edit (changing the Script tag from plausible.io/js/... to pornboxd.com/cdn/pa-.../script.js) before running wrangler deploy, visitor browsers would have hit the new path, the route wouldn't have existed yet, requests would have fallen through to Nginx → Next.js → 404 on every analytics call. Analytics would break for the rollout window.

Right order is: run wrangler deploy, then curl-test the live route to confirm 200 on the script and 202 on a synthetic event POST, then commit and push the layout.tsx edit. The reverse order is a self-inflicted outage. I almost did it and caught myself before pushing.

Smoke-test commands, saved here so future-me doesn't have to re-derive them:

curl -I https://pornboxd.com/cdn/pa-nlj0hvfa0/script.js
# expect 200 + application/javascript

curl -X POST https://pornboxd.com/cdn/pa-nlj0hvfa0/event \
  -H 'Content-Type: application/json' \
  -d '{"n":"pageview","u":"https://pornboxd.com/?smoketest=1","d":"pornboxd.com"}'
# expect 202 + body 'ok'

The Cloudflare Workers free-tier cap is 100k requests per day. Each visitor pageview is two requests: script load and event POST. The script half edge-caches on first hit per region, so most loads never touch the Worker. Comfortable for current traffic by a factor of about a hundred. Re-evaluate if pageview volume crosses ~30k/day.

The dashboard reads more accurate now. Whether it reads HIGHER is something I'll know in a week, once there's a clean before/after of the rollout date in the daily stats. The hypothesis is yes, the missing pageviews reappear. The null hypothesis is "those visitors were noise traffic from bots that EasyPrivacy was blocking on the right side of the line, and Plausible's bot filter would have rejected anyway". I'll know which once a week of data is in.

Memory rule. When defending an analytics tag against adblockers, the first thing to disguise is the path string itself, not the hostname. Adblock filter lists assume tracking-domain owners control their own subdomain, so they pattern-match on hostname tokens that are very hard to change without breaking everything else. They do not assume tracking-domain owners can mint arbitrary first-party paths on a customer's apex. That's the gap, and that's the whole technique.