Skip to content

feat(solid,web,signals): server boundary records on OBSERVE.server.records - #3452

Merged
ryansolid merged 3 commits into
nextfrom
server-boundary-records
Sep 15, 2026
Merged

ryansolid merged 3 commits into
nextfrom
server-boundary-records

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Sentry integration plan C3 — server boundary timing, as records on the server observe surface. Follows #3433 (C2) and #3441 (SSR component labels).

The "boundary" record

One per <Loading> boundary that waited during a server render, delivered when it settles:

OBSERVE.server.records.subscribe("boundary", (event, live) => {
  // event: { id, at, durationMs, heldMs, passes,
  //          outcome: "settled" | "fallback" | "client" | "error",
  //          streamed, revealGroup?, ownerPath? }
  // live:  { error? }
});
  • id is the boundary's hydration id — the same data.boundary on SSR_RENDER_ERROR_CONTAINED, so record and finding pair.
  • passes = discovery pass + one per wait. 2 is one round of async; more is a sequential chain (the waterfall substrate). It also replaces the convergence budget's private counter.
  • streamed is registerFragment's answer: did the outcome reach the client after the shell flushed.
  • heldMs is real: under a <Reveal> group the record waits for the group's swap (a per-leaf onReveal hook every revealFragments site in Reveal now fires) and measures how long finished content sat behind its siblings. A group that never reveals (stream abandoned) loses the record; SSR_STREAM_ABANDONED is that request's account.
  • First-pass boundaries emit nothing (the client hold rule). No clock is read without a listener. Slots and emitter fold out of dist/server.js (pinned).

Channel rename

OBSERVE.server.records.subscribe(type, listener) — the server twin of OBSERVE.attribution.subscribe(type, …). C0's OBSERVE.server.invocations (unreleased) is renamed onto it as the "invocation" type; InvocationChannel is gone. Frame records (C4) join by name.

Type layering — a TypeScript trap worth knowing about

The first cut had signals declare ServerRecords, solid-js augment it via "@solidjs/signals" and web via "solid-js". Each side then saw only its own overload, with no error. Reproduced in isolation: TS (6.0.3) merges two augmentations of one re-exported interface through different alias paths order-dependently and silently drops one set. ServerObserve had dodged this so far only because web was its sole augmenter.

Now one augmenter per interface, matching the package chain:

  • @solidjs/signals declares ServerObserve empty.
  • solid-js augments it once (via "@solidjs/signals") with records: ServerRecords and trace: ServerTrace, declaring both interfaces itself, "boundary" overload inline.
  • @solidjs/web augments those two via "solid-js" ("invocation", provide). TraceSlot is now an alias of ServerTrace.

The rule is documented on ServerObserve; web/test/server-observe.type-tests.ts requires both overloads. This also caught that core/index.ts had never re-exported the new name.

Tests

web/test/server/server-boundary-records.spec.tsx (14) against the real renderers and <Reveal>: settled / streamed / inlined via deferStream, a 3-pass chain, renderToString fallback, client holes at discovery and after a real wait, error paired with the finding by id, together hold (heldMs ≥ 30 early sibling, ≈0 late one), natural, grouped error, throwing listener, unsubscribe, prod fold-out. Type tests updated. Signals 1891 / solid 618 / web 783 + 909 green; tsc and test-types clean on all three.

Docs

RFC 08 (OBSERVE.server — the layering, the "boundary" record), RFC 10, sentry-integration-plan.md (C3 landed, with the deviations noted), server-dev-build-plan.md. Changeset included.

…cords

The `"boundary"` record — one per `<Loading>` boundary that WAITED during
a server render, delivered when it settles: `{ id, at, durationMs, heldMs,
passes, outcome: "settled" | "fallback" | "client" | "error", streamed,
revealGroup?, ownerPath? }`, the thrown error beside it. `id` is the
boundary's hydration id, the one `SSR_RENDER_ERROR_CONTAINED` names in
`data.boundary`, so a record and a finding pair by it. `passes` counts
render passes over the content (discovery plus one per wait — `2` is one
round of async, more is a sequential chain) and doubles as the convergence
budget's counter. `streamed` is `registerFragment`'s answer — whether the
outcome reached the client after the shell flushed. Under a `<Reveal>`
group the record waits for the group's swap, via a per-leaf `onReveal`
hook every `revealFragments` site in `Reveal` now fires, so `heldMs` is
how long finished content sat behind its siblings. A boundary that
renders on its first pass emits nothing (the client `hold` rule); no clock
is read without a listener; the slots and emitter fold out of prod.

The channel is `OBSERVE.server.records.subscribe(type, listener)` — the
server twin of `OBSERVE.attribution.subscribe(type, …)`. C0's
`OBSERVE.server.invocations` (unreleased) is renamed onto it as the
`"invocation"` type; `InvocationChannel` is gone.

The types now layer one augmenter per interface, matching the package
chain. First cut had signals declare `ServerRecords`, solid-js augment it
through `"@solidjs/signals"` and web through `"solid-js"`; each side then
saw only its own overload. Reproduced in isolation: TypeScript merges two
augmentations of one re-exported interface through DIFFERENT alias paths
order-dependently, dropping one set with no error. So: signals declares
`ServerObserve` empty; solid-js augments it once with `records:
ServerRecords` and `trace: ServerTrace`, declaring both (the `"boundary"`
overload inline); web augments those two through `"solid-js"`
(`"invocation"`, `provide`). `TraceSlot` is an alias of `ServerTrace`.
The rule is documented on `ServerObserve`; the web type tests require
both overloads. (`core/index.ts` had also never re-exported the new name.)

Specs: web/test/server/server-boundary-records.spec.tsx against the real
renderers and Reveal — settled/streamed/inlined, a 3-pass chain,
renderToString fallback, client holes at discovery and after a wait, error
paired with the finding, together/natural holds, grouped error, listener
resilience, prod fold-out. Docs: RFC 08 (`OBSERVE.server`, the record),
RFC 10, the plans (C3 landed).

Co-authored-by: Cursor <cursoragent@cursor.com>
@coveralls

coveralls commented Sep 15, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 34942153511

Coverage remained the same at 71.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 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 15, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 172 untouched benchmarks


Comparing server-boundary-records (f519d15) with next (08fe835)

Open in CodSpeed

ryansolid and others added 2 commits September 15, 2026 00:30
…eveal onReveal plumbing folds out of prod

- `@solidjs/signals` exports `ownerPath`; the server boundary record uses it
  instead of a verbatim copy (`ownerLabels`) in solid-js's server observe module.
- The Reveal group's `onReveal` hook registration and dispatch are gated on
  `IS_OBSERVE` so the prod artifact carries neither; a second tier pin in
  server-boundary-records.spec asserts `onReveal` is absent from server.js and
  present in server.observe.js / server.dev.js.
- Merged origin/next.

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

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f519d15

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

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