Skip to content

feat: namespaced, runtime-toggleable trace channels (zero cost when disabled) - #157

Open
fe-lix- wants to merge 3 commits into
mainfrom
feat/debug-tracing
Open

fe-lix- wants to merge 3 commits into
mainfrom
feat/debug-tracing

Conversation

@fe-lix-

@fe-lix- fe-lix- commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Why

Three separate investigations this cycle (SITES-48958, the Universal Editor extension-loading-timeout work, and SITES-49454/RTE load) each hand-rolled the same throwaway instrumentation to see what Host.load()/Port.invokeHostMethod/Guest.invokeChecker were actually doing at runtime — an unconditional console.log, a flag re-checked per call, formatting done by hand at every call site. The formatting mistake (passing an object as a second console.log argument, which collapses to unreadable Array(N)/[{…}] placeholders in a plain-text devtools export) got made and re-fixed three separate times because there was nowhere for the lesson to live.

Each of those investigations also required vendoring a patched tarball into the consuming app just to get visibility — a multi-hour detour purely to answer "which guest is slow, and how long did it take."

What this adds

createTracer(namespace) in @adobe/uix-core (packages/uix-core/src/trace.ts):

  • tracer.enabled is a live getter — a disabled tracer costs one function call + one property read + one typeof check. No string building, no object allocation, no JSON.stringify, ever, while disabled.
  • details may be a plain object or a function returning one. Pass a function whenever building it costs more than a property read (mapping a guest list, serializing an error) — it's only invoked while the namespace is enabled.
  • The tracer owns formatting (inline JSON.stringify, single log line, ISO timestamp) — a call site can no longer reintroduce the collapsing-object bug, because it never touches console.log directly.

window.__UIX_DEBUG__ — enable(pattern) / disable() / list() — lets anyone with devtools access turn tracing on after the fact, at runtime, without rebuilding or redeploying anything:

  • Exact namespace ("uix-host"), trailing wildcard ("uix-*"), or "*" for everything.
  • Persisted via ?uixDebug=... query param or localStorage["uix:debug"], so it survives a reload.
  • Enabled-checks are evaluated live against the current pattern set, so enable("*") (or any pattern) also covers tracers created afterward — no dependency on module load order.

Wired into the three spots that needed ad-hoc vendoring to see previously:

  • Host.addLoadsNewGuests/loadOneGuest (uix-host namespace): batch start/settle with the guest-id list, per-guest connect timing and outcome.
  • Port.invokeHostMethod (uix-host namespace): every call, which host-api namespaces are currently registered for that guest, success/throw (including the async case via a non-blocking .then/.catch — behavior unchanged).
  • Guest.invokeChecker (uix-guest namespace): the unbounded 500ms retry loop wrapping every guest→host call, with a per-call id and attempt counter.

