feat(core)!: track non-navigation requests as pageViews#45
Merged
Conversation
d98b3ff to
f3a225b
Compare
The middleware only auto-tracked browser page navigations, so every other request was dropped — Route Handler responses and requests from non-browser clients (crawlers, scripts, server-to-server) were never counted, even when they were real content views. Track every request as a pageView except browser-initiated sub-requests (RSC soft-navigations, XHR, fetch, subresources), identified by Sec-Fetch-Dest being present and not "document". Browsers always send Sec-Fetch-* (it can't be spoofed); non-browser clients omit it, so they get tracked while soft navigations are skipped and counted client-side via /api/event instead — no duplicates. On non-API routes only reads (GET/HEAD) and document navigations count as pageViews, so a webhook or programmatic POST to a Route Handler isn't mistaken for one. API paths still flow through to apiCall for any method. Gating on Sec-Fetch-Dest rather than an RSC header is deliberate: in Next 15.5+ the soft-nav RSC fetch carries no reliable rsc/next-url header. BREAKING CHANGE: non-navigation requests are now tracked as pageViews by default. Use excludePaths to opt specific paths out.
f3a225b to
4c60052
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The middleware only auto-tracked browser page navigations. Every other request was dropped —
Route Handler responses and requests from non-browser clients (crawlers, scripts, server-to-server)
were never counted, even when they were real content views.
This tracks every request as a
pageViewexcept browser-initiated sub-requests (RSCsoft-navigations, XHR,
fetch(), subresources).How
The discriminator is
Sec-Fetch-Dest:Sec-Fetch-*(it's a forbidden header, so it can't be spoofed). A sub-requesthas
Sec-Fetch-Destpresent and ≠document→ skip (it's a soft nav / XHR, already trackedclient-side via
/api/event, so counting it here would double-count).Sec-Fetch-*entirely → track. Verified: Claude Code's
WebFetchand OpenAI's Codex CLI both fetch withoutSec-Fetch-*, so agent access to content is recorded.Sec-Fetch-Dest: document) → track as before.apiCallpath (subject toexcludeApiCalls).On non-API routes, only reads (GET/HEAD) and document navigations become pageViews — a webhook or
programmatic
POST/PUTto a Route Handler is not a page view and is skipped. API paths still flowthrough for any method.
getRequestInfonow exposesisDocumentRequestandisBrowserSubrequest.Why not gate on the RSC header?
The original skip relied on
!isPageNavigation. Gating the new behavior on anrsc/next-urlheaderdoes not work: in Next 15.5+ the soft-navigation RSC fetch carries no reliable RSC header — it
arrives as
sec-fetch-dest: empty,accept: */*, norsc/next-url. The e2e canary caught this(navigation went 2→3 pageViews).
Sec-Fetch-Destis the reliable signal.Tests
pageViewat its path; a browsersub-request (
Sec-Fetch-Dest: empty) is not; a non-GET (POST) to a non-API route is not.pageViews.length === 2) still pass — no duplicates.packages/website) gets a smallexcludePathsto keep its own data clean.Non-navigation requests are now tracked as
pageViewsby default. This increases tracked volume:non-browser traffic on non-API paths (uptime monitors, health checks, scrapers, security scanners) now
counts, alongside the agent/crawler traffic this is meant to capture. Existing human metrics are
unchanged (no data loss; page navigations still fire exactly one pageView). Consumers that want to drop
specific machine/asset paths should use
excludePaths.🤖 Generated with Claude Code