fix(analytics): identify page views via fetch metadata, not router headers
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Successful in 6m26s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🩺 Smoke Test (push) Successful in 5s
Build & Deploy / 🔔 Notify (push) Successful in 3s

Next.js strips the router-internal headers (RSC, next-router-prefetch)
before the request proxy runs, so prefetch detection based on them never
fired: link prefetches were counted as page views. A page view is now a
browser navigation, identified by the standard sec-fetch-dest: document
header, which survives into the proxy. Prefetch payloads, subresource
fetches and metadata-less clients are skipped as non-navigation
requests.
This commit is contained in:
2026-10-04 12:51:54 +02:00
parent a54df524c7
commit ed35413a44
3 changed files with 40 additions and 33 deletions
+5 -7
View File
@@ -3,6 +3,10 @@
* the pure domain decision, and records a page view without touching the
* response. All analytics happen server-side; no tracking script ships to
* the browser.
*
* Page views are browser navigations (`sec-fetch-dest: document`).
* Router-internal headers (RSC, next-router-prefetch) are stripped by the
* server before this shell runs and cannot be used here.
*/
import { NextResponse, type NextRequest } from "next/server";
@@ -18,14 +22,10 @@ const firstForwardedIp = (forwardedFor: string | null): string =>
const hostnameWithoutPort = (host: string): string => host.replace(/:\d+$/, "");
const isPrefetchRequest = (request: NextRequest): boolean =>
request.headers.get("next-router-prefetch") === "1" ||
request.headers.get("purpose") === "prefetch" ||
request.headers.get("sec-purpose") === "prefetch";
export default function proxy(request: NextRequest): NextResponse {
const decision = decidePageview({
method: request.method,
secFetchDest: request.headers.get("sec-fetch-dest") ?? "",
pathname: request.nextUrl.pathname,
search: request.nextUrl.search,
hostname: hostnameWithoutPort(
@@ -37,8 +37,6 @@ export default function proxy(request: NextRequest): NextResponse {
language: primaryLanguage(request.headers.get("accept-language")),
userAgent: request.headers.get("user-agent") ?? "",
clientIp: firstForwardedIp(request.headers.get("x-forwarded-for")),
isRscNavigation: request.headers.get("rsc") === "1",
isPrefetch: isPrefetchRequest(request),
});
if (decision.track) {