Skip to content

feat: declare navigations to Solid's observe tier (withOrigin) - #601

Merged
ryansolid merged 2 commits into
nextfrom
observe-navigation-origin
Sep 11, 2026
Merged

feat: declare navigations to Solid's observe tier (withOrigin)#601
ryansolid merged 2 commits into
nextfrom
observe-navigation-origin

Conversation

@ryansolid

@ryansolid ryansolid commented Sep 10, 2026

Copy link
Copy Markdown
Member

Depends on OBSERVE.attribution.withOrigin from solidjs/solid#3332, released in solid-js 2.0.0-rc.8; the last commit moves the dev/peer ranges and lockfile to rc.8. Against the published packages: 401/401 client tests, 35/35 server tests, type tests clean. Ready for review.

What

Every client location write — navigate(), a redirect hop chased while the previous target is still pending, the browser's own back/forward — now runs inside OBSERVE.attribution.withOrigin(ref, write), declaring the navigation to Solid's attribution engine. One seam, in createIntegration's setter, since every client write passes through it.

The ref carries:

  • name — the parametrized route pattern of the leaf match (/users/:id; / for the root route, whose pattern is "")
  • params — merged params of the match chain
  • to / from — the target and the committed location (which read() still holds while a navigation is pending, so a hop's from is right too)
  • redirect_navigation - 1 when _navigation > 1, i.e. the router's own redirect depth becomes the engine's hop index

name and params are getters. The engine re-reads the ref when the navigation settles, so a lazy route subtree that loaded during the hold names the exact route it resolved to instead of its placeholder (/plugins/widgets/:id, not /plugins/*). No refine call, no plumbing between createRouterContext and the integration — the late-read contract does the work.

Also names the location signal (location) and the matches, routingPending, and lazy-subtree (lazyRoutes) memos, so diagnostics show them as themselves rather than signal / computed (acknowledgedBy: ["isPending:location"], heldWrites: ["location"]).

What the engine then gives

  • attribution.navigations() records per navigation: route name, to/from/params, writes, outcome (committed | held | superseded), settledMs, the HoldEvent when held, redirects: [{ name, to, at }] for hops
  • Holds and re-runs caused by the navigation report origin = navigation to /users/:id (/users/42), and redirected from /files when a hop was involved
  • feedback().navigations ranks routes by time spent held navigating to them, with silent / superseded / redirected counts
  • Navigation timing starts at the user event that enclosed the write, not the write — the click, through the router's async guard, to settle

Production

Nothing. OBSERVE is undefined outside the observe/dev builds, and OBSERVE ? … : commit() folds out.

Tests

test/observe-navigation.spec.tsx: pattern/params/from on a plain navigation; root named /; a query redirect thrown while pending recorded as one navigation with writes: 2 and redirects: [/files]; lazy subtree named by its resolved route at settle; history.go(-1) declared.

Notes from the spike that motivated this

Observed with a Sentry adapter prototype over the observe tier (findings list to follow in the solid repo):

  • The router's data-pending link claim reads isPending(location) from an effect, so a held navigation started from a link always counts as acknowledged by the engine's hold census. That is by design: the claim is the router's hook for attaching a pending affordance to the link that started the navigation (in particular when the source sits in a non-hydrated region and nothing else can react), so it is an affordance, not a loophole — no opt-out. Under this router SILENT_HOLD for a navigation therefore fires only for a programmatic navigate() with no claimed link and no other indicator, which is the case that deserves the report.
  • Optional params are string | undefined; NavigationRef.params is Record<string, string>. Cast here; loosening the core type is the right fix.

Every client location write — navigate(), a redirect hop chased while the
previous target is pending, the browser's own back/forward — now runs inside
OBSERVE.attribution.withOrigin with the parametrized route pattern, params,
and origin location. The attribution engine names holds and re-runs after the
route, times the navigation from the user event to settle, folds redirect
hops onto the navigation they belong to (redirect depth from _navigation),
and reports routes in feedback().navigations.

The ref's name and params are getters read at settle, so a lazy subtree that
resolved during the hold names the exact route rather than its placeholder.
The location signal and the matches / routingPending / lazyRoutes memos are
named so diagnostics read as themselves instead of `signal` / `computed`.

Production builds are untouched: OBSERVE is undefined there and the
declaration folds out. Requires solid-js 2.0.0-rc.8.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3753889

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/router Patch

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

OBSERVE.attribution.withOrigin shipped in rc.8; dev/peer ranges and the
lockfile move with it. 401 client + 35 server tests, types clean, against
the published packages.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedsolid-js@​2.0.0-rc.7 ⏵ 2.0.0-rc.81001009596100

View full report

@ryansolid
ryansolid marked this pull request as ready for review September 11, 2026 16:35
@ryansolid
ryansolid merged commit 8585d2d into next Sep 11, 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