Summary
Under Solid's observe tier, a navigation performed by clicking an <a> records no interaction: NavigationEvent.interaction is undefined, and the calls the route's data makes carry a navigation origin with no click behind it. A navigation performed by navigate() from an onClick handler is attributed correctly. The difference is where the handler runs: the router attaches its anchor handler to document directly, outside the web runtime's interaction frame.
Where
src/data/events.ts, setupNativeEvents:
// ensure delegated event run first
delegateEvents(["click", "submit"]);
document.addEventListener("click", handleAnchorClick);
if (preload) {
document.addEventListener("mousemove", handleAnchorMove, { passive: true });
document.addEventListener("focusin", handleAnchorPreload, { passive: true });
document.addEventListener("touchstart", handleAnchorPreload, { passive: true });
}
document.addEventListener("submit", handleFormSubmit);
@solidjs/web wraps every handler it dispatches — delegated events via eventHandler, runtime-attached ones via addEvent — in dispatchAsInteraction, i.e. OBSERVE.attribution.withInteraction({ type, target, at }, fn), so every root write inside the handler (the router's location write included) is attributed to that interaction. A raw document.addEventListener goes around that wrapper. handleAnchorClick then calls navigateFromRoute, whose withOrigin(describeNavigation…) frame opens with no enclosing interaction, so the record is an orphan.
handleFormSubmit has the same shape: an action submitted through a native <form> is unattributed, while one submitted from a handler is not.
Observed
With @sentry/solid-2 consuming the records (e2e app on rc.13 / next.32, / → /users/6):
<button onClick={() => navigate('/users/6')}>: NavigationEvent.interaction is the click; the click span links to the route-named navigation span.
<a href="/users/6">: NavigationEvent.interaction === undefined; there is no click span at all, since no interaction frame ever opened. The navigation and its server-function call stand alone. The INP-relevant facts on the interaction record (inputDelayMs, handlerMs, settledMs) are lost for every link click in the app — which is most navigations.
The consumer documents the caveat for now rather than guessing a parent by time.
Cause
The direct document listeners date from the ordering requirement noted in the comment — Solid's delegated click must run first so a component's onClick with preventDefault() can stop the navigation — and predate the runtime having an interaction frame to run under. That ordering still has to hold; the fix is to run the handler inside the frame, not to change when it runs.
Options
- Router wraps its handlers. In
setupNativeEvents, when OBSERVE is defined, dispatch handleAnchorClick and handleFormSubmit through OBSERVE.attribution.withInteraction({ type: evt.type, target: …, at: evt.timeStamp }, () => handler(evt)). Smallest change, but it duplicates the web runtime's describeEventTarget (element description, the values privacy gate for text) and interactionStart (the timeStamp clock check), which the router should not own.
- Web exposes the frame.
@solidjs/web gains a small public entry — a listener attach that wraps under observe and is the identity function otherwise (addEvent(node, name, handler, false) already does exactly this but is marked @internal, compiler-emitted). The router uses it for the click and submit listeners. Preload listeners (mousemove, focusin, touchstart) stay raw: preloading is not a user interaction and should not open frames.
- Web stamps
document-level listeners itself. Not viable without patching EventTarget.prototype; not proposed.
(2) is the right shape: the web runtime owns interaction description, the router only needs "run this as the event's handler". It is a public-surface addition on @solidjs/web, so it needs a decision there first; (1) can land in the router alone if that is preferred short-term.
Either way the order relative to Solid's delegated handler must not change: attach after delegateEvents(["click", "submit"]) exactly as today, so evt.defaultPrevented still reflects component handlers.
Acceptance
- An anchor click that navigates produces a
NavigationEvent whose interaction is the click's InteractionRef (type click, target a#…), the same object identity a navigate() from onClick gets today.
- A
<form> submitted to an action produces an interaction of type submit on the action's records.
- A component
onClick calling preventDefault() still stops the anchor navigation.
- Nothing changes in non-observe builds (the wrapper folds to the plain listener).
Related: solidjs/solid#3683 (route declaration to the observe tier), getsentry/sentry-javascript#24517 (consumer; the caveat is documented in docs/solid-2-observe.md there).
— Claude via Cursor
Summary
Under Solid's observe tier, a navigation performed by clicking an
<a>records no interaction:NavigationEvent.interactionisundefined, and the calls the route's data makes carry a navigation origin with no click behind it. A navigation performed bynavigate()from anonClickhandler is attributed correctly. The difference is where the handler runs: the router attaches its anchor handler todocumentdirectly, outside the web runtime's interaction frame.Where
src/data/events.ts,setupNativeEvents:@solidjs/webwraps every handler it dispatches — delegated events viaeventHandler, runtime-attached ones viaaddEvent— indispatchAsInteraction, i.e.OBSERVE.attribution.withInteraction({ type, target, at }, fn), so every root write inside the handler (the router's location write included) is attributed to that interaction. A rawdocument.addEventListenergoes around that wrapper.handleAnchorClickthen callsnavigateFromRoute, whosewithOrigin(describeNavigation…)frame opens with no enclosing interaction, so the record is an orphan.handleFormSubmithas the same shape: an action submitted through a native<form>is unattributed, while one submitted from a handler is not.Observed
With
@sentry/solid-2consuming the records (e2e app on rc.13 / next.32,/→/users/6):<button onClick={() => navigate('/users/6')}>:NavigationEvent.interactionis the click; the click span links to the route-named navigation span.<a href="/users/6">:NavigationEvent.interaction === undefined; there is no click span at all, since no interaction frame ever opened. The navigation and its server-function call stand alone. The INP-relevant facts on the interaction record (inputDelayMs,handlerMs,settledMs) are lost for every link click in the app — which is most navigations.The consumer documents the caveat for now rather than guessing a parent by time.
Cause
The direct
documentlisteners date from the ordering requirement noted in the comment — Solid's delegatedclickmust run first so a component'sonClickwithpreventDefault()can stop the navigation — and predate the runtime having an interaction frame to run under. That ordering still has to hold; the fix is to run the handler inside the frame, not to change when it runs.Options
setupNativeEvents, whenOBSERVEis defined, dispatchhandleAnchorClickandhandleFormSubmitthroughOBSERVE.attribution.withInteraction({ type: evt.type, target: …, at: evt.timeStamp }, () => handler(evt)). Smallest change, but it duplicates the web runtime'sdescribeEventTarget(element description, thevaluesprivacy gate for text) andinteractionStart(thetimeStampclock check), which the router should not own.@solidjs/webgains a small public entry — a listener attach that wraps under observe and is the identity function otherwise (addEvent(node, name, handler, false)already does exactly this but is marked@internal, compiler-emitted). The router uses it for theclickandsubmitlisteners. Preload listeners (mousemove,focusin,touchstart) stay raw: preloading is not a user interaction and should not open frames.document-level listeners itself. Not viable without patchingEventTarget.prototype; not proposed.(2) is the right shape: the web runtime owns interaction description, the router only needs "run this as the event's handler". It is a public-surface addition on
@solidjs/web, so it needs a decision there first; (1) can land in the router alone if that is preferred short-term.Either way the order relative to Solid's delegated handler must not change: attach after
delegateEvents(["click", "submit"])exactly as today, soevt.defaultPreventedstill reflects component handlers.Acceptance
NavigationEventwhoseinteractionis the click'sInteractionRef(typeclick, targeta#…), the same object identity anavigate()fromonClickgets today.<form>submitted to an action produces an interaction of typesubmiton the action's records.onClickcallingpreventDefault()still stops the anchor navigation.Related: solidjs/solid#3683 (route declaration to the observe tier), getsentry/sentry-javascript#24517 (consumer; the caveat is documented in
docs/solid-2-observe.mdthere).— Claude via Cursor