Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/render-record-and-values-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@solidjs/signals": patch
"solid-js": patch
"@solidjs/web": patch
---

`"render"` record and `AttributionOptions.values` — the two remaining places where the observe surface duplicated a record or scrubbed one after the fact.

**`"render"` record** (`@solidjs/web`, server). A server render — `renderToString` or `renderToStream` — is now a record on `OBSERVE.records`: `RenderEvent { mode: "string" | "stream", at, shellMs?, durationMs, boundaries, outcome: "complete" | "abandoned" | "error" }`, delivered when the render ends, with `RenderLive { event?: RequestEvent, trace: TraceContext }` beside it. `shellMs` is render start → the shell complete (the stream's shell handed to the sink; the string's document assembled); `boundaries` counts the `<Loading>` boundaries the shell waited on. Types `RenderEvent`, `RenderLive`, `RenderListener` are exported from `@solidjs/web`.

The response's `Server-Timing` metrics are now strictly projections of records, one gate each (`observed(type) || dev`): `solid-invocation` from the `"invocation"` record, `solid-shell` from the `"render"` record's `shellMs`, `solid-boundary` from each `"boundary"` record the shell waited on — computed from the record objects at head commit, no second push. Wire format unchanged. **Behavior change (observe tier):** `solid-shell` now rides the `"render"` listener, not the `"boundary"` listener; an observe deployment that subscribed to `"boundary"` alone keeps its `solid-boundary` metrics and needs a `"render"` subscription for `solid-shell`. Dev builds still write all three always.

**`AttributionOptions.values: "full" | "labels" | "none"`** (`@solidjs/signals`, re-exported by `solid-js/attribution`). One engine option governs the user-data fields of the engine's records at the source: `ChangeRecord.prev`/`value`, `HeldWrite.prev`/`value`, `ChangeOrigin.target` (and so `InteractionEvent.target`, `HoldEvent.interaction.target`), and every sentence built from them (`formatRerun`, `formatOrigin`, `SILENT_HOLD`/`LONG_HOLD`, `OPTIMISTIC_REVERTED`). `"full"` is today's dev output; `"labels"` drops value previews and keeps element text only on a `button` or an `a`; `"none"` drops both. **The default is the build tier's: `"full"` in dev builds, `"none"` in observe builds** (folded at build time — the observe engine ships `"none"` only). Across holds the **least permissive** level wins; a holder naming no level asks for the tier's default, so in an observe build it tightens to `"none"` beside anyone, while a single holder passing `"full"` there gets `"full"`; an explicit `"full"` never loosens what another holder demanded. Observe-tier consumers that export records should pass their level explicitly and treat it as their export contract.

**Removed:** `PerformanceTracksOptions.scrub` and the adapter's scrub helpers. `@solidjs/web/performance-tracks` paints what the engine put on the record: an observe build's tracks inherit `"none"` (tighter than the old scrub — no element text on buttons/links either); the old observe posture is `enablePerformanceTracks({ attribution: { values: "labels" } })`. **Behavior change:** a finding's marker always carries `event.message`.

