Install & operate
Serving from your own domain
Content blockers remove analytics two ways: by hostname (anything sent to a
known analytics domain) and by path (a URL shaped like /api/collect,
on any domain — yours included). Vitrus handles the second out of the box; a proxy on your own domain
handles the first, and typically recovers the 10–30% of visitors that would otherwise be invisible.
Paths are already handled
The tracker sends events to /api/d. Blocklists match
/api/collect as a path on every domain, so a first-party install
still lost every visitor running one. /api/collect keeps working for
tags and integrations already in place — it is the same endpoint.
What you need to proxy
| Your path | Upstream | Why |
|---|---|---|
/vs.js | https://app.vitrus.dev/v.js | The tracker script |
/vs/api/d | https://app.vitrus.dev/api/d | Where events are sent |
/vs/r.js, /vs/api/replay/* | https://app.vitrus.dev/… | Only if you use session replay |
Pick your own path names — /vs.js is just an example. Then point the
tag at them with data-host:
<script defer data-site="SITE_ID" data-host="https://example.com/vs" src="/vs.js"></script>
data-host is the base the tracker appends
/api/d to, which is why the examples proxy
/vs/api/d.
X-Vitrus-Client-IP.
Behind your proxy, every request reaches us from your server's address. Without this header all
visitors hash to the same visitor id and your unique count collapses to roughly one. Plain
X-Forwarded-For is not enough: it does not survive our edge. The IP is
never stored — it feeds a daily-salted hash and is discarded.nginx
location = /vs.js {
proxy_pass https://app.vitrus.dev/v.js;
proxy_set_header Host app.vitrus.dev;
proxy_ssl_server_name on;
}
location = /vs/api/d {
proxy_pass https://app.vitrus.dev/api/d;
proxy_set_header Host app.vitrus.dev;
proxy_set_header X-Vitrus-Client-IP $remote_addr;
proxy_ssl_server_name on;
}
If nginx itself sits behind Cloudflare, use $http_cf_connecting_ip
instead of $remote_addr.
Caddy
example.com {
handle /vs.js {
rewrite * /v.js
reverse_proxy https://app.vitrus.dev {
header_up Host app.vitrus.dev
}
}
handle /vs/api/d {
rewrite * /api/d
reverse_proxy https://app.vitrus.dev {
header_up Host app.vitrus.dev
header_up X-Vitrus-Client-IP {remote_host}
}
}
}
Cloudflare Workers
Route example.com/vs* to this Worker.
export default {
async fetch(request) {
const url = new URL(request.url);
const path = url.pathname.replace(/^\/vs/, "");
const target = path === ".js" ? "/v.js" : path;
const headers = new Headers(request.headers);
headers.set("X-Vitrus-Client-IP", request.headers.get("CF-Connecting-IP") || "");
return fetch("https://app.vitrus.dev" + target + url.search, {
method: request.method,
headers,
body: request.method === "GET" ? undefined : request.body,
});
}
};
Next.js
A rewrite cannot add a header, so the script uses a rewrite and the endpoint a route handler.
// next.config.js module.exports = { async rewrites() { return [{ source: "/vs.js", destination: "https://app.vitrus.dev/v.js" }]; } }; // app/vs/api/d/route.js export async function POST(request) { const ip = (request.headers.get("x-forwarded-for") || "").split(",")[0].trim(); const res = await fetch("https://app.vitrus.dev/api/d", { method: "POST", headers: { "content-type": "application/json", "user-agent": request.headers.get("user-agent") || "", "X-Vitrus-Client-IP": ip, }, body: await request.text(), }); return new Response(null, { status: res.status }); }
Forward the user-agent too: it is half of the visitor hash and all of
the browser, OS and bot classification. The Worker and the proxies above pass it through already.
Checking it worked
- Load your site and confirm
/vs.jsreturns JavaScript, not a 404. - In the network tab, the
/vs/api/drequest should go to your own domain and return204. - In the dashboard, check that unique visitors is not stuck at 1 — if it is, the forwarded IP header is missing.
Self-hosting instead
If you self-host, this is moot: the script and the endpoint are already on your own domain. See self-hosting.