Skip to content

feat(solid,web,signals): server diagnostics on OBSERVE.diagnostics; OBSERVE.server owned by solid-js's server entry - #3433

Merged
ryansolid merged 1 commit into
nextfrom
server-diagnostics-c2
Sep 14, 2026
Merged

ryansolid merged 1 commit into
nextfrom
server-diagnostics-c2

Conversation

@ryansolid

Copy link
Copy Markdown
Member

What

The server runtime reports on OBSERVE.diagnostics — the channel the client already uses — and OBSERVE.server's objects move to solid-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.

  • Findings (observe + dev builds): facts about a render a production observer wants counted.
    • SSR_RENDER_ERROR_CONTAINED — a render error a boundary routed; data.handling is fallback (<Errored> rendered its fallback), client (the <Loading> fragment rejected, the client re-renders it), or failed (nothing contained it, the request failed). The structured face of what renderToStream's per-call onError receives, 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.
  • Checks (dev build): every server console.warn becomes a code — SERVER_WRITE, REVEAL_IN_RENDER_TO_STRING, LAZY_ASSET_UNMAPPED, PRELOAD_DESCRIPTOR_INVALID, HEAD_TAG_INVALID (data.reason names the rule), BEHAVIOR_CLAIM_DROPPED, UNRECOGNIZED_INSERT_VALUE. The last is one code and a new render kind on both platforms; the client's console.warn in client.ts is converted too. ASYNC_OUTSIDE_LOADING_BOUNDARY on the server records with data.side: "server" before it throws.
  • ownerPath on the server. createComponent runs the body under a transparent owner labelled <Name> in observe/dev (createComponentOwner — no hydration id consumed, no ownerCreations tick), and the core's existing ownerPath walk reads SSROwner._parent/_name unchanged, so in <App> › <Page> reads the same on both sides.
  • OBSERVE.server owned by solid-js's server entry. The invocation listener set and the trace-provider slot are created in packages/solid/src/server/observe.ts, once per process under Symbol.for("solid-js/observe/server") on globalThis. An observer's init() that imports only solid-js can subscribe and provide before @solidjs/web has 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 keeps server: {}.
  • Console footer on the server. The repair-guide 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.

Design notes

  • Two gates, three verbs. packages/solid/src/server/diagnostics.ts and packages/web/src/diagnostics.ts: emitFinding (observe + dev — emit, and in dev report through DEV.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 (emitFinding behind the dev gate). Call sites are additionally wrapped in a literal if ("_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).
  • Why the slots are not in the signals core. A draft put serverSlots() in dev.ts so 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 only solid-js", and that entry is server-only. Client cost: zero.
  • render kind. UNRECOGNIZED_INSERT_VALUE fires from the client's insert as well as the server's; ssr was the wrong kind for it, and no existing kind covered "the renderer's insert positions". DiagnosticKind gains ssr, head, render.

Known gap (separate PR)

The SSR compiler inlines <Comp/> as Comp(props) (componentNames is gated on generate === "dom" in both babel and Oxc), so compiled JSX trees get no label on the server — ownerPath is populated for createComponent callers (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 emit createComponent(Comp, props, "Comp") for SSR output under componentNames, which the vite-plugin already sets for dev || 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 and creationStamp untouched; 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_CONTAINED from <Errored>.
  • solid/test/server/observe-server.spec.ts — slots populated by the server entry, one per process on globalThis, generic subscribe/provide semantics, installed on the core's own OBSERVE object.
  • solid/test/server/console-footer.spec.ts — the footer once per code on the server.
  • web/test/server/server-diagnostics.spec.tsxSSR_RENDER_ERROR_CONTAINED fallback/client/failed, SSR_SUBTREE_ABANDONED, SSR_STREAM_ABANDONED (consumer and sink), LATE_HEADER_WRITE, the head/preload/insert checks with data.reason/data.field; dev checks fold out of the observe artifacts (string scan).
  • web/test/frames-marker-corruption.spec.tsxFRAME_MARKER_CORRUPTED on the client, reported once.
  • web/test/dev-warning.spec.tsx — client UNRECOGNIZED_INSERT_VALUE located to its component.
  • server-observe-invocations.spec.tsx / server-trace.spec.tsx — updated to the solid-js/observe/server/* names.
  • signals 1786 / solid 613 / web 768 + 892 + 180 pass; test-types clean for solid and web.

Docs

RFC 08 gains a "Server rendering" reference section (one entry per code) and quick-reference rows, and the OBSERVE.server paragraph 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.md records P1/P2 landed and the compiler gap. Changeset included (patch × @solidjs/signals, solid-js, @solidjs/web); one clause of the unreleased server-observe-install-point.md adjusted so the changelog does not say web populates the slot.

…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-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bae6b06

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

This PR includes changesets to release 11 packages
Name Type
@solidjs/signals Patch
solid-js Patch
@solidjs/web Patch
test-integration Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/universal Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics 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

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 34882893492

Coverage remained the same at 71.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: 6 of 6 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1007
Covered Lines: 772
Line Coverage: 76.66%
Relevant Branches: 790
Covered Branches: 519
Branch Coverage: 65.7%
Branches in Coverage %: Yes
Coverage Strength: 15.09 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Sep 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 160 untouched benchmarks


Comparing server-diagnostics-c2 (bae6b06) with next (a031271)

Open in CodSpeed

@ryansolid
ryansolid merged commit 7623ce1 into next Sep 14, 2026
7 checks passed
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>
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.

2 participants