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.
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
/**
|
|
* Server request shell. Reads the incoming request once, classifies it via
|
|
* 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";
|
|
|
|
import { recordPageviewToUmami } from "@/src/analytics/recordPageviewToUmami";
|
|
import { decidePageview } from "@/src/domain/pageview-decision";
|
|
|
|
const primaryLanguage = (acceptLanguage: string | null): string =>
|
|
acceptLanguage?.split(",")[0]?.split(";")[0]?.trim() ?? "";
|
|
|
|
const firstForwardedIp = (forwardedFor: string | null): string =>
|
|
forwardedFor?.split(",")[0]?.trim() ?? "";
|
|
|
|
const hostnameWithoutPort = (host: string): string => host.replace(/:\d+$/, "");
|
|
|
|
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(
|
|
request.headers.get("x-forwarded-host") ??
|
|
request.headers.get("host") ??
|
|
"",
|
|
),
|
|
referrer: request.headers.get("referer") ?? "",
|
|
language: primaryLanguage(request.headers.get("accept-language")),
|
|
userAgent: request.headers.get("user-agent") ?? "",
|
|
clientIp: firstForwardedIp(request.headers.get("x-forwarded-for")),
|
|
});
|
|
|
|
if (decision.track) {
|
|
recordPageviewToUmami(decision.facts);
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
"/((?!_next/static|_next/image|.*\\.(?:png|jpe?g|gif|svg|webp|avif|ico|css|mjs|cjs|js|map|txt|xml|json|webmanifest|woff2?|ttf|otf|mp4|webm|mov|pdf|zip)$).*)",
|
|
],
|
|
};
|