Skip to content

feat(core)!: track non-navigation requests as pageViews#45

Merged
vklimontovich merged 1 commit into
mainfrom
feat/track-non-navigation-routes
Jun 6, 2026
Merged

feat(core)!: track non-navigation requests as pageViews#45
vklimontovich merged 1 commit into
mainfrom
feat/track-non-navigation-routes

Conversation

@vklimontovich
Copy link
Copy Markdown
Collaborator

@vklimontovich vklimontovich commented Jun 6, 2026

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 pageView except browser-initiated sub-requests (RSC
soft-navigations, XHR, fetch(), subresources).

How

The discriminator is Sec-Fetch-Dest:

  • Browsers always send Sec-Fetch-* (it's a forbidden header, so it can't be spoofed). A sub-request
    has Sec-Fetch-Dest present and ≠ documentskip (it's a soft nav / XHR, already tracked
    client-side via /api/event, so counting it here would double-count).
  • Non-browser clients (crawlers, scripts, coding agents, server-to-server) omit Sec-Fetch-*
    entirely → track. Verified: Claude Code's WebFetch and OpenAI's Codex CLI both fetch without
    Sec-Fetch-*, so agent access to content is recorded.
  • Hard document navigations (Sec-Fetch-Dest: document) → track as before.
  • API paths are exempt from the skip, so they still flow to the apiCall path (subject to
    excludeApiCalls).

On non-API routes, only reads (GET/HEAD) and document navigations become pageViews — a webhook or
programmatic POST/PUT to a Route Handler is not a page view and is skipped. API paths still flow
through for any method.

getRequestInfo now exposes isDocumentRequest and isBrowserSubrequest.

Why not gate on the RSC header?

The original skip relied on !isPageNavigation. Gating the new behavior on an rsc/next-url header
does not work: in Next 15.5+ the soft-navigation RSC fetch carries no reliable RSC header — it
arrives as sec-fetch-dest: empty, accept: */*, no rsc/next-url. The e2e canary caught this
(navigation went 2→3 pageViews). Sec-Fetch-Dest is the reliable signal.

Tests

  • New e2e: a direct non-navigation Route Handler GET is tracked as a pageView at its path; a browser
    sub-request (Sec-Fetch-Dest: empty) is not; a non-GET (POST) to a non-API route is not.
  • Existing soft-navigation canaries (pageViews.length === 2) still pass — no duplicates.
  • Full matrix green: 44/44 e2e (next15 + next16 × app + pages), 33/33 unit.
  • Demo site (packages/website) gets a small excludePaths to keep its own data clean.

⚠️ Breaking / operational note

Non-navigation requests are now tracked as pageViews by 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

@vklimontovich vklimontovich force-pushed the feat/track-non-navigation-routes branch 2 times, most recently from d98b3ff to f3a225b Compare June 6, 2026 23:09
@vklimontovich vklimontovich marked this pull request as ready for review June 6, 2026 23:10
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.
@vklimontovich vklimontovich force-pushed the feat/track-non-navigation-routes branch from f3a225b to 4c60052 Compare June 6, 2026 23:13
@vklimontovich vklimontovich merged commit 45e097d into main Jun 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant