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
9 changes: 9 additions & 0 deletions .changeset/observability-surface-prune.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@solidjs/signals": patch
"solid-js": patch
"@solidjs/web": patch
"@solidjs/diagnostics": patch
"@solidjs/compiler": patch
---

Prune the observability surface: `OBSERVE.attribution.install`, the `AttributionHooks` type, `DEV.setConsoleFooter`, the `TraceSlot` and `OriginRef` aliases, `PerformanceTracksOptions.group`, and the untested `solid-js/refresh` runtime modes (`esm`, `webpack5`, `rspack-esm`) are gone — `@solidjs/compiler`'s `transformRefresh({ bundler })` accepts only `"vite" | "standard"` to match. `ownerPath()` is now `OBSERVE.ownerPath(subject)` and `diagnosticGuideUrl()` is `DEV.guideUrl(code)`; `why()` also accepts a scope name. Engine record types (`RerunEvent`, `HoldEvent`, …) live on `solid-js/attribution` only, and `InteractionRef`/`NavigationRef` on the main entries only. `@solidjs/diagnostics` types its record tables off the runtimes' own catalogue (`RecordEvent<K>`), adds the `recovery` table, and bumps the artifact `formatVersion` to 8.
8 changes: 5 additions & 3 deletions documentation/plans/chrome-performance-tracks-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ interaction.at`).
Propagation track read `signal → computed → effect`.
- **Stage 4 — dev enrichments** (`@solidjs/web`, `solid-js`): diagnostics
as Timings markers with `performanceIssue` at `warn`+ (`learnMoreUrl` =
`diagnosticGuideUrl(code)`, the repair guide's section, now exported from
`solid-js`; `info` stays a plain marker; under the scrub only code, kind
the repair guide's section for the code — since landed as
`DEV.guideUrl(code)` (dev tier; the link is omitted in observe builds), the
`diagnosticGuideUrl` export from `solid-js` it began as is gone; `info` stays a plain marker; under the scrub only code, kind
and owner travel); `console.createTask(label)` on the dev component
record (`_component.task`) and every span/marker emitted inside the
nearest component's task so its stack in the panel is the JSX site;
Expand Down Expand Up @@ -159,7 +160,8 @@ transition)`. Shape: `{ ownerPath?, at, shownMs, interaction? }` (item 4's
Tracks"; the subpath stays on `@solidjs/web` because the adapter paints
web records (`call`, `frame`) and inherits the dev/observe/prod tiering.
- Imports only `attribution`, `formatRerun`, `formatOrigin`, the hold
verdicts, and now `diagnosticGuideUrl` — never `costs`/`feedback`, which
verdicts (and reads `OBSERVE.ownerPath` / `DEV.guideUrl` off the runtime
objects) — never `costs`/`feedback`, which
would re-enable the fold tables the engine diet made optional.
- `minMs` is `0` in dev and `0.05` in observe builds; the vendor adapter keeps
its own thresholds (sketch §4.1). The wave span is always painted and
Expand Down
13 changes: 9 additions & 4 deletions documentation/plans/observe-tier-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,14 @@ byte-identical to today under every bundler.
capture, emit }, attribution: { install, installed, withInteraction },
records: { subscribe, observed, emit } }` (the live node travels as a
listener's second argument, not through a lookup); `DEV` = `{ hooks, getChildren, getSignals, getParent,
getSources, getObservers, report, setConsoleFooter }`.
getSources, getObservers, report, setConsoleFooter }`. _(As landed, then
pruned: `attribution.install` is internal to `@solidjs/signals` — only
`installed` is public, as an opaque presence check; `setConsoleFooter` is an
`@internal` seam `solid-js` imports, not a `DEV` member; `OBSERVE` also
carries `ownerPath(subject)`, and `DEV` carries `guideUrl(code)`.)_
- **D6 — The engine is an entry, not a member.** `OBSERVE.attribution` is the
core's side only: the hook slot (`install(hooks)`, `installed`) and the
core's side only: the hook slot (`install(hooks)` — since made internal;
`installed` stays public — and
interaction frame (`withInteraction`, which the web runtime calls on every
dispatch and which is `fn()` with no engine installed). The engine —
`enable/disable/history(type)/why/costs/feedback/markFlight/
Expand Down Expand Up @@ -209,8 +214,8 @@ _Status (2026-09-16)._ Landed, in three pieces:
it); since superseded — the lookup is gone, and the node arrives beside the
record as the listener's second argument (`OBSERVE.records.subscribe("rerun",
(event, live) => …)`, `OBSERVE.diagnostics.subscribe((event, subject) =>
…)`). `@solidjs/diagnostics` stores re-runs verbatim (`RerunRecord` is now
an alias of `RerunEvent`).
…)`). `@solidjs/diagnostics` stores re-runs verbatim (as `RerunEvent`
itself — the `RerunRecord` alias it carried for a while is gone).
- **Clocks: no per-record `ts`.** Every `at` the engine and the runtimes emit
is on the `performance.now()` clock, consistently; a second clock per
record would cost bytes on every record and drift against the first. The
Expand Down
17 changes: 10 additions & 7 deletions documentation/plans/server-dev-build-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ Decision: **reuse `@solidjs/signals`'s channel, do not fork it.**
> **Update 2026-09-08.** `observe-tier-plan.md` PR A landed between P0 and
> P1 and renames what the bullets below refer to: the channel is
> `OBSERVE.diagnostics` (`OBSERVE` exists in dev and observe builds); the
> console face is `DEV.report` / `DEV.setConsoleFooter` (dev only); the server
> console face is `DEV.report` (dev only; the footer is registered through an
> internal seam `solid-js` imports, no longer a `DEV` member); the server
> gates wiring on the `"_SOLID_OBSERVE_"` literal and checks on
> `"_SOLID_DEV_"`; `emit` accepts an explicit `ownerPath`, so the server
> labels its own owners without signals walking `SSROwner._parent`.
Expand Down Expand Up @@ -351,12 +352,14 @@ becomes the contract test for server codes.
> (`null` otherwise — client captures, the browser bridge). _(v6, with C4's
> client half: `artifact.records.{boundary, invocation, frame, call}`,
> folded from the core's `OBSERVE.records` on both platforms and always
> present.)_ The package still
> depends on `@solidjs/signals` alone: it reads the channel by its contract
> (`subscribe(type, listener)`, structurally) and mirrors the record
> types (`BoundaryRecord`, `InvocationRecord`, `FrameRecord`, `CallRecord`); the web server
> suite pins the mirrors to the runtime types at compile time, both ways and
> by key set. JSONL egress adds one line per record. The contract
> present.)_ The package's runtime
> imports are `@solidjs/signals` alone: it reads the channel by its contract
> (`subscribe(type, listener)`). Its record tables are typed off the
> runtimes' own catalogue (`RecordEvent<K>` for `boundary`, `recovery`,
> `invocation`, `frame`, `call`; `solid-js` and `@solidjs/web` are type-only
> peers) — the hand-mirrored `BoundaryRecord`/`InvocationRecord`/`FrameRecord`/
> `CallRecord` this first shipped with are gone; the web server suite pins
> the tables to the runtime types at compile time. JSONL egress adds one line per record. The contract
> test is `packages/web/test/server/diagnostics-server-scenario.spec.tsx`
> (harness aliased from source in `vite.config.server.mjs` and
> `tsconfig.test.json`): the seeded `HEAD_TAG_INVALID` and `SERVER_WRITE`
Expand Down
12 changes: 7 additions & 5 deletions documentation/proposals/production-observability-sketch.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ whether they need dev checks or ride the observe wiring.
vendor-neutral (OTel-shaped where spans make sense). Observability-vendor
consumption is a separate track and must not appear in this package."
`attribution.ts` states the pattern: _one mechanism, N front-ends_ — a
vendor SDK is one more implementer of `AttributionHooks` / subscriber to
the event feeds, never a fork of the engine.
vendor SDK is one more subscriber to the event feeds (`OBSERVE.records`,
`OBSERVE.diagnostics`), never a fork of the engine — and not an
implementer of the engine's hook table, which is internal to
`@solidjs/signals`.

