Sign up

Proxy

Proxy with Next.js

On this page

One route handler in the App Router serves the script and relays events.

Set it up

  1. Add a route handler

    app/a/[kind]/route.ts
    // app/a/[kind]/route.ts — Next.js App Router
    import { NextRequest } from "next/server";
    
    async function proxy(req: NextRequest, context: { params: Promise<{ kind: string }> }) {
      const { kind } = await context.params;
      const script = kind === "script" && req.method === "GET";
      const event = kind === "event" && req.method === "POST";
      if (!script && !event) return new Response("Not found", { status: 404 });
      const headers = new Headers();
      for (const name of ["content-type", "origin", "user-agent"]) {
        const value = req.headers.get(name);
        if (value) headers.set(name, value);
      }
      // Trust these only if your hosting provider overwrites them.
      const ip = req.headers.get("cf-connecting-ip") || req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
      if (ip) headers.set("x-visitor-ip", ip);
      for (const [source, target] of [["cf-ipcountry", "x-visitor-country"], ["cf-region", "x-visitor-region"], ["cf-ipcity", "x-visitor-city"]]) {
        const value = req.headers.get(source);
        if (value) headers.set(target, value);
      }
      const upstream = await fetch(script ? "https://metricraven.com/js/script.js" : "https://metricraven.com/api/v1/collect", {
        method: req.method, headers, body: event ? await req.arrayBuffer() : undefined,
        cache: "no-store", signal: AbortSignal.timeout(15000),
      });
      return new Response(upstream.body, { status: upstream.status, headers: {
        "Content-Type": upstream.headers.get("content-type") || "application/octet-stream",
        "Cache-Control": script ? "public, max-age=3600" : "no-store",
      }});
    }
    export { proxy as GET, proxy as POST };

    On Vercel, x-forwarded-for is set by the platform. Behind Cloudflare, cf-connecting-ip and Cloudflare's visitor location headers are used when present.

  2. Load the tracker with next/script

    In your root layout, set src="/a/script" and data-api="/a/event" on the existing Script component.

  3. Point your script tag at your own paths

    Replace your MetricRaven tag with this one. src loads the tracker from your domain and data-api sends its events there too.

    Your website's shared layout
    <script defer src="/a/script" data-api="/a/event" data-site-id="YOUR_SITE_ID"></script>

Verify it works

Open your live site. In the browser's Network panel, /a/script loads the tracker and /a/event answers with ok: true. Nothing goes to metricraven.com from the browser.

In Traffic, your own visit should show your real country and city. If every visit appears to come from your server's or host's location, the visitor IP isn't being forwarded.

Notes

  • The handler reads x-forwarded-for. Trust it only if your host overwrites it; a self-hosted Next.js server without a proxy in front should use the socket address instead.
  • If you use Next.js middleware with a matcher for all paths, exclude /a/ so it doesn't redirect or rewrite the tracker's requests.

How the proxy works and which headers it must send: Use your own domain.

Details

  • The proxy forwards only Content-Type, Origin, User-Agent and the visitor's IP as x-visitor-ip. Your site's cookies and auth headers never leave your server.
  • Origin matters: MetricRaven checks it against your site's allowed domains. A missing or wrong origin is refused with domain_not_allowed.
  • MetricRaven allows about 120 events per minute per visitor IP. Without x-visitor-ip, all of your visitors share the proxy's IP, so busy sites lose events as well as locations.
  • A proxy doesn't guarantee that every blocker will allow tracking.