Tracking
Proxying (first-party)
Serve the tracker and the collect endpoint from your own domain with Next.js, vercel.json, nginx, Caddy or a Cloudflare Worker.
Ad blockers and privacy extensions often block requests to third-party analytics hosts. Serving the script and the collect endpoint from your own domain makes Globatics first-party by design: the browser only talks to your site.
Why proxy
- Fewer blocked visits, so counts closer to reality. How many depends on your audience.
- One less third-party host in your Content Security Policy.
- Proxy three paths:
script.js,vitals.js(loaded from the same folder as the script) andapi/collect. Then point the tag at them:
<script defer data-site="SITE_ID" data-api="/_glb/api/collect" src="/_glb/script.js"></script>data-api is required
Without data-api, the tracker posts to /api/collect on the origin it was loaded from, which is now your own site. Keep the attribute, and use a neutral prefix like /_glb: blockers match paths and subdomains named “analytics”, “tracking” or “telemetry”.
Next.js rewrites
import type { NextConfig } from "next";
const GLOBATICS = "https://globatics.live";
const nextConfig: NextConfig = {
async rewrites() {
return [
{ source: "/_glb/script.js", destination: `${GLOBATICS}/script.js` },
{ source: "/_glb/vitals.js", destination: `${GLOBATICS}/vitals.js` },
{ source: "/_glb/api/collect", destination: `${GLOBATICS}/api/collect` },
];
},
};
export default nextConfig;Rewrites can’t add a header, so whether the visitor’s address reaches Globatics depends on your host (see Forward the visitor’s IP). Check once: open Live, then visit your site from your phone on mobile data. If you show up in your hosting region instead of your city, keep the two script rewrites and serve the collect endpoint with a route handler that forwards the address:
// "%5Fglb" is the URL-encoded "_glb": folders starting with "_" are private in the App Router.
const GLOBATICS = "https://globatics.live";
export async function POST(req: Request) {
// on Vercel, the first X-Forwarded-For entry is the visitor
const ip = (req.headers.get("x-forwarded-for") ?? "").split(",")[0].trim();
const res = await fetch(`${GLOBATICS}/api/collect`, {
method: "POST",
body: await req.text(),
headers: {
"content-type": "text/plain",
"user-agent": req.headers.get("user-agent") ?? "",
"x-globatics-ip": ip,
},
});
return new Response(null, { status: res.status });
}The same goes for vercel.json rewrites on other frameworks hosted on Vercel: same three source and destination pairs, same check.
nginx
location = /_glb/script.js { proxy_pass https://globatics.live/script.js; include globatics.conf; }
location = /_glb/vitals.js { proxy_pass https://globatics.live/vitals.js; include globatics.conf; }
location = /_glb/api/collect { proxy_pass https://globatics.live/api/collect; include globatics.conf; }
# /etc/nginx/globatics.conf
proxy_set_header Host globatics.live;
proxy_set_header X-Globatics-IP $remote_addr; # behind Cloudflare: $http_cf_connecting_ip
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Cookie ""; # never send your site's cookies
proxy_ssl_server_name on;
proxy_http_version 1.1;Caddy
example.com {
# /_glb/script.js → /script.js, /_glb/api/collect → /api/collect
handle_path /_glb/* {
reverse_proxy https://globatics.live {
header_up Host {upstream_hostport}
header_up X-Globatics-IP {remote_host}
header_up -Cookie
}
}
# …the rest of your site
}Cloudflare Worker
const UPSTREAM = "https://globatics.live";
const ALLOWED = new Set(["/script.js", "/vitals.js", "/api/collect"]);
export default {
async fetch(request) {
const path = new URL(request.url).pathname.replace(/^\/_glb/, "");
if (!ALLOWED.has(path)) return new Response("Not found", { status: 404 });
const headers = new Headers(request.headers);
headers.set("X-Globatics-IP", request.headers.get("CF-Connecting-IP") || "");
headers.delete("Cookie");
return fetch(UPSTREAM + path, {
method: request.method,
headers,
body: request.method === "POST" ? request.body : undefined,
});
},
};Forward the visitor’s IP
Globatics uses the visitor’s IP for three things only: the country and city, the daily visitor hash and flood protection. It is never stored. Behind a proxy, every request comes from the proxy’s address. Left as is, every visitor would be placed in your proxy’s city, visitors with the same browser version could merge into one, and they would all share one rate limit.
Send the visitor’s address in X-Globatics-IP, as the nginx, Caddy, Worker and route-handler recipes above do. Globatics accepts one public address there, and looks it up instead of the proxy’s location. Hosted Globatics runs on Vercel, which replaces X-Forwarded-For with the address that connected to it, so this header is the reliable way.
Trade-off
The header can’t be signed, so anyone could send it. The only thing a forged value changes is the location and visitor hash of the sender’s own hits. Flood protection still applies per connecting address: a proxy gets 20 times the budget of a single visitor (about 600 requests per second per server instance), and its requests past that are counted against the proxy as usual.
Self-hosting? Set GLOBATICS_TRUST_PROXY=1 when your own proxy sits in front of Globatics. The address is then read from CF-Connecting-IP, then X-Real-IP, then the first X-Forwarded-For entry. Without it, Globatics takes the right-most public address, which a visitor can’t spoof but which is your proxy’s. X-Globatics-IP works in both cases.
GLOBATICS_TRUST_PROXY=1To check, send a test visit through the proxy: if it shows your server’s city instead of yours, the address isn’t reaching Globatics.