Skip to content

Proxy DataFast through your Next.js domain so ad blockers stop eating visits

Blockers match the datafa.st hostname. Serve the script and the events endpoint from your own origin with two route handlers, and forward the visitor's IP.

DataFast3 min readships at docs/solutions/datafast/proxy-datafast-through-nextjs.md

Tags: datafast · analytics · ad-blockers · reverse-proxy · nextjs · route-handlers

Your server logs show 1,000 visits. DataFast shows 780. The gap does not move. That is not a bug in the script. It is a blocklist.

Why it happens

Blocklists match hostnames. datafa.st is on the common privacy lists, so uBlock Origin, Brave and friends drop the request before it leaves the browser. Nothing throws. Nothing logs. The visitor just never exists.

The people who block skew technical. If you sell to developers, the traffic you lose is the traffic you most wanted to measure.

The fix: same-origin paths

If the page loads /js/script.js from your own domain and posts to /api/events on your own domain, there is no third-party hostname to match. Your server forwards both to DataFast, from a data centre, where no blocker runs.

DataFast supports this out of the box. When the script is loaded from anywhere other than datafa.st, it posts events to /api/events on the page's origin. You can point it elsewhere with data-api-url.

Option 1: rewrites (DataFast's docs)

// next.config.ts
async rewrites() {
  return [
    { source: "/js/script.js", destination: "https://datafa.st/js/script.js" },
    { source: "/api/events", destination: "https://datafa.st/api/events" },
  ];
}

Short, and it works. It is also invisible: you cannot log it, and you cannot return something harmless when the upstream is down.

Option 2: two route handlers

The script:

// src/app/js/script.js/route.ts
export const dynamic = "force-dynamic";

export async function GET() {
  try {
    const upstream = await fetch("https://datafa.st/js/script.js", {
      signal: AbortSignal.timeout(5_000),
      next: { revalidate: 3600 },
    });
    if (!upstream.ok) throw new Error(String(upstream.status));
    return new Response(await upstream.text(), {
      headers: {
        "content-type": "application/javascript; charset=utf-8",
        "cache-control": "public, max-age=3600",
      },
    });
  } catch {
    // An empty script, not an error page. The site keeps working.
    return new Response("", { headers: { "content-type": "application/javascript" } });
  }
}

Yes, the folder is literally named script.js. The App Router allows dots in segment names.

The events:

// src/app/api/events/route.ts
import type { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const headers = new Headers({ "content-type": "application/json" });
  for (const name of ["user-agent", "origin", "referer"]) {
    const value = request.headers.get(name);
    if (value) headers.set(name, value);
  }
  const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
  if (ip) {
    headers.set("x-forwarded-for", ip);
    headers.set("x-real-ip", ip);
  }

  try {
    const upstream = await fetch("https://datafa.st/api/events", {
      method: "POST",
      headers,
      body: await request.text(),
      signal: AbortSignal.timeout(5_000),
    });
    return new Response(await upstream.text(), { status: upstream.status });
  } catch {
    return new Response(null, { status: 204 });
  }
}

Then the tag:

<Script
  src="/js/script.js"
  data-website-id={process.env.NEXT_PUBLIC_DATAFAST_WEBSITE_ID}
  data-domain="example.com"
  data-api-url="/api/events"
  strategy="afterInteractive"
/>

The three details that break it

Forward the IP. Behind your proxy, DataFast sees your server. Skip the forwarded headers and every visitor comes from iad1, on the same browser. DataFast's own troubleshooting names this: "all visitors showing from the same location".

Forward the user agent. Browser, OS and device come from it. Without it, everything is "unknown".

Answer inert on failure. An empty script and a 204. A red 500 in a visitor's console, for analytics, is a bad trade.

Already have /api/events?

Pick another path for the handler, say /api/df-events, and set data-api-url="/api/df-events" on the tag. Keep it boring. A path with "analytics" or "tracking" in it is exactly what a blocklist adds next.

The managed alternative

DataFast also runs a managed proxy: a CNAME such as a.example.com pointing at DataFast, with SSL handled for you. Then src and data-api-url both use full URLs on that subdomain. No code, one DNS record. Pick it if you would rather not own two route handlers.

Check it works

  1. Deploy. Open the site with uBlock Origin on.
  2. DevTools, Network: script.js and events hit your domain and return 200. Nothing to datafa.st.
  3. DataFast's realtime view shows you, in your real city.

Proxying changes a hostname. It does not change what you collect or what consent you need.