feat(observe): the route the document arrived on — NavigationRef.initial, NavigationRef.interaction, RenderEvent.route - #3683
Conversation
🦋 Changeset detectedLatest commit: 95429ec The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Coverage Report for CI Build 36458493477Coverage remained the same at 73.772%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance
Comparing Footnotes
|
1660836 to
9c3e1c1
Compare
|
…ial, NavigationRef.interaction, RenderEvent.route
A router declares its initial match with the call it already uses for
navigations, on both sides: `withOrigin({ kind: "navigation", initial:
true, … })` around the work that builds its context. Client: the engine
opens the frame at the time origin, takes no `from`, settles it on the
no-write rule — the first "navigation" record, `initial: true`, kept
out of the feedback fold. Server: the server entry's `withOrigin` files
the ref through the render context (`_declareRoute`) and the request's
"render" record carries `RenderEvent.route` — the `http.route` a
consumer names the request by.
`NavigationRef.interaction`: a router that awaits before writing hands
back the origin it captured with `currentOrigin()`; declared beats
ambient, the key's presence is the declaration.
Prod artifacts byte-identical; observe client artifacts byte-identical
(tier cap +25 B of mangler layout); engine +122 B.
Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
9c3e1c1 to
95429ec
Compare
Size (brotli, eager entry chunk)
Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps in |
The same withOrigin call the router makes for every navigation, around
the work that builds its context, with `initial: true` on the ref — on
both sides. Client: the first "navigation" record names the pageload's
route. Server: the request's "render" record carries RenderEvent.route.
`describeNavigation` and the new `describeInitial` share the match
description (`routeName`/`routeParams`).
Also the rc.10 moves the observe test and this file need: NavigationRef
is exported from `solid-js`, and `attribution.history("navigation")`
replaces `attribution.navigations()`.
Requires the solid release shipping NavigationRef.initial and
RenderEvent.route (solidjs/solid#3683).
Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The same withOrigin call the router makes for every navigation, around
the work that builds its context, with `initial: true` on the ref — on
both sides. Client: the first "navigation" record names the pageload's
route. Server: the request's "render" record carries RenderEvent.route.
`describeNavigation` and the new `describeInitial` share the match
description (`routeName`/`routeParams`).
Also the rc.10 moves the observe test and this file need: NavigationRef
is exported from `solid-js`, and `attribution.history("navigation")`
replaces `attribution.navigations()`.
Requires the solid release shipping NavigationRef.initial and
RenderEvent.route (solidjs/solid#3683).
Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Why
Every navigation record already carries the matched route pattern (
NavigationEvent.name) — except the first. Nothing writes a location on a fresh document, so the route the page loaded as was never declared, and on the server it was missing entirely: the request had a URL and no route. That is the one fact a consumer naming page loads and requests needs most — Sentry's Performance product (and every peer framework SDK) keys on the route-parameterised transaction name (/users/:id, sourceroute); without it one page scatters across as many names as it has parameters. The audits missed it because they asked what Solid can observe that React cannot, not what a framework SDK has to do; the plan records the miss and the process fix.What
One declaration a router makes with the call it already uses, on both sides:
initialframe opens at the time origin (atdefaults to0— the document's own navigation start, so the record joins Navigation Timing; a router mounted late passes its own), takes nofrom, and settlescommittedwithwrites: 0when the frame closes — the existing no-write rule, so the record is delivered as the router finishes describing the route.NavigationEvent.initial: true. It is a declaration, not a timing: kept out offeedback().navigations, whosesettledMsmeans "what the person waited".formatOrigin:initial navigation to /users/:id (/users/42).withOriginits one server meaning: aninitialref is filed through the render context (_declareRoute, the_recordBoundarypattern — installed by the renderer under the"render"record's gate) and the request's"render"record carriesRenderEvent.route({ name, to, params }, read from the ref at settle so a match refined during the render lands). A non-initial ref and a call outside a render arefn()as before. Two copies of the entry over one core each wrap once, reading their ownsharedConfig; only the rendering copy has a context.NavigationRef.interaction. A router that awaits between request and write (TanStack's load transaction) loses the click — the frame is gone when it publishes. It capturesOBSERVE.attribution.currentOrigin()in the request and hands it back on the ref; the record and every hold and re-run its write causes join the click as if the write had been synchronous. Declared beats ambient: the key's presence is the declaration (interaction: currentOrigin()with nothing in effect declares "for no interaction"); the stack is consulted only when the key is absent. This is what makes the contract complete for the async-writing class of router.Public API changes
@solidjs/signals:NavigationRef.initial?: boolean,NavigationRef.interaction?: ChangeOrigin(both accepted byOBSERVE.attribution.withOrigin);NavigationEvent.initial?: true;formatOriginprefixesinitialfor such a frame.solid-js(server entry):OBSERVE.attribution.withOrigingains behaviour forinitialrefs under a render (files the route); otherwise unchanged.@solidjs/web:RenderEvent.route?: RenderRoute; new exported typeRenderRoute { name?, to?, params? }(server and client entries, types only).Cost
signalsobserve core,solid.observe.js,web.observe.js) byte-identical; on the Rolldown harness (size: measure with Rolldown instead of size-limit/esbuild; re-base caps #3679) the tier scenario moves +6 B of shared-mangler layout (16,336 → 16,342 B againstnext@ 61a33af) — cap 16.34 → 16.35 KB with the note.initial/interactionhandling and theformatOriginprefix — cap 30.52 → 30.54 KB. Every prod scenario byte-identical.originStartruns once per navigation); the engine-cost tripwire passes.Tests
attribution-navigation.test.ts+8: initial declaration (time origin, nofrom, no interaction,writes: 0,committed, ref re-read at settle, router-suppliedat, feedback fold exclusion, no-engine passthrough);interaction(async-write join composing with feat(signals): interactions wait for the handler's promise; UNTRACKED_ASYNC_HANDLER #3604's open interaction, declared-beats-ambient, declared-none).packages/web/test/server/server-render-route.spec.tsx(9):renderToString/renderToStream, settle-time re-read, later declaration replaces, only given fields, non-initial declares nothing, no router → noroute, no listener → plain call, slot members intact.observe.type-tests.tscoversRenderRoute.Note:
packages/webserver suite has 19 pre-existing failures on cleannextin this checkout (owner-path/sourceNamesshapes — same set with and without this change); none touch these files.Docs
RFC 08: navigation section (
initial,interaction, the "first navigation has no write" rule), the"render"record'sroute, thewithOriginsnippet. Skill:history("navigation")note. Plan: the parity miss, what closed it, and the audit process fix.Follow-ups (separate PRs)
@solidjs/router:createRouterContextdeclares the initial match (draft, pending this release).@sentry/solid-2: rename the pageload root from the initial record; drive the navigation idle span from records;http.routefromrender.route(amends #24517 once released).— Claude via Cursor
Made with Cursor