feat(solid,web,signals): server diagnostics on OBSERVE.diagnostics; OBSERVE.server owned by solid-js's server entry - #3433
Merged
Conversation
…BSERVE.server owned by solid-js's server entry
The server runtime reports on the same structured channel as the client
(server-dev-build-plan P1 + P2, one PR). Two helper modules hold the gates
— packages/solid/src/server/diagnostics.ts and packages/web/src/diagnostics.ts
— with three verbs: emitFinding (observe + dev: emit, and in dev report
through DEV.report), recordFinding (emit only, for a site that throws its
message), devCheck (emitFinding behind the dev gate). Call sites are wrapped
in literal "_SOLID_OBSERVE_" / "_SOLID_DEV_" tests so Rollup drops the
message strings from the tiers below; the prod server artifacts carry none
of it.
Findings (observe + dev): SSR_RENDER_ERROR_CONTAINED (data.handling:
fallback from createErrorBoundary, client/failed from the Loading boundary's
routing, failed from the root — the structured face of renderToStream's
onError, on the process-wide channel), SSR_SUBTREE_ABANDONED,
SSR_STREAM_ABANDONED (consumer | sink), LATE_HEADER_WRITE (recorded beside
the dev throw / prod log), SERVER_FN_ERROR_SANITIZED (the original the wire
replaced), FRAME_MARKER_CORRUPTED (frames client).
Checks (dev): every server console.warn becomes a code — SERVER_WRITE,
REVEAL_IN_RENDER_TO_STRING, LAZY_ASSET_UNMAPPED, PRELOAD_DESCRIPTOR_INVALID,
HEAD_TAG_INVALID, BEHAVIOR_CLAIM_DROPPED, UNRECOGNIZED_INSERT_VALUE (one code
and a new `render` kind on both platforms; the client's console.warn is
converted too). ASYNC_OUTSIDE_LOADING_BOUNDARY on the server records with
data.side: "server" before it throws. DiagnosticKind gains ssr, head, render.
ownerPath: the server createComponent runs the body under a transparent
owner labelled <Name> in observe/dev (createComponentOwner — no hydration id
consumed, no ownerCreations tick); the core's ownerPath walk reads
SSROwner._parent/_name unchanged. The repair-guide console footer moves to
src/console-footer.ts and both entries install it, so a server render's
[SERVER_WRITE] points at the same skill section. The server entry also
exports the Dev/Observe/Diagnostic* types the client entry does.
OBSERVE.server (C0/C1 follow-ups J.23/J.24): the invocation listener set and
trace-provider slot are created by solid-js's server entry
(src/server/observe.ts), once per process under
Symbol.for("solid-js/observe/server") on globalThis — not by web's module
init (an observer's init() importing only solid-js found the slot empty) and
not by the signals core (one artifact per tier for both platforms; a draft
there cost the client observe tier 138 B). The core keeps `server: {}`; web
reads the containers by the registered names.
Docs: RFC 08 "Server rendering" reference + quick-reference rows, the
corrected OBSERVE.server ownership paragraph; reactivity-diagnostics skill
gains a section per server code; plan doc records P1/P2 landed and the
compiler gap (SSR output inlines Comp(props); labels need createComponent
emitted under componentNames — separate PR).
Specs: solid/test/server/{server-diagnostics,observe-server,console-footer}.spec.ts,
web/test/server/server-diagnostics.spec.tsx, web/test/frames-marker-corruption.spec.tsx,
a client UNRECOGNIZED_INSERT_VALUE case in web/test/dev-warning.spec.tsx.
Size: observe CSR 15493 B (cap 15550), attribution 27093 B (cap 27160).
Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: bae6b06 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 34882893492Coverage remained the same at 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
ryansolid
added a commit
that referenced
this pull request
Sep 14, 2026
…Scope swaps the id-bearing owner (#3441) The SSR generate honours `componentNames` the way `dom` does. Under the option both compilers keep the `createComponent` call they otherwise inline to `Comp(props)` and pass the source tag text — `_$createComponent(Comp, props, "Comp")` — so the server runtime's observe/dev `createComponent` (a transparent, labelled owner around the body, #3433) labels every compiled component and a server finding's `ownerPath` reads `<App> › <Page>` like the client's. Without the option SSR still inlines: prod output is byte-identical. Universal and dynamic stay unlabelled. `@solidjs/vite-plugin` already passes the option for its dev and observe postures for every generate, so app server builds pick this up with no config change. Oxc's `jsx_tag_name` is now a shared, closure-based helper so the SSR lowering reuses it (`this.Row` through the `_self$` pre-pass, member tags). The `ssrInert` fixture becomes `ssr`, widened to the DOM fixture's shapes; the Oxc suite pins that SSR without the option imports no `createComponent`. Compiling the web server suite with the option (vite.config.server.mjs — the dev posture) surfaced a latent `ssrScope` bug: the virtual hole scope swapped the CURRENT owner's `id`/`_childCount`, but content inside a hole resolves ids by walking up past transparent owners, so with one in between the reserved slot was invisible and the hole's content took fresh ids from the enclosing counter (`_hk=3` where the client expects `_hk=10`). Reachable in prod today through `runInServerComponentScope` (transparent), ubiquitous once every component body runs under a labelled owner. The scope now swaps the nearest id-bearing owner (solid/test/server/ssr-scope.spec.ts covers a plain transparent owner, the server-component scope, the labelled component owner, and nesting). server-diagnostics.spec.tsx drops its `mount()` workaround for plain JSX; boundaries are compiled components too, so a boundary's finding now locates as `<App> › <Errored>` / `<App> › <Loading>` — as on the client. Server, hydrate and client suites pass under the native compiler and JSX_COMPILER=babel. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The server runtime reports on
OBSERVE.diagnostics— the channel the client already uses — andOBSERVE.server's objects move tosolid-js's server entry. Server-dev-build-plan P1 + P2 in one PR, with the two C0/C1 follow-ups from the Sentry spike (J.23/J.24) folded in.SSR_RENDER_ERROR_CONTAINED— a render error a boundary routed;data.handlingisfallback(<Errored>rendered its fallback),client(the<Loading>fragment rejected, the client re-renders it), orfailed(nothing contained it, the request failed). The structured face of whatrenderToStream's per-callonErrorreceives, on the process-wide channel — a fallback that renders on every request is visible without anyone watching a 200.SSR_SUBTREE_ABANDONED— a failed fragment's pending descendants discarded (data.fragments,data.serialized).SSR_STREAM_ABANDONED— consumer cancelled or sink failed mid-render (data.reason,data.pendingFragments,data.shellFlushed).LATE_HEADER_WRITE— recorded beside the existing dev throw / prod log.SERVER_FN_ERROR_SANITIZED— the original error the production wire replaced (data.error).FRAME_MARKER_CORRUPTED— the frames client's range check, on the same channel.console.warnbecomes a code —SERVER_WRITE,REVEAL_IN_RENDER_TO_STRING,LAZY_ASSET_UNMAPPED,PRELOAD_DESCRIPTOR_INVALID,HEAD_TAG_INVALID(data.reasonnames the rule),BEHAVIOR_CLAIM_DROPPED,UNRECOGNIZED_INSERT_VALUE. The last is one code and a newrenderkind on both platforms; the client'sconsole.warninclient.tsis converted too.ASYNC_OUTSIDE_LOADING_BOUNDARYon the server records withdata.side: "server"before it throws.ownerPathon the server.createComponentruns the body under a transparent owner labelled<Name>in observe/dev (createComponentOwner— no hydration id consumed, noownerCreationstick), and the core's existingownerPathwalk readsSSROwner._parent/_nameunchanged, soin <App> › <Page>reads the same on both sides.OBSERVE.serverowned bysolid-js's server entry. The invocation listener set and the trace-provider slot are created inpackages/solid/src/server/observe.ts, once per process underSymbol.for("solid-js/observe/server")onglobalThis. An observer'sinit()that imports onlysolid-jscan subscribe andprovidebefore@solidjs/webhas loaded (J.23), and a host that bundles the runtime into its server build and instruments through a--imported module finds one listener set and one provider across both copies (J.24). Web reads the containers by the registered names; the core keepsserver: {}.src/console-footer.tsand both entries install it, so a server render's[SERVER_WRITE]points at the same skill section. The server entry also exports theDev/Observe/Diagnostic*types the client entry does.Design notes
packages/solid/src/server/diagnostics.tsandpackages/web/src/diagnostics.ts:emitFinding(observe + dev — emit, and in dev report throughDEV.report),recordFinding(emit only, for a site that throws its message — the throw is the console face, and the core lands the once-per-code footer beneath it),devCheck(emitFindingbehind the dev gate). Call sites are additionally wrapped in a literalif ("_SOLID_OBSERVE_")/if ("_SOLID_DEV_")so Rollup drops the message strings from the tiers below; the prod server artifacts carry none of it (asserted by artifact scans in the specs).serverSlots()indev.tsso any importer found them. The core has one artifact per tier for both platforms, so the client observe tier paid 138 B for server-only containers.solid-js's server entry is the right owner: the J.23 contract is "an observer that imports onlysolid-js", and that entry is server-only. Client cost: zero.renderkind.UNRECOGNIZED_INSERT_VALUEfires from the client'sinsertas well as the server's;ssrwas the wrong kind for it, and no existing kind covered "the renderer's insert positions".DiagnosticKindgainsssr,head,render.Known gap (separate PR)
The SSR compiler inlines
<Comp/>asComp(props)(componentNamesis gated ongenerate === "dom"in both babel and Oxc), so compiled JSX trees get no label on the server —ownerPathis populated forcreateComponentcallers (flow components,Dynamic, tests) and empty for a plain compiled tree. The runtime is ready (createComponent(Comp, props, name?)on the server); the compiler change is to emitcreateComponent(Comp, props, "Comp")for SSR output undercomponentNames, which the vite-plugin already sets fordev || observe. Recorded in the plan doc.Size
All ten scenarios within budget on the rebased base; no caps moved. Observe CSR 15566 B, attribution 27109 B. Prod tiers unchanged.
Tests
solid/test/server/server-diagnostics.spec.ts— component labels andcreationStampuntouched;SERVER_WRITE,ASYNC_OUTSIDE_LOADING_BOUNDARY(recorded then thrown),LAZY_ASSET_UNMAPPED(both shapes, located to the rendering owner),REVEAL_IN_RENDER_TO_STRING,SSR_RENDER_ERROR_CONTAINEDfrom<Errored>.solid/test/server/observe-server.spec.ts— slots populated by the server entry, one per process onglobalThis, generic subscribe/provide semantics, installed on the core's ownOBSERVEobject.solid/test/server/console-footer.spec.ts— the footer once per code on the server.web/test/server/server-diagnostics.spec.tsx—SSR_RENDER_ERROR_CONTAINEDfallback/client/failed,SSR_SUBTREE_ABANDONED,SSR_STREAM_ABANDONED(consumer and sink),LATE_HEADER_WRITE, the head/preload/insert checks withdata.reason/data.field; dev checks fold out of the observe artifacts (string scan).web/test/frames-marker-corruption.spec.tsx—FRAME_MARKER_CORRUPTEDon the client, reported once.web/test/dev-warning.spec.tsx— clientUNRECOGNIZED_INSERT_VALUElocated to its component.server-observe-invocations.spec.tsx/server-trace.spec.tsx— updated to thesolid-js/observe/server/*names.test-typesclean for solid and web.Docs
RFC 08 gains a "Server rendering" reference section (one entry per code) and quick-reference rows, and the
OBSERVE.serverparagraph now states the ownership; the reactivity-diagnostics skill gains a section per server code (one heading each — the footer anchors on#code);server-dev-build-plan.mdrecords P1/P2 landed and the compiler gap. Changeset included (patch ×@solidjs/signals,solid-js,@solidjs/web); one clause of the unreleasedserver-observe-install-point.mdadjusted so the changelog does not say web populates the slot.