Files
mintel.me/apps/web/src/domain/pageview-decision.spec.ts
T
mmintel ed35413a44
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
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.
2026-10-04 12:51:54 +02:00

141 lines
4.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
decidePageview,
type IncomingPageviewRequest,
} from "./pageview-decision";
const humanRequest = (
overrides: Partial<IncomingPageviewRequest> = {},
): IncomingPageviewRequest => ({
method: "GET",
secFetchDest: "document",
pathname: "/",
search: "",
hostname: "mintel.me",
referrer: "",
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",
...overrides,
});
describe("decidePageview", () => {
it("tracks a browser navigation for the homepage", () => {
const decision = decidePageview(humanRequest());
expect(decision).toEqual({
track: true,
facts: {
url: "/",
hostname: "mintel.me",
referrer: "",
language: "de-DE",
userAgent: humanRequest().userAgent,
clientIp: "84.117.2.55",
},
});
});
it("keeps the query string in the tracked url", () => {
const decision = decidePageview(
humanRequest({ pathname: "/imprint", search: "?from=footer" }),
);
expect(decision).toMatchObject({
track: true,
facts: { url: "/imprint?from=footer" },
});
});
it("tracks a blog-style slug that merely contains a file-like segment", () => {
const decision = decidePageview(
humanRequest({ pathname: "/blog/next.js-vs-remix" }),
);
expect(decision).toMatchObject({ track: true });
});
it("tracks a slug that merely starts like a metadata image route", () => {
const decision = decidePageview(
humanRequest({ pathname: "/notes/opengraph-image-deep-dive" }),
);
expect(decision).toMatchObject({ track: true });
});
it("accepts the method in lower case", () => {
const decision = decidePageview(humanRequest({ method: "get" }));
expect(decision).toMatchObject({ track: true });
});
it.each([
["a form submission", { method: "POST" }],
["a head request", { method: "HEAD" }],
["an options probe", { method: "OPTIONS" }],
])("skips %s", (_label, overrides) => {
const decision = decidePageview(humanRequest(overrides));
expect(decision).toEqual({ track: false, reason: "non-get-method" });
});
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([
["a next build chunk", "/_next/static/chunks/main-abc123.js"],
["a next image request", "/_next/image"],
["a public image", "/images/portrait.webp"],
["a jpeg photo", "/images/photo.jpg"],
["a woff font", "/fonts/inter.woff"],
["a font", "/fonts/inter.woff2"],
["a feed document", "/rss.xml"],
["the sitemap", "/sitemap.xml"],
["the robots file", "/robots.txt"],
["the web manifest", "/manifest.webmanifest"],
["an open graph image route", "/opengraph-image"],
["a twitter image route", "/twitter-image"],
["a favicon", "/favicon.ico"],
])("skips %s at %s", (_label, pathname) => {
const decision = decidePageview(humanRequest({ pathname }));
expect(decision).toEqual({ track: false, reason: "static-asset" });
});
it.each([
["the analytics proxy", "/stats/api/send"],
["the error relay", "/errors/api/relay"],
["an api route", "/api/contact"],
["the api health check", "/api/health"],
["the health check", "/health"],
["the gatekeeper", "/gatekeeper/api/verify"],
["a well-known challenge", "/.well-known/acme-challenge/token"],
])("skips %s at %s", (_label, pathname) => {
const decision = decidePageview(humanRequest({ pathname }));
expect(decision).toEqual({ track: false, reason: "infrastructure-path" });
});
it.each([
["curl", "curl/8.1.2"],
["wget", "Wget/1.21"],
["python requests", "python-requests/2.31"],
["go http", "Go-http-client/2.0"],
["uptime kuma", "Uptime-Kuma/1.23.11"],
["a crawler", "Googlebot/2.1 (+http://www.google.com/bot.html)"],
["a link preview", "WhatsApp/2.23.20.0"],
])("skips the %s user agent", (_label, userAgent) => {
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" });
});
});