Skip to content

feat(solid-2): add @sentry/solid-2 — Solid 2 SDK (client + server) - #24517

Draft
ryansolid wants to merge 6 commits into
getsentry:developfrom
ryansolid:feat/solid-2
Draft

ryansolid wants to merge 6 commits into
getsentry:developfrom
ryansolid:feat/solid-2

Conversation

@ryansolid

@ryansolid ryansolid commented Sep 18, 2026

Copy link
Copy Markdown

A Sentry SDK for Solid 2 (solid-js ^2.0.0-rc.9). One package covers every Solid 2 project — client and server, any router, any host — because Solid 2 moved the things an SDK needs into the runtime itself. There is no SolidStart for Solid 2 and there is nothing in this package for a router or a host: the runtime has error hooks on both platforms, a structured observability channel (OBSERVE.records), an attribution engine that names the user interaction and the route behind every write (routers declare navigations to it with withOrigin; the SDK never sees router code), and a server-side trace-context slot whose answer the runtime carries to the browser on its own (Server-Timing, the <meta> pair). So where @sentry/solid plus @sentry/solidstart needed a router integration, a middleware to rewrite HTML, and a --import preload per host, this is one init() on each side that subscribes.

The server half's one requirement is @sentry/node's, not Solid's: init() must run before the app's server graph loads so OpenTelemetry can patch node:http and friends. Each host does that its own way — node --import, a first import in a CJS entry; for apps served by @solidjs/vite-plugin's handler, its start.instrument option is that preload, since ESM hoisting defeats "import it first" there. The unit tests run the server integrations against bare renderToStream with no plugin involved; the e2e app is a plugin-hosted app because that is the reference deployment and it exercises start.instrument.

Errors report in every Solid build tier through the two hooks, once per error object, with where it was thrown and where it was met. Tracing is opt-in and needs Solid's observe build (the observe export condition plus the compiler's componentNames; solid({ observe: true }) sets both): one root span per user interaction with its navigations, holds and server-function calls as children (joined by the engine's object identity, not by time); server-function executions and waiting <Loading> boundaries as spans under OTel's http.server; the browser pageload continuing the server trace with no middleware.

Decisions worth a look: records set span status only and never capture — the same error reaches a hook, and Sentry's once-per-object guard made whichever ran first win (the e2e app caught the record misreporting a handled boundary catch as an unhandled crash). Element text from interaction targets is stripped unless targetText: true. A call that lands after its interaction settled (onClick={async () => set(await call())}) is parented under the ended interaction span, marked after_settle. Interactions are root spans rather than children of the pageload idle span; open to changing that.

The package name is a working one — @sentry/solid-2 vs. a new major of @sentry/solid still needs a decision, and nothing in the code depends on it.

docs/solid-2-observe.md is the reviewer's brief on what the runtime exposes. The e2e app is marked optional until the CI matrix has run it.

ryansolid and others added 6 commits September 18, 2026 12:16
One package, both halves: the browser SDK for the client and the Node SDK
for the server, resolved by export condition. `init()` on each side adds
Solid 2's error hook to the platform defaults — every failure a boundary
renders a fallback for (client), and every failure the server runtime
handles or fails on (server), reported once with where it was thrown and
where it was met. Tracing is opt-in and reads Solid's observe tier: one
root span per user interaction with its navigations, holds and
server-function calls as children (calls joined by the engine's own
interaction frame, not by time); a trace provider on `OBSERVE.server.trace`
so the runtime carries Sentry's trace to the browser on its own carriers;
spans for server-function executions, waiting `<Loading>` boundaries and
frame streams; the runtime's diagnostics as issues fingerprinted by code
and component path.

Element text on interaction targets is user data and left out unless
`targetText: true`; a finding's `data.error` is not forwarded as an issue
extra. Sibling package to `@sentry/solid` (1.x) for the transition; peer
`solid-js`/`@solidjs/web` ^2.0.0-rc.8.

Local dev dependencies are `link:` to a local Solid checkout until an rc
carrying the record shapes and error hooks this builds on is published.

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Swap the local `link:` development dependencies for the published rc.9 —
the first release carrying the record shapes, the error hooks and the
observe server build the package relies on — and rebuild the lockfile.

The tests' Solid inlining moved from Vite's `server.deps` to Vitest's
`test.server.deps`, where it belongs: the linked checkout had masked the
mistake (paths outside node_modules inline by default), and against the
installed packages Node loaded `@solidjs/signals` natively beside the
copy Vite inlined — two `OBSERVE`s, and a server half that looked like the
wrong tier. The package roots are now resolved from this package rather
than assumed hoisted.

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ion settled is still its child

Two things the e2e app surfaced against the published rc.9. The record
spans captured the errors they carried (`live.error`), which duplicated the
error hooks: the same error object reaches both, and whichever runs first
wins Sentry's once-per-object guard — the invocation record beat the server
hook and reported a handled boundary catch as an unhandled server-function
crash with the wrong mechanism. The hooks are the one error path; records
set the span's status.

`onClick={async () => set(await call())}` makes no synchronous write, so the
interaction settles as `idle` before the call it dispatched lands; the call
carries the interaction's frame but arrived after the claim window and
became a root span. It is now a child of the interaction's span by the same
identity, marked `solid.server_function.after_settle`.

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…hrough Playwright

A Solid 2 app on @solidjs/vite-plugin's start mode with `observe: true`,
the server SDK through `start.instrument`, the browser SDK from the app's
client module, and a bare node:http host. Against the packed tarballs it
proves what the units cannot: boundary and invocation spans under OTel's
http.server span, the pageload continuing the server trace with no
middleware, a click's server-function call becoming its child through a
real fetch, and both error hooks firing once with component paths.

Two findings folded into the README and the reviewer brief: a module both
graphs reach must import the explicit client/server entry, and the app
should declare @sentry/node so the plugin's inlining of this package does
not bundle it too (import-in-the-middle cannot find itself from a bundle).

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…one host's preload

Co-Authored-By: Claude via Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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