Testing

  • New packages/uix-core/src/trace.test.ts: namespace matching (exact/wildcard/*), disabled-by-default, details-thunk never called while disabled, enable()/disable()/list(), localStorage persistence across a simulated reload, retroactive enable of tracers created after enable("*").
  • New packages/uix-host/src/host.test.ts (no host.test.ts existed on main before this): addLoadsNewGuests/loadOneGuest log batch start/settle and per-guest connect outcomes once uix-host is enabled, and log nothing otherwise (mocks Port entirely — real connection behavior stays covered by the existing port.test.ts).
  • packages/uix-host/src/port.test.ts: new invokeHostMethod tracing block — logs the call plus currently-registered host-api namespaces, logs a throw when the requested namespace hasn't been provided yet (the exact signature that surfaced the Api.ts updateOn: "all" bug in the extension-loading investigation), and logs nothing while disabled.
  • New packages/uix-guest/src/guest.test.ts, plus a uix-guest jest project in jest.config.ts (uix-guest had no wired-up unit tests on main at all before this): invokeChecker logs each attempt/success/failure with an incrementing attempt count and a callId held stable across the whole retry chain, and logs nothing while disabled.
  • Every suite above explicitly asserts the disabled case emits zero console.log calls — the "no behavior/perf impact when off" claim is verified, not just documented.
  • npx tsc --build clean across the whole monorepo.
  • npx jest --selectProjects uix-core uix-host uix-guest — 65 passing, 0 failing, 13/13 suites.
  • npx tsup builds uix-core, uix-host, and uix-guest clean; verified the tracing calls are present in each built dist/index.js.
  • prettier --check clean on all touched files.

No behavior change when tracing is disabled (the default).

🤖 Generated with Claude Code

fe-lix- and others added 3 commits September 25, 2026 10:18
Several investigations into extension-loading/RPC-timing bugs (SITES-48958,
SITES-49454, and the Universal Editor extension-loading-timeout work) each
independently hand-rolled the same throwaway tracing: an unconditional
console.log, gated by a flag re-checked or re-parsed per call, formatted by
hand at every call site. That last part caused the same bug three times --
passing an object as a second console.log argument collapses to unreadable
`Array(N)`/`[{…}]` placeholders in a plain-text devtools export, so every
investigation had to rediscover and fix that convention for itself.

createTracer(namespace) centralizes this:
- enabled is a live getter (cheap property read, no re-parsing), so a
  disabled tracer costs one function call + one property read + one typeof
  check -- no string building, no object allocation, no JSON.stringify.
- details may be a function; it's only invoked while the namespace is
  enabled, so building it (mapping a guest list, serializing an error) never
  happens on the hot disabled path.
- Formatting (inline JSON.stringify into the log message, with a timestamp)
  is owned by the tracer itself, not the call site -- the collapsing-object
  mistake becomes structurally impossible instead of a convention to
  remember.

window.__UIX_DEBUG__ exposes enable(pattern)/disable()/list() at runtime --
query param (?uixDebug=...) or localStorage ("uix:debug") for persistence
across reloads, exact/wildcard/"*" pattern matching, live against whichever
tracers exist at call time (so enable("*") also covers tracers created
afterward, regardless of module load order).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… channels

Wires createTracer into the three spots that took a multi-hour vendored-tarball
detour to instrument ad-hoc in the last two investigations:

- Host.addLoadsNewGuests/loadOneGuest (uix-host namespace): batch start/settle
  with the guest-id list, and per-guest connect start/succeeded/failed with
  duration -- answers "which guest is slow or broken, and how long does the
  whole batch take."
- Port.invokeHostMethod (uix-host namespace): logs every call along with
  which host-api namespaces are currently registered for that guest, and
  whether it succeeded or threw (including the async case, via a
  non-blocking .then/.catch purely for tracing -- behavior is unchanged).
  This is what previously surfaced "host. has no property X" errors caused
  by Api.ts's updateOn: "all" batch-gating bug.
- Guest.invokeChecker (uix-guest namespace): the unbounded 500ms retry loop
  wrapping every guest->host call, with a per-call id and attempt counter --
  answers "how many times did this retry, and for how long."

None of this runs unless a consuming app (or someone in devtools) calls
__UIX_DEBUG__.enable("uix-host,uix-guest"). No behavior change when disabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds basic coverage that the wired-in tracing actually behaves as
designed, at each of the three call sites from the previous commit:

- host.test.ts (new -- no host.test.ts existed on main before this):
  Host.addLoadsNewGuests/loadOneGuest log batch start/settle and
  per-guest connect outcomes once uix-host is enabled, and log nothing
  otherwise (mocks Port entirely; real connection behavior is covered
  by port.test.ts).
- port.test.ts: Port.invokeHostMethod logs the call plus which host-api
  namespaces are currently registered, and logs a throw when the
  requested namespace hasn't been provided -- the exact signature that
  surfaced the Api.ts updateOn: "all" bug in the extension-loading
  investigation.
- guest.test.ts (new -- uix-guest had no wired-up jest project on main;
  added one alongside core/host/host-react): Guest.invokeChecker logs
  each attempt/success/failure with an incrementing attempt count and a
  callId held stable across the whole retry chain, and logs nothing
  while disabled.

Every suite asserts the disabled case emits zero console.log calls --
the core "no behavior/perf impact when off" claim, verified rather than
just documented.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant