Skip to content

feat(signals,solid,web,diagnostics): OBSERVE.records — one records channel on both platforms; the call record and its origin - #3472

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

ryansolid merged 3 commits into
nextfrom
server-frame-records

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Sentry-integration plan C4 (client half) and the channel unification it forced, plus J.28. Follows #3452 (C3) and #3453 (P4).

One channel: OBSERVE.records

The server records ("boundary", "invocation", "frame") lived on OBSERVE.server.records; the client had a separate, inherited observeServerFunctionCalls. Adding the client half of frames would have meant a third surface. Instead every record now rides one channel owned by the core:

  • OBSERVE.recordssubscribe(type, listener), observed(type), emit(type, event, live). One object per process, registered on globalThis under Symbol.for("@solidjs/signals/observe/records"), so a second copy of the core (a bundled server build instrumented through --import, SHAPE-NOTES §J.24) and wire layers bundled without a framework import (the server-function client) reach the same listener sets. Listeners snapshotted per emit; a throwing listener is reported and the rest run. Folds out of prod.
  • Type layering: the core declares two empty interfaces, one augmenter each — RecordTypes (solid-js adds "boundary") extends HostRecordTypes (@solidjs/web adds "invocation", "frame", "call" through the solid-js re-export). A consumer reading OBSERVE.records off solid-js sees the whole catalogue; pinned by a type test against the built declarations.
  • OBSERVE.server keeps only the trace slot.

The "frame" record, both sides

FrameEvent is FrameProducedEvent | FrameAppliedEvent, discriminated by side, the same census on both; the client half (applyFrameResponse, one per stream in a response) adds address (the as remap) and outcome: "truncated". The two join by id and version; the wire is the difference. Server census fix: regions counted a chunk type that does not exist — it now counts html chunks addressed to a child frame id — and shellMs is set by the stream's own shell only.

The "call" record, and its origin

One per server-function call made from the browser, at the caller's settle: { id, at, durationMs, method, outcome, status?, origin?, deferred? } with { args, response?, result? | error? } beside it. Joins the server's "invocation" by id. observeServerFunctionCalls is removed from both entries (no consumers in-repo, in the spike, in solid-router or solid-start; it also had to await the response body to show it to observers).

origin closes J.28: read at dispatch through the new OBSERVE.attribution.currentOrigin() — the provenance a root write made now would be stamped with: the interaction whose handler is running, the open navigation/effect/action frame, or inside a recompute the origin of the change that caused the run (a createAsync calling the server on a navigation's write is the navigation's, under its click). It is the engine's own ChangeOrigin object, so an observer files the call under InteractionEvent.origin / NavigationEvent.origin by identity, not by a time window. The installed hooks are also registered on globalThis (Symbol.for("@solidjs/signals/observe/attribution")) — the import-free reach the server-function client needs.

Tiers

The server-functions and frames client entries gain observe and development builds and export conditions. The server-functions client is now built with its flags replaced in every tier — before, _SOLID_DEV_ there was an unreplaced truthy string, so the dev cookie checks ran in prod. Dist scans pin the emitters (and the two registered names) out of the prod artifacts and into the observe/dev ones.

@solidjs/diagnostics v6

artifact.serverartifact.records: { boundary, invocation, frame, call }, always present, captured on both platforms including the browser bridge. JSONL: one line per record with type; recordCounts in the meta line.

Verified against real Sentry