---

Expand Down Expand Up @@ -288,7 +290,7 @@ build.

Serialized as-is: since observe-tier-plan PR B the event carries `nodeId`
instead of the live `node` (in-process consumers get the node as the
listener's second argument, `live`), so `@solidjs/diagnostics`'s `RerunRecord` is the same shape. Attached to the interaction
listener's second argument, `live`), so `@solidjs/diagnostics` stores `RerunEvent` itself. Attached to the interaction
span only above thresholds (4.1); otherwise folded into the span's aggregates.

### 4.5 Cause chain (from `ChangeRecord`)
Expand Down Expand Up @@ -403,8 +405,8 @@ Solid (this repo):
`OBSERVE.records`.)
3. Component-root labeling in the observe build; compiler `name` emission for
user primitives (already a plan item).
4. Serializable projections as exported types (`RerunRecord` exists in
`@solidjs/diagnostics`; the finding/hold/interaction projections should
4. Serializable projections as exported types (`RerunEvent` is already
serializable and is what `@solidjs/diagnostics` stores; the finding/hold/interaction projections should
live next to it — the plan already says protocol types publish from
there).
5. An enabled-engine overhead benchmark.
Expand Down
23 changes: 15 additions & 8 deletions documentation/solid-2.0/08-dev-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Every console report is one entry built for a human to act on:
- The message, with the code in brackets and the repair in the text.
- An `in` line naming the owners enclosing the subject, root first — component roots as `<Name>`, computations by their `name` option or the `effect`/`computed` default (`in <App> › <TodoList> › <TodoRow> › effect`) — a compiled binding effect reads as what it writes (`span.textContent`, `div.class:active`, a hole `div.children`) when the JSX compiler's `sourceNames.bindings` is on (both `@solidjs/babel-plugin` and `@solidjs/compiler` take the same `sourceNames` option, on by default in dev builds alongside `components`); a primitive reads as the identifier it was declared as (`count`, `doubled`, `todos.title`, `createCounter.value` inside a composed primitive) when `@solidjs/compiler`'s standalone `transformSourceNames` pass has run — primitive naming is not a JSX-transform feature: the build tool applies that pass to every module, `.ts`/`.js` and JSX alike, independently of which JSX compiler handles the file (the Vite plugin's `sourceNames.primitives`, on by default in its dev and `observe` postures; solid-vite-plugin #371) — or as its `name` option otherwise. The same chain is `event.ownerPath` on the structured event. A component's name is the tag as written in source when the JSX compiler's `sourceNames.components` is on (`createComponent(Home, props, "Home")` — on by default in dev builds; the Vite plugin also enables it for the `observe` posture, so minified observe builds still read `<Home>`), otherwise the function's `.name`, which a minifier rewrites and a `lazy()` wrapper hides. Components a library invokes by value rather than by tag (a router rendering a route's `component`) carry only the function name.
- For a compiled JSX binding effect (attribute, class, style, property, spread, insert), the element it writes as a second console argument — hover highlights it on the page, click jumps to it in the Elements panel. The web runtime tags binding effects with their element in dev; the core prints whatever the subject knows.
- The first report of each code ends with a footer registered by `solid-js` (`DEV.setConsoleFooter`): the installed repair skill path (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) and the same file's stable GitHub URL anchored to the code's section. Perf, graph, and responsiveness codes add a second line pointing at `attribution.enable()` from `solid-js/attribution` and the `agent-loops` skill in `@solidjs/diagnostics`.
- The first report of each code ends with a footer `solid-js` registers through an internal seam of the engine (not a public `DEV` method): the installed repair skill path (`node_modules/solid-js/skills/reactivity-diagnostics/SKILL.md`) and the same file's stable GitHub URL anchored to the code's section — `DEV.guideUrl(code)`, the one place that URL is built, so a profiler track's Insights link (in dev) and the console footer agree. Perf, graph, and responsiveness codes add a second line pointing at `attribution.enable()` from `solid-js/attribution` and the `agent-loops` skill in `@solidjs/diagnostics`.

Attribution's own output (`[why-run]` chains) prints as collapsed console groups — one headline per re-run, the cause chain and dependency delta inside.

Expand Down Expand Up @@ -696,7 +696,7 @@ Check (`warn`, dev only; server components). A behavior position (an event handl

## Programmatic diagnostics API

In dev and observe builds, `OBSERVE.diagnostics` provides two methods for tooling (and `OBSERVE.exclude`/`isExcluded`, described under attribution, mark an observer's own subtree so neither channel reports it):
In dev and observe builds, `OBSERVE.diagnostics` provides two methods for tooling (and `OBSERVE.exclude`/`isExcluded`, described under attribution, mark an observer's own subtree so neither channel reports it; `OBSERVE.ownerPath(subject)`, below, is the labelling the channel's events carry, for a consumer that holds a live handle):

### `OBSERVE.diagnostics.subscribe(listener)`

Expand Down Expand Up @@ -724,6 +724,14 @@ const events = capture.stop();
// events: DiagnosticEvent[]
```

### `DEV.guideUrl(code)`

Dev builds only. The repair guide's section for a code — the `reactivity-diagnostics` skill's stable GitHub URL, anchored to the code (`…/SKILL.md#strict_read_untracked`). One place builds it: the console footer's "learn more" line and the profiler track's Insights link (`learnMoreUrl`, in dev) both read it, so a tool that renders findings elsewhere links to the same text. Dev-tier rather than observe because it is guidance for a developer, and the URL string on a retained object would be a cost every observe build paid.

### `OBSERVE.ownerPath(subject)`

The owner chain of a live owner or node, root first, as the events carry it (`["<App>", "<TodoRow>", "effect"]` — `event.ownerPath` on a finding, the `ownerPath` field on a record); `undefined` for `null`/`undefined` and for a subject with no named owner above it. For a consumer holding the live handle the channel passed beside an event — the profiler track labelling a span by the computation it received — rather than a copy that already left the process, which carries the path itself.

Each `DiagnosticEvent` has:

| Field | Type | Description |
Expand Down Expand Up @@ -925,7 +933,7 @@ Beyond the always-on diagnostics above, dev and observe builds ship an opt-in **
← signal "notifications" write (#5) 2 → 3
```

The engine is its own entry, `solid-js/attribution` (re-exporting `@solidjs/signals/attribution`), so a build that never imports it never ships it: the runtime carries only the hook slot the engine installs into (`OBSERVE.attribution.install`) and the two declared frames — the interaction frame the web runtime opens around event dispatch (`OBSERVE.attribution.withInteraction`) and the origin frame a router opens around its navigation write (`OBSERVE.attribution.withOrigin`). The import is legal in every tier — the prod tier resolves an inert engine with the same surface, so app code needs no per-tier guard.
The engine is its own entry, `solid-js/attribution` (re-exporting `@solidjs/signals/attribution`), so a build that never imports it never ships it: the runtime carries only the hook slot the engine installs into (internal to `@solidjs/signals`; `OBSERVE.attribution.installed` says whether an engine is present) and the two declared frames — the interaction frame the web runtime opens around event dispatch (`OBSERVE.attribution.withInteraction`) and the origin frame a router opens around its navigation write (`OBSERVE.attribution.withOrigin`). The import is legal in every tier — the prod tier resolves an inert engine with the same surface, so app code needs no per-tier guard.

### API (`solid-js/attribution`)

Expand Down Expand Up @@ -1019,9 +1027,9 @@ OBSERVE
// undefined — and puts it on its record (the web runtime's "call" record
// does this at dispatch); an observer then joins the two by identity.
const origin = OBSERVE.attribution.currentOrigin();
// An external engine (devtools) installs into the same slot the built-in
// one uses: OBSERVE.attribution.install(hooks) / .installed. The installed
// hooks are also registered on globalThis under
// Whether an engine is present: OBSERVE.attribution.installed (the hook
// table itself is opaque — the slot is the engine's, not a public seam).
// The installed hooks are also registered on globalThis under
// Symbol.for("@solidjs/signals/observe/attribution"), the records channel's
// reach for a layer bundled without a framework import.
// An observer that renders inside the app it watches (an APM adapter's
Expand Down Expand Up @@ -1115,7 +1123,6 @@ const disable = enablePerformanceTracks({
minMs: 0, // floor for run spans; 0 in dev, 0.05 in observe builds
rich: true, // performance.measure with tooltips/properties (dev default) vs console.timeStamp
scrub: false, // drop value previews and element text (observe default)
group: "Solid", // the track group
attribution: {} // options for the engine hold it takes (log: false by default)
});
```
Expand All @@ -1132,6 +1139,6 @@ Findings become markers: every `DiagnosticEvent` delivered while enabled is a ma

The engine is decoupled from the core through a narrow dev-only hook surface (`attribution-hooks.ts`): the core's only obligation is to report true facts (recompute start/end with lane and transition posture, committed writes, async landings, refreshes) at the moments they happen. All semantics — stamps, cause chains, timings, thresholds — live in the engine. Disabled cost is one null check per hook site; production builds fold every site out entirely (the size guard enforces byte-parity).

The same hook surface is the intended substrate for external devtools: install your own `AttributionHooks` implementation instead of the built-in engine — one mechanism, two front-ends.
External devtools build on the engine's public face — `attribution.enable()` plus `OBSERVE.records` — not on the hook table, which is internal to `@solidjs/signals` (a devtools engine that replaced the built-in one would be a change to the package, not an integration).

Naming: attribution output uses debug names from the `name` option on primitives (`createSignal(0, { name: "count" })`); store nodes are named `store.path` automatically while the engine is active. Unnamed nodes fall back to their owner id.
10 changes: 9 additions & 1 deletion packages/compiler/__tests__/refresh-options.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,16 @@ describe("transformRefresh options", () => {

it("rejects unsupported bundlers", () => {
expect(() => transformRefresh(CODE, { ...OPTIONS, bundler: "webpack" })).toThrow(
/`bundler` option must be "esm", "vite", "webpack5", "rspack-esm" or "standard"/
/`bundler` option must be "vite" or "standard"/
);
// The modes `solid-js/refresh` dropped: the runtime probes `module.hot`
// / `import.meta.webpackHot` itself under `standard`, and Snowpack's
// `esm` shape is Vite's.
for (const bundler of ["esm", "webpack5", "rspack-esm"]) {
expect(() => transformRefresh(CODE, { ...OPTIONS, bundler })).toThrow(
/`bundler` option must be "vite" or "standard"/
);
}
});

it("rejects the unported JSX-granularity mode", () => {
Expand Down
13 changes: 0 additions & 13 deletions packages/compiler/__tests__/refresh/fixtures/bundler-esm/code.jsx

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading