fix(analytics): identify page views via fetch metadata, not router headers
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:
@@ -8,6 +8,7 @@ const humanRequest = (
|
||||
overrides: Partial<IncomingPageviewRequest> = {},
|
||||
): IncomingPageviewRequest => ({
|
||||
method: "GET",
|
||||
secFetchDest: "document",
|
||||
pathname: "/",
|
||||
search: "",
|
||||
hostname: "mintel.me",
|
||||
@@ -15,13 +16,11 @@ const humanRequest = (
|
||||
language: "de-DE",
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/131.0.0.0",
|
||||
clientIp: "84.117.2.55",
|
||||
isRscNavigation: false,
|
||||
isPrefetch: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("decidePageview", () => {
|
||||
it("tracks a document request for the homepage", () => {
|
||||
it("tracks a browser navigation for the homepage", () => {
|
||||
const decision = decidePageview(humanRequest());
|
||||
expect(decision).toEqual({
|
||||
track: true,
|
||||
@@ -60,11 +59,6 @@ describe("decidePageview", () => {
|
||||
expect(decision).toMatchObject({ track: true });
|
||||
});
|
||||
|
||||
it("tracks a client-side route change (RSC navigation without prefetch)", () => {
|
||||
const decision = decidePageview(humanRequest({ isRscNavigation: true }));
|
||||
expect(decision).toMatchObject({ track: true });
|
||||
});
|
||||
|
||||
it("accepts the method in lower case", () => {
|
||||
const decision = decidePageview(humanRequest({ method: "get" }));
|
||||
expect(decision).toMatchObject({ track: true });
|
||||
@@ -79,16 +73,17 @@ describe("decidePageview", () => {
|
||||
expect(decision).toEqual({ track: false, reason: "non-get-method" });
|
||||
});
|
||||
|
||||
it("skips a router prefetch", () => {
|
||||
const decision = decidePageview(humanRequest({ isPrefetch: true }));
|
||||
expect(decision).toEqual({ track: false, reason: "prefetch" });
|
||||
});
|
||||
|
||||
it("skips a prefetch even when the agent is a bot", () => {
|
||||
const decision = decidePageview(
|
||||
humanRequest({ isPrefetch: true, userAgent: "curl/8.1.2" }),
|
||||
);
|
||||
expect(decision).toEqual({ track: false, reason: "prefetch" });
|
||||
it.each([
|
||||
["a router prefetch payload", "empty"],
|
||||
["a script subresource", "script"],
|
||||
["an image subresource", "image"],
|
||||
["a request without fetch metadata", ""],
|
||||
])("skips %s", (_label, secFetchDest) => {
|
||||
const decision = decidePageview(humanRequest({ secFetchDest }));
|
||||
expect(decision).toEqual({
|
||||
track: false,
|
||||
reason: "non-navigation-request",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -135,4 +130,11 @@ describe("decidePageview", () => {
|
||||
const decision = decidePageview(humanRequest({ userAgent }));
|
||||
expect(decision).toEqual({ track: false, reason: "bot-user-agent" });
|
||||
});
|
||||
|
||||
it("skips a bot even when its request looks like a navigation", () => {
|
||||
const decision = decidePageview(
|
||||
humanRequest({ userAgent: "curl/8.1.2", secFetchDest: "document" }),
|
||||
);
|
||||
expect(decision).toEqual({ track: false, reason: "bot-user-agent" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user