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.

This is not evasion. Do Not Track and Global Privacy Control are still respected, no cookie is set and no identifier is stored. A proxy changes where the request goes, not what is collected. If a visitor sends DNT, they are still not measured.

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 pathUpstreamWhy
/vs.jshttps://app.vitrus.dev/v.jsThe tracker script
/vs/api/dhttps://app.vitrus.dev/api/dWhere 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.

Send the visitor's IP in 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

  1. Load your site and confirm /vs.js returns JavaScript, not a 404.
  2. In the network tab, the /vs/api/d request should go to your own domain and return 204.
  3. 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.