Internal: `solid-js`'s server render context seam `_timing` became `_recordBoundary(event: BoundaryEvent)` — the boundary files its record, the web runtime projects the header from it.
22 changes: 21 additions & 1 deletion documentation/plans/chrome-performance-tracks-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,17 @@ transition)`. Shape: `{ ownerPath?, at, shownMs, interaction? }` (item 4's
`47 runs`.
- PII: observe builds apply the sketch §6 scrub by default (no value
previews; element text only on a `button`/`a`; a finding's sentence
dropped); dev shows everything.
dropped); dev shows everything. _As landed (public-API consolidation,
PR 3):_ the adapter's `scrub` option and its scrub helpers were removed;
the same posture is now the engine's `AttributionOptions.values`
(`"full"` | `"labels"` — the old observe scrub | `"none"`; the default is
the tier's: `"full"` in dev, `"none"` in observe), applied at the source
when the record is built, least permissive level winning across holds.
The adapter paints what the record carries — an observe build's tracks
inherit `"none"`; pass
`enablePerformanceTracks({ attribution: { values: "labels" } })` for the
old observe posture. A finding's marker always carries `event.message`
(a message from a non-engine emitter is not the engine's to govern).
- Clock quantization (08-dev-diagnostics): without cross-origin isolation
many `Effects`/`Memos` spans are zero-width. Never dropped; the wall-clock
tracks carry the meaning.
Expand Down Expand Up @@ -248,6 +258,16 @@ gate `ssrLoadingBoundary` has.) No new server API: an observe deployment
that wants server spans in the panel installs an observer, the same way it
gets the trace advertised.

_As landed (public-API consolidation, PR 3):_ each metric is a projection
of one record object, read at head commit (`appendTraceServerTiming` over
`TraceRecord.timing` / `TraceRecord.render`), not a second push beside the
record. `solid-shell` got its own record — `"render"` (`RenderEvent`: `mode`,
`at`, `shellMs`, `durationMs`, `boundaries`, `outcome`; live `event`,
`trace`) — and its own gate, `observed("render") || IS_DEV`, instead of
riding the boundary listener; `shellMs` is stamped where the shell actually
completes (the stream's `doShell`, the string's assembled document) rather
than at stub commit. The wire format is unchanged.

**Client — the adapter.** Nothing new on the wire from the browser and no
change to the `call` record: the adapter reads the metrics off
`CallLive.response` (the transport's own `Response`; same-origin headers are
Expand Down
29 changes: 24 additions & 5 deletions documentation/solid-2.0/08-dev-diagnostics.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion documentation/solid-2.0/12-ssr-http.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ interface TraceContext {

The runtime also hands the trace **down to the browser**, which cannot send a header on the initial document request and has to learn the server's trace from the response: `entries` are emitted as `Server-Timing` metrics (`traceparent;desc="00-…"`) on the response head when it commits — every exit, so frames, server-function responses and redirects carry it too — and, for HTML documents, as `<meta name="traceparent" content="…">` in the shell head (the `</head>` splice or the `onHead` string; a headless fragment ships only the header). A `Server-Timing` name the application already wrote (an `httpHeader` declaration, an integration's metrics) is respected; the runtime's entries fold in beside it. One rule governs when the browser is told: **only when something is recording the trace** — the incoming `traceparent` had its sampled flag set (the caller says it recorded), or (observe/dev builds) a provider answered. Two things therefore stay silent while remaining fully available to `getTraceContext()`: a trace the runtime originated alone, and an **unsampled** upstream trace (`00-…-00`). Neither has a recorded server span for the browser to attach to, and a parent with flags `00` would make a parent-based browser sampler drop the pageload it would otherwise record. The unsampled case is everyday infrastructure — load balancers and meshes (GCP, Envoy/Istio, Azure Front Door) stamp `traceparent` on every request they forward — so an app on them with no APM sees zero wire change, while forwarding that trace downstream from a server function stays correct W3C propagation. The incoming `baggage` is likewise never echoed to the page — it is upstream context; a provider that wants its own in the document adds it.

The same header carries the request's **timed work** for the Performance panel (RFC 08, [Chrome Performance panel](08-dev-diagnostics.md#chrome-performance-panel-solidjswebperformance-tracks)) under its own gate: `solid-invocation;dur=…;desc="<id>"` on a server-function response, `solid-shell;dur=…` (render start → head commit) and one `solid-boundary;dur=…;desc="<owner path>"` per boundary that waited and settled before the shell on a document. Dev builds always write them; observe builds only while something has `observe.records.observed("invocation")` / `("boundary")` — the measurement is being taken for a listener, so the header rides along — and prod builds never have the code. The header freezes when the head leaves, so a streamed boundary is not on it; `desc` values are sanitized to printable ASCII (the owner path joins with `>` on the wire) because a header value cannot carry code points above `0xFF`.
The same header carries the request's **timed work** for the Performance panel (RFC 08, [Chrome Performance panel](08-dev-diagnostics.md#chrome-performance-panel-solidjswebperformance-tracks)) each a projection of one of the server's records on `OBSERVE.records`, under that record's gate: `solid-invocation;dur=…;desc="<id>"` on a server-function response (the `"invocation"` record), `solid-shell;dur=…` (the `"render"` record's `shellMs`: render start → the shell complete) and one `solid-boundary;dur=…;desc="<owner path>"` per boundary that waited and settled before the shell on a document (each a `"boundary"` record). The header is computed from the record objects at head commit — one clock, one object per metric. Dev builds always write them; observe builds only while something has `observe.records.observed("invocation")` / `("render")` / `("boundary")` — the record is being built for a listener, so the header rides along — and prod builds never have the code. The header freezes when the head leaves, so a streamed boundary is not on it; `desc` values are sanitized to printable ASCII (the owner path joins with `>` on the wire) because a header value cannot carry code points above `0xFF`.

**The provider** (observe and dev builds — `OBSERVE.server.trace`, see RFC 08) is how an APM overrides or extends the derivation once, globally: Sentry's server SDK answers from OpenTelemetry's active span and adds its `sentry-trace`/`baggage` entries, which the browser SDK reads from the same two carriers. A provider answers every field its vendor decides — `parentId` included, since a vendor continuing from its own header (`sentry-trace`) has a parent the runtime's `traceparent` derivation cannot know. That is the entire integration surface a server-side observer needs from the render — it never owns the head, never re-streams the body, and never has to know the host.

Expand Down
1 change: 1 addition & 0 deletions packages/signals/src/attribution.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type {
Acknowledgement,
Attribution,
AttributionOptions,
AttributionValues,
ChangeKind,
ChangeOrigin,
ChangeRecord,
Expand Down
1 change: 1 addition & 0 deletions packages/signals/src/attribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type {
Acknowledgement,
Attribution,
AttributionOptions,
AttributionValues,
ChangeKind,
ChangeOrigin,
ChangeRecord,
Expand Down
7 changes: 6 additions & 1 deletion packages/signals/src/core/attribution-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,12 @@ export interface AttributionHooks {
export interface InteractionRef {
/** Event type — `click`, `keydown`, `input`… */
type: string;
/** The element hit, e.g. `button#next "Next →"`. */
/**
* The element hit, e.g. `button#next "Next →"` — the tag, then `#id` or
* `[name=…]`, then the element's text in quotes. Describe fully; the
* engine keeps the quoted text as its `values` option allows (the
* record's `target` is its own string, this one is never mutated).
*/
target?: string;
/** Dispatch time on the `performance.now()` clock; defaults to now. */
at?: number;
Expand Down
Loading
Loading