The solid-sentry-spike (out of repo) was re-pointed to the channel on both platforms. Its instrument module now imports solid-js only — J.23 resolved — and the round trip joins: GET /http.server with function.solid.direct and solid.boundary children sharing the boundary id; a click in the page → browser function.solid.call (status 200/500, sanitized error, origin: click on button#echo "echo(1)" through the compiled event binding) and server function.solid.rpc (unsanitized) in the same trace under the same id. The spike adapter files calls under the open interaction's transaction by identity.

Docs

RFC 08 (channel, all four records, currentOrigin), RFC 10, 11, 12; diagnostics README and skills; changeset.

@changeset-bot

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5680a39

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
@solidjs/diagnostics Patch
test-integration Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/universal Patch
@solidjs/babel-plugin Patch
@solidjs/compiler 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

ryansolid and others added 2 commits September 15, 2026 12:16
…annel on both platforms; the call record and its origin

The runtimes' records move onto one channel, `OBSERVE.records`, owned by
the core: `subscribe(type, listener)`, `observed(type)`, `emit(type,
event, live)`, created once per process and registered on `globalThis`
under `Symbol.for("@solidjs/signals/observe/records")` so a second copy of
the core (a bundled server build instrumented through `--import`) and
wire layers bundled without a framework import reach the same listener
sets. The catalogue is two empty interfaces the runtimes augment, one
augmenter each: `RecordTypes` (solid-js: `"boundary"`) extends
`HostRecordTypes` (@solidjs/web: `"invocation"`, `"frame"`, `"call"`).
`OBSERVE.server` keeps only the `trace` slot.

The `"frame"` record gains its client half: `FrameEvent` is
`FrameProducedEvent | FrameAppliedEvent`, discriminated by `side`, the
same census on both, the client adding `address` and `outcome:
"truncated"`. The server census is corrected — `regions` counts `html`
chunks addressed to a child frame id (the old count read a chunk type
that does not exist) and `shellMs` is the stream's own shell only.

New `"call"` record: one per server-function call made from the browser,
at the caller's settle — `{ id, at, durationMs, method, outcome, status?,
origin?, deferred? }` with `{ args, response?, result? | error? }` beside
it. It replaces `observeServerFunctionCalls`, which is removed from both
server-function entries. `origin` is what the call ran for, read at
dispatch through the new `OBSERVE.attribution.currentOrigin()` (the
`currentOrigin` hook; the installed hooks are also registered on
`globalThis` under `Symbol.for("@solidjs/signals/observe/attribution")`
for the import-free client): the engine's own `ChangeOrigin` — the
interaction whose handler made it, the navigation whose recompute needed
it — so an observer files the call under the interaction record by
identity, not by a time window.

The server-functions and frames client entries gain `observe` and
`development` builds and export conditions; the server-functions client
is now built with its flags replaced in every tier (`_SOLID_DEV_` there
was an unreplaced truthy string).

`@solidjs/diagnostics` format v6: `artifact.server` becomes
`artifact.records: { boundary, invocation, frame, call }`, captured on
both platforms including the browser bridge; JSONL carries one line per
record with `type`, and `recordCounts` in the meta line.

RFC 08/10/11/12 updated; the diagnostics README and the two skills follow
the artifact.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ogram

The web test program holds the package twice — the runtime specs import
`../src`, and everything importing `@solidjs/web` reaches the built
`types/` through the self-link. Both copies now augment `solid-js`'s
record catalogue (src/observe.ts), and TypeScript requires duplicate
property declarations to be of identical type; the copies are, unless a
test augments the published one alone. `request-event-locals.*` does
exactly that (`RequestEventLocals`), so the two `InvocationLive.event`
types differed and the conflict surfaced as TS2717 in whichever file the
program loaded second — hidden before #3470 because the built copy came
second and `skipLibCheck` swallowed it there; the seams change reordered
the imports and put `src/observe.ts` second.

The augmentation acceptance tests move to `tsconfig.test.augment.json`, a
program with one copy of the package (the published one, which is their
point); `test-types` runs both.

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

coveralls commented Sep 15, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 35014116783

Coverage remained the same at 71.619%

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: 1014
Covered Lines: 772
Line Coverage: 76.13%
Relevant Branches: 790
Covered Branches: 520
Branch Coverage: 65.82%
Branches in Coverage %: Yes
Coverage Strength: 14.99 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Sep 15, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 2.42%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 173 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
projection derive: write one NESTED field (reference) 2.5 ms 2.8 ms -10.37%
createStore setter: delete + set one root key (#3044 overlay) 582.6 µs 548.4 µs +6.23%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing server-frame-records (5680a39) with next (d2a36f5)

Open in CodSpeed

…ize caps

The Size check caught the prod frames scenario at +286 B: `records()`
folded to `return undefined`, but the emitters it guarded
(`observeCall`, `observeFrameApply`, `frameCensus`, `isDeferredBody`) and
the wrappers around the dispatch and the apply were retained — rollup
cannot fold a call's undefined return through its call site. Now the
emitters return early behind the observe literal, and the wrappers exist
only where it holds: `fetchServerFunction` IS the dispatch and
`applyFrameResponse` IS the apply loop in prod (`IS_OBSERVE ? observed :
plain` aliases), so prod pays neither the extra frame nor the promise
hop. The frames scenario is back under its cap (11430 B).

The two observe-tier scenarios grew deliberately — the records channel
and the `currentOrigin` query ship there — and their caps move with the
measured artifacts: CSR observe 15.90 -> 16.05 KB (+216), attribution
27.45 -> 27.70 KB (+323).

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit 3ae9e92 into next Sep 15, 2026
6 checks passed
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