feat(signals,solid,web,diagnostics): OBSERVE.records — one records channel on both platforms; the call record and its origin - #3472
Conversation
🦋 Changeset detectedLatest commit: 5680a39 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 |
be7abe4 to
99512e7
Compare
…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>
99512e7 to
6ce0418
Compare
Coverage Report for CI Build 35014116783Coverage remained the same at 71.619%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will degrade performance by 2.42%
|
| 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)
…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>
Sentry-integration plan C4 (client half) and the channel unification it forced, plus J.28. Follows #3452 (C3) and #3453 (P4).
One channel:
OBSERVE.recordsThe server records (
"boundary","invocation","frame") lived onOBSERVE.server.records; the client had a separate, inheritedobserveServerFunctionCalls. Adding the client half of frames would have meant a third surface. Instead every record now rides one channel owned by the core:OBSERVE.records—subscribe(type, listener),observed(type),emit(type, event, live). One object per process, registered onglobalThisunderSymbol.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.RecordTypes(solid-js adds"boundary") extendsHostRecordTypes(@solidjs/web adds"invocation","frame","call"through thesolid-jsre-export). A consumer readingOBSERVE.recordsoffsolid-jssees the whole catalogue; pinned by a type test against the built declarations.OBSERVE.serverkeeps only thetraceslot.The
"frame"record, both sidesFrameEventisFrameProducedEvent | FrameAppliedEvent, discriminated byside, the same census on both; the client half (applyFrameResponse, one per stream in a response) addsaddress(theasremap) andoutcome: "truncated". The two join byidandversion; the wire is the difference. Server census fix:regionscounted a chunk type that does not exist — it now countshtmlchunks addressed to a child frame id — andshellMsis set by the stream's own shell only.The
"call"record, and itsoriginOne 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"byid.observeServerFunctionCallsis 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).origincloses J.28: read at dispatch through the newOBSERVE.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 (acreateAsynccalling the server on a navigation's write is the navigation's, under its click). It is the engine's ownChangeOriginobject, so an observer files the call underInteractionEvent.origin/NavigationEvent.originby identity, not by a time window. The installed hooks are also registered onglobalThis(Symbol.for("@solidjs/signals/observe/attribution")) — the import-free reach the server-function client needs.Tiers
The server-functions and frames client entries gain
observeanddevelopmentbuilds 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/diagnosticsv6artifact.server→artifact.records: { boundary, invocation, frame, call }, always present, captured on both platforms including the browser bridge. JSONL: one line per record withtype;recordCountsin 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 importssolid-jsonly — J.23 resolved — and the round trip joins:GET /→http.serverwithfunction.solid.directandsolid.boundarychildren sharing the boundary id; a click in the page → browserfunction.solid.call(status 200/500, sanitized error,origin: click on button#echo "echo(1)"through the compiled event binding) and serverfunction.solid.rpc(unsanitized) in the same trace under the sameid. 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.