x-trace-history RoadmapA time-travel debugger for BareDOM web components.
BareDOM is built on epochal-time, immutable-value, stateless-component architecture (DOM = f(attrs, props)). That foundation makes time-travel debugging a natural fit — every model transition is already a snapshot. No comparable tool exists for web components today.
x-trace-history is a dev-and-debug tool that consumers of BareDOM (CLJS, JS, TS, Angular, React, Vue, vanilla) load into their app, opt-in. It records every render, every CustomEvent, every attribute mutation, every lifecycle callback, and surfaces them as a navigable timeline with cause→effect chains, value drill-down, search, and shareable JSON exports.
The goal isn't one bug — it's productization. A serious component library needs serious tooling. After x-trace-history ships, BareDOM looks and feels at the level of React DevTools, Stencil DevTools, Storybook. Adopting BareDOM becomes more attractive because the debugging story is no longer "use generic browser DevTools and squint."
A prerequisite shipped earlier (PRs #108, #109): every CustomEvent in BareDOM now flows through one of three named helpers in baredom.utils.dom (dispatch!, dispatch-cancelable!, dispatch-document!). That gives x-trace-history a clean instrumentation chokepoint set with 100% coverage.
.d.ts.?baredom-trace-history URL param or window.BAREDOM_TRACE_HISTORY = true — same opt-in pattern as the existing x-debug.^js hints, gobj/get|set for non-native fields, no monkey-patching.x-debug. See below.x-debugx-debug (the existing element inspector) and x-trace-history solve genuinely different problems and ship as separate, complementary tools:
x-debug = "inspect and tweak the current state of one element" (live, editable, present-tense). Floating panel with checkbox toggles for boolean attributes and text inputs for string/number attributes.x-trace-history = "review what happened across all components" (recorded, read-only, past-tense). Timeline dock with cross-instance lanes, scrubber, causality.They share x-debug-registry for component metadata, and both can be active simultaneously (separate URL params: ?baredom-debug and ?baredom-trace-history; non-colliding z-indices). The split mirrors mainstream tooling: Chrome DevTools (Elements vs Performance), Redux DevTools (state vs time-travel) — users need both views.
Possible late-phase enhancement: light cross-linking — x-trace-history's detail pane gets an "open in inspector" link for the source element; x-debug's panel gets a "show recent trace records" tab. They become two views into a unified surface without merging code.
js->clj if they prefer maps. JS/TS users get an interface TraceRecord type.baredom.utils.dom and baredom.utils.component (one-line (when-some [h *trace-hook*] (h ...))). Negligible cost when off (one nil check). Captures 100% of dispatch + lifecycle + state mutation.defonce atom in baredom.dev.x-trace-history.recorder. Component instances stay stateless.schemaVersion: 1. Future schema evolutions stay backward-compatible.Each PR is sized for ~1 hour of review, ships something visible, and ends green on lint + test compile + release lib. PRs marked (consumer-visible) add or polish features that BareDOM consumers see; (internal) are recorder/instrumentation work.
*trace-hook* extension to dispatch!, dispatch-cancelable!, dispatch-document! in baredom.utils.dom. New baredom.dev.x-trace-history.recorder ns: ring buffer, schema, install/uninstall, JS API at window.BareDOM.traceHistory.{records, pause, resume, clear}. Activated by ?baredom-trace-history. Records every CustomEvent. Tests. (consumer-visible) — open console, see every event flowing through the page.setv!, set-attr!, set-bool-attr!, remove-attr! in du/, plus connectedCallback / attributeChangedCallback / disconnectedCallback via a *lifecycle-hook* in baredom.utils.component/make-element-class. Now records cover full mutation surface. (internal)gobj/set el "__xTraceHistoryId" (next-id!)). Recorder maintains a side-index {componentId → {tag, firstSeen}}. Records carry componentId and tag. Component-id survives disconnect. Tests. (internal)<x-trace-history> element + flat record list. New custom element. Floating dock pattern reused from x-debug.cljs:173-182. Right-side dock initially. Renders a flat list of records (newest first) with tag, event type, payload preview, timestamp. Click a record → expanded JSON detail view. Filter by tag (dropdown reuses x-debug-registry) and event type (checkboxes). (consumer-visible)EventTarget.prototype.dispatchEvent once at activation. Push causeId onto a recorder stack on entry, pop on exit. Every record produced inside that synchronous call carries the outer dispatch's id. Detail pane shows cause-of and effects-from links — clickable to navigate the chain. Document the async-chain limitation in docs/x-trace-history.md. (consumer-visible)(componentId, eventName). Toggle via ?baredom-trace-history=raw for forensic recording. Per-event-type filters (off by default for state/instance-field-set). (internal).trace.json. Toolbar button → Blob download. JSON schema versioned and documented in docs/x-trace-history-schema.md. (consumer-visible).trace.json. Drag-drop on the dock or file-picker. Loaded sessions appear as read-only ghost lanes alongside live recording. Schema-version check; clear error on mismatch. (consumer-visible):lib module + ESM export. ✅ Added baredom.exports.x-trace-history, registered in shadow-cljs.edn :lib :modules, added "./x-trace-history" entry in package.json exports, and wired the dock into baredom.registry/all-registers so the all-bundle ships it. Also decoupled the dock from x-debug-registry (which would have transitively pulled every component into the dev-tool bundle): tags are discovered dynamically from the recorder's observed-components index. Bundle-size budget bumped to accommodate the dev tool — base.js to 56 KB and a per-module override of 20 KB for x-trace-history.js.dist/x-trace-history.d.ts emitted by scripts/generate_types.bb alongside the component .d.ts files. TraceRecord is a discriminated union on type covering all eight record kinds. BareDOMNamespace is exported as a top-level interface so future dev tools can augment it via TypeScript declaration merging.docs/x-trace-history.md. ✅ Expanded the doc into a full user guide: activation, dock anatomy + keyboard shortcuts, complete console-API table, capture-and-share-a-bug-report workflow, recording sessions, import/export, adapter notes (vanilla JS / TypeScript / Angular / React), performance contract, JSON-schema link. README points at the new doc from the Stateless design-principle bullet.viewer.html for sharing traces. ✅ Standalone page at public/viewer.html, deployed alongside the demo site to avanelsas.github.io/baredom/viewer.html. Activates the recorder before loading the all-bundle, then drag-drop or ?trace=<base64> feeds the dock. Dock auto-switches to the freshly-loaded import when live is empty (heuristic only fires on new-import transitions, so an active session with records is never yanked). URL-param decoder supports both standard and URL-safe base64.<input type="search"> in the dock's filter bar. Every record gets a lazily-built lowercase JSON haystack memoised under a gobj key on the record itself, so the recording hot path pays nothing for search and the first keystroke pays one JSON.stringify + toLowerCase per record (subsequent keystrokes reuse the cached strings). Falls back to an empty haystack on cyclic detail payloads. AND-combines with the tag dropdown and category checkboxes.<x-select> in the dock toolbar flips the pane between :timeline and :causality. The causality view builds the tree containing the currently-selected record — causality-root walks up the cause chain, causality-tree discovers descendants, and a post-order tidy layout assigns (cx, cy) centres so a flat node + edge SVG draws in one pass. Each record carries at most one causeId, so the causality structure is a forest of trees rather than a general DAG; the algorithm stays simple (no topological sort, no cycle detection). Clicking a node reuses the existing data-x-th-link-id selection delegate, so timeline ↔ causality keeps the same record selected. Trees over causality-max-nodes (200) show a notice instead of drawing. Fit-to-view auto-scrolls the selected node to the centre of the pane on every mode-switch into causality and on every selection change while in causality — math lives in model/fit-to-view-scroll so it's unit-testable, and the UI layer reads the pane's clientWidth/Height at render-end to apply the scroll. Dock-mode persists across remount alongside axis-mode and the filter spec. Bundle-size budget bumped to 23 KB to accommodate the new pane.:time axis mode, each lane independently decides whether to render as individual dots or as a heatmap band: bin-records-by-x buckets a lane's records into 4 px pixel-aligned columns, and a lane whose max bin exceeds density-threshold (3 records) renders bins instead. Each bin is a <rect> filled with the dominant category's colour at opacity scaled to count vs. the lane's local max (floor 0.4 for WCAG AA 3:1 non-text contrast, ceiling 1.0). The roadmap's "50 events/sec" intuition is implemented as pixel overlap rather than a fixed rate — overlap is the actual rendering problem, and rate-based thresholds are zoom-dependent. The decision is per-lane, so a mixed timeline (one animation-heavy lane + several quiet ones) keeps each lane in the right rendering. Clicking a bin selects its first record (lowest id, deterministic via bin-records-by-x's sort), routed through the existing data-x-th-bin-record-id → click delegate so the detail pane and arrow-stepping keep working. Hover shows a bin-aware tooltip (count + dominant category + first record's tag/time). A thin 2 px pink line marks the selected record's exact x inside its bin so the scrubber-style highlight survives in dense lanes. :order axis skips density entirely — uniform-by-index spacing means no overlap. Same-PR bug fix: handle-pointermove! and start-scrub! now read records from the active view instead of the live buffer, so hovering / scrubbing inside a session or import view correctly resolves records.import "@vanelsas/baredom/x-trace-history" into each adapter's existing test-app/ (React's src/main.tsx, Angular's src/main.ts after zone.js) with a per-adapter TraceHistoryPanel smoke component that polls window.BareDOM.traceHistory.records() and offers a verifier button. Both test-apps type-check (tsc --noEmit) under their existing tsconfigs. Added a register-is-idempotent-test at the dock layer asserting triple register! produces exactly one <x-trace-history> element — protects against React StrictMode double-effects and any bundler that pulls the side-effect entry twice. The dispatchEvent wrapper's stamp guard (already covered in recorder_test.cljs) prevents zone.js + recorder ordering issues; the Angular doc + main.ts comments call out the "zone.js first, trace-history second" import order. Expanded the React + Angular sections of docs/x-trace-history.md from one-line snippets into full guides with StrictMode + zone notes and dev-only gating snippets for Vite / Webpack / Next.js. Added "Dev tools" sections to each adapter's README pointing at the main doc.Phase 8 closed. The trace-history dock now ships with full-text search, causality DAG view with fit-to-view scrolling, heatmap density rendering for animation-heavy lanes, and verified React + Angular integration.
The roadmap touches these files repeatedly:
src/baredom/utils/dom.cljs — hook-point additions in PR 1 + PR 2src/baredom/utils/component.cljs — hook-point addition in PR 2src/baredom/dev/x_trace_history/ (new) — recorder, model, UI, element class. Three layers: model.cljs (pure: schema, ring buffer ops), recorder.cljs (effects: hooks, install/uninstall, causality), x_trace_history.cljs (effects: dock element, UI rendering)src/baredom/exports/x-trace-history.cljs (new in PR 12) — ESM entry pointsrc/baredom/registry.cljs — registration entry in PR 12shadow-cljs.edn — :lib module entry in PR 12package.json — exports entry in PR 12docs/x-trace-history.md (new in PR 14) — user documentationdocs/x-trace-history-schema.md (new in PR 10) — JSON schema referenceEvery PR must pass:
clj-kondo --lint src test — zero new warningsnpx shadow-cljs compile test — zero :infer-warningnpx shadow-cljs release lib — Closure Advanced passes, zero warningstest/baredom/dev/x_trace_history/ for any new pure logic (ring buffer, schema, filters)http://localhost:8000/?baredom-trace-history for any UI/integration changeThis roadmap is intentionally light on per-PR file-level detail. Each PR will get its own focused planning session before implementation — small enough that we can scope-and-go without bloating the roadmap. Value compounds across PRs; we can stop after any phase boundary if priorities change.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |