feat(web,signals)!: "render" record + AttributionOptions.values; Server-Timing as record projections - #3650
Merged
Conversation
🦋 Changeset detectedLatest commit: 92bc67b 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 |
ryansolid
force-pushed
the
render-record-scrub
branch
from
September 25, 2026 06:43
94b0623 to
2066cdc
Compare
Coverage Report for CI Build 36104867710Coverage remained the same at 73.611%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
…er-Timing as record projections
Leg A: a server render is a "render" record on OBSERVE.records
(RenderEvent: mode, at, shellMs, durationMs, boundaries, outcome; live
event + trace). The Server-Timing metrics are now strict projections of
records under one gate each: solid-invocation <- "invocation", solid-shell
<- "render", solid-boundary <- "boundary" (the solid-js boundary files its
BoundaryEvent through `_recordBoundary`). Wire format unchanged.
Leg B: AttributionOptions.values ("full" | "labels" | "none") governs the
user-data fields of the engine's records at the source; least permissive
level wins across holds; default is the tier's (dev "full", observe
"none", folded at build time). performance-tracks' `scrub` option and
helpers are removed — the adapter paints what the record carries.
Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid
force-pushed
the
render-record-scrub
branch
from
September 25, 2026 06:52
2066cdc to
92bc67b
Compare
Merging this PR will not alter performance
Comparing Footnotes
|
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.
Was stacked on #3644 (records channel), which landed on
nextas 384a631; rebased ontonextafter #3645, #3646 and #3649 (235173e) and retargeted.PR 3 of the public-API consolidation. Two legs, both closing the same gap: the observe surface had places where a fact was duplicated beside its record (the
Server-Timingmetrics kept their own clock and their own push) or scrubbed after the record was built (the performance-tracks adapter'sscrub). Both now go through the record.Leg A — the
"render"record;Server-Timingstrictly projected from recordsNew record. A server render (
renderToString/renderToStream) is a record onOBSERVE.records, from@solidjs/web's server runtime:Built by
observeRender(server-observe.ts) under the one gatetimesServerWork("render")=observed("render") || IS_DEV; the renderer holds aRenderObservation { shell(), boundary(), settle(outcome) }.shellMsis stamped where the shell actually completes — the stream'sdoShell(immediately before the sink handoff, so the head commit inside the sink's first write reads it) and the string render's assembled document. Settled atonDone(complete), inabandon(abandonedon a disconnect,erroron a render failure), and inrenderToString'sfinally. Delivered before the render's trace is cleared, sogetTraceContext()in a listener finds it. Registered through the existingHostRecordTypesaugmentation beside"invocation"; types exported from both@solidjs/webentries.Header = projection.
TraceRecordnow holds record objects, not formatted metrics:timing: TimedWork[]({ type: "invocation", event: InvocationEvent } | { type: "boundary", event: BoundaryEvent }) andrender?: RenderEvent.appendTraceServerTimingprojects at head commit —solid-shellfromrender.shellMs, thenmetricOf(work)per entry (solid-invocation←durationMs/id;solid-boundary←durationMs/ownerPath.join(" > ")elseid). One code path builds each record; the header reads it. Thesolid-jsboundary's seam changed from_timing.push({name,dur,desc})to_recordBoundary(event: BoundaryEvent)— it builds itsBoundaryEventonce (whentimedand either a listener or the header wants it) and hands the same object to the seam and the channel.Gates, one per metric:
solid-invocation↔"invocation",solid-shell↔"render",solid-boundary↔"boundary". Dev: all three always. The web side installs the_recordBoundaryseam undertimesServerWork("boundary")— the same rule the boundary applies before building its record — so the two sides of one measurement agree by construction, and a mixed-tier harness (devsolid-jssource under the observe@solidjs/webartifact, which is howserver-trace.specloads them) cannot make the header say what no listener asked for.Wire unchanged. Same names,
durrounded to a tenth,descquoted and ASCII-sanitised, owner path joined with>(full path, not shortened),solid-shellfirst then completion order. The pre-existing header assertions inserver-trace.spec.tsxpass untouched; perf-tracks'navigation()andServerSpansare not modified.Leg B —
AttributionOptions.values, applied at the sourceOne engine option, three monotone levels — a second axis was not needed:
ChangeRecord.prev/value,HeldWrite.prev/valueChangeOrigin.target/InteractionEvent.target/HoldEvent.interaction.targettextformatRerun,formatOrigin, SILENT_HOLD/LONG_HOLD, OPTIMISTIC_REVERTED)"full"(dev default)"labels"button/a1 → 2; element text only for button/a"none"(observe default)Default is the build tier's:
defaultOptions.values = __DEV__ ? "full" : "none"— folded at build time, so the observe engine artifact ships the literal"none"(pinned indist-artifacts.test.ts). Applied where the record is built:stampWritetakes the previews only under"full"(soHeldWrite, copied from theChangeRecord, and every sentence built on it follow);interactionStartcopiesInteractionRef.targetthroughtargetLabel()(cuts at the first", the documentedtag#id "text"shape — the ref itself is never mutated);checkOptimisticRevertquotesshown/truth(message anddata) only under"full". Merge rule: least permissive wins (demanding()ranksnone < labels < full). A holder that names no level asks for the tier's default — in observe that tightens to"none"beside anyone; a single holder passing"full"in observe gets"full"; an explicit"full"never loosens what another holder demanded; a release restores the remaining holders' level. Navigationto/from/paramsare the router's description and stay outside the option (documented).perf-tracks lost: the
scruboption,Painter.scrub,scrubRerun,scrubOrigin,scrubCause,scrubTarget,TARGET_TEXT, and the scrub branches inrootCause,heldWrite,origin,writesand the diagnostic tooltip. The adapter paints what the record carries; its observe artifact inherits the tier default"none";enablePerformanceTracks({ attribution: { values: "labels" } })reproduces the old observe posture exactly.Public API changes
"render"record type onOBSERVE.records(HostRecordTypesaugmentation in@solidjs/web); added exported typesRenderEvent,RenderLive,RenderListenerfrom@solidjs/web(client and server entries).AttributionOptions.values?: "full" | "labels" | "none"and exported typeAttributionValuesfrom@solidjs/signals/attribution(andsolid-js/attributionviaexport *), on the real and inert engines. Default is tier-dependent:"full"in dev builds,"none"in observe builds.PerformanceTracksOptions.scrubfrom@solidjs/web/performance-tracks. No compat alias (prerelease).solid-shellnow rides the"render"gate, not the"boundary"listener. A deployment subscribed to"boundary"alone keepssolid-boundaryand needs a"render"subscription forsolid-shell."none"); a holder that wants them passesvaluesexplicitly. perf-tracks observe builds go from the old scrub posture (button/link labels kept) to"none"; a finding's marker/tooltip always carriesevent.message(a non-engine emitter's message is not the engine's to govern).OPTIMISTIC_REVERTEDdatais{ source, how }withoutshown/truthunder"labels"/"none"; message wording changes at those levels.solid-jsserver render-context seam_timing→_recordBoundary(event);@solidjs/webTimingMetrictype →TimedWork(trace.ts, never exported).Verification
pnpm build(18 package tasks),pnpm typesclean.@solidjs/signals: 229 files / 3708 tests pass — newattribution-values.test.ts(11: per level for ChangeRecord/HeldWrite/target/formatOrigin/formatRerun, SILENT_HOLD and OPTIMISTIC_REVERTED sentences anddata, dev default, ref not mutated, three-holder merge with release, unnamed holder never loosens, level applies from its moment) and two tier pins indist-artifacts.test.ts(dev artifact defaults"full"; observe artifact defaults"none", explicit"full"applies when alone, and the dev branch of the default is folded out of the artifact).solid-js: 37 files / 781 tests pass;test-typesclean.@solidjs/web: client 102/943, server 112/1155 (+server-trace.spec: per-gate observe-tier test — boundary listener alone →["solid-boundary"], render alone →["solid-shell"], both → both; a"render" recordblock pinning stream/string/no-scope/error/abandoned records,shell.dur === round(record.shellMs, 0.1),live.event/live.trace), hydrate 37/248;test-types(incl. augment) clean, with"render"added toobserve.type-tests.ts.@solidjs/diagnostics: 6/34 pass (unchanged — itstypes.tsstill carries hand-writtenBoundaryRecord/InvocationRecordmirrors on this base, so noRenderRecordwas added; see follow-ups).scrub(adapter),_timing,shellStart,TimingMetric,timesServerWork("boundary")outsidetimeDocument.scripts/size, head vsrecords-channelbuilt in a detached temp worktree): every prod scenario and the observe tier byte-identical (0 B).app: CSR, observe tier + attribution engine enabled: 31,689 → 31,868 B (+179 B), cap 31.70 → 31.90 KB with a note. That is thevaluesmachinery —targetLabel, the"full"gates instampWrite/checkOptimisticRevert,VALUES_RANK+ thedemandingbranch — a deliberate feature.Judgment calls
"labels"rather than"previews": under"full"the values are already previews, so"previews"would name the thing being removed."labels"names what is kept (the control's caption).shellMsis stamped at the shell's own completion point, not at stub commit. For a stream that is equivalent to before (commit happens inside the shell's first sink write); for an awaitedrenderToStreamwhose head is committed at completion,solid-shellnow measures the shell rather than the whole render —durationMscarries the whole render.RenderEvent.boundariescounts the boundaries the render filed through the seam, i.e. under the"boundary"gate: in observe with a"render"listener alone it is0(documented on the field). Counting independently would need the boundary to call the seam without a record — a second path for one fact.performance-tracks-adapter.md,performance-tracks-dev-enrichments.md) still describe the observe scrub. Left untouched; the new changeset states the removal.Follow-ups
targetText: true↦values: "full"(most permissive;false↦"none", which is now also the observe default), drop its local regex scrub oftarget, and subscribe to"render"for the SSR shell span (solid-shellno longer arrives on the"boundary"listener alone).performance-tracksobserve-tier artifact: it is a dev tool in practice, and observe should stay tight; withvaluesdefaulting to"none"there its tracks carry little a developer wants anyway.@solidjs/diagnostics: once PR 2 lands the generic server-record listing, add"render"to the artifact; on this base the package still hand-mirrorsBoundaryRecord/InvocationRecord, so nothing was added here.valueslevels + tier default + merge rule, the"render"record,enablePerformanceTracksoptions (noscrub), the per-recordServer-Timinggates.Rebase notes
Rebased the single
render-record-scrubcommit (94b0623b0) ontonextat235173e6a(git rebase --onto origin/next 94b0623b0^; the branch sat on the olderrecords-channeltip255f8e063). Four conflicts, all mechanical:packages/solid/src/server/hydration.ts— refactor!: prune observability surface — OBSERVE.ownerPath, DEV.guideUrl, drop install/aliases/unused options #3649 turnedownerPath(o)intoOBSERVE!.ownerPath(o); this PR replaced thetiming.push(...)block with the_recordBoundaryseam. Kept this PR's structure with refactor!: prune observability surface — OBSERVE.ownerPath, DEV.guideUrl, drop install/aliases/unused options #3649's call:const path = OBSERVE!.ownerPath(o);.packages/web/performance-tracks/src/index.ts—PerformanceTracksOptions: refactor!: prune observability surface — OBSERVE.ownerPath, DEV.guideUrl, drop install/aliases/unused options #3649 removedgroup, this PR removesscrub. Both gone; the interface ends atrich.documentation/solid-2.0/08-dev-diagnostics.md— the "Values in records" paragraph: kept this PR's rewrite (thevalueslevels) with feat!: consolidate diagnostic codes; sourceNames follows dev in both compilers #3646's code rename applied inside it (SSR_ERROR_SANITIZED,SERVER_FN_ERROR_SANITIZED→SERVER_ERROR_SANITIZEDon either road). TheenablePerformanceTracksexample: dropped bothscrubandgroup, keptattribution: { values: "labels" }.scripts/size/.size-limit.js— "CSR, observe tier + attribution engine enabled": kept both notes (refactor!: prune observability surface — OBSERVE.ownerPath, DEV.guideUrl, drop install/aliases/unused options #3649's 31.70 → 31.71 ratchet, then this PR's). Re-measured on the merged code: 31,618 (nextafter refactor!: prune observability surface — OBSERVE.ownerPath, DEV.guideUrl, drop install/aliases/unused options #3649) → 31,804 B (+186 B); cap set to 31.81 KB (was 31.90 in the original, measured 31,868 againstrecords-channel), matching the ledger's ~10 B ratchet convention. Every prod scenario byte-identical tonext; the observe tier scenario is −10 B (17,676 → 17,666, mangler layout).Re-verified on the new base: signals 229 files / 3711 (+2 skipped) incl.
attribution-values.test.ts11; solid 781,test-typesclean; web client 960 / server 1168 (+2 skipped) / hydrate 248,test-types(incl. augment) clean; diagnostics 34. (Relative tonext: client −1 — twoscrubtests replaced by onevaluestest — and server +5, theserver-trace.specadditions; the Verification section's client "943" was PR 2's count.)pnpm build --filter './packages/*'17/17,pnpm typesclean.@solidjs/diagnosticswas left without a"render"row on purpose (follow-up below).