A framework-agnostic debugging utility that records DOM mutations as typed, serializable events through an explicit, side-effect-free package API.
- Attribute, child-list, and character-data observation
- Serializable events without native DOM nodes or
MutationRecordobjects - Strict TypeScript types and declaration maps
- Side-effect-free ESM and CommonJS package entries
- Console presentation and visual highlighting
- Bounded history, deduplication, subscriptions, and explicit lifecycle control
- Reproducible Chromium performance benchmarks with machine-readable results
- Zero runtime dependencies
pnpm add --save-dev orbi-tationImporting the package does not start an observer or change the page.
import { createTracker } from "orbi-tation";
const tracker = createTracker();
const unsubscribe = tracker.subscribe((event) => {
console.log(event);
});
tracker.start();
// Later:
unsubscribe();
tracker.stop();To scope tracking to one subtree, pass a root node or a selector. Selectors are
resolved when start() runs so the target can be created after the tracker
instance:
const tracker = createTracker({ root: "#app" });
tracker.start();Installing the package as a devDependency does not by itself keep it out of a
production browser bundle. Guard a lazy import with a compile-time development
constant so the production build can remove the import and tracker code:
if (import.meta.env.DEV) {
void Promise.all([import("orbi-tation"), import("orbi-tation/panel")]).then(
([{ createTracker }, { createPanel }]) => {
const tracker = createTracker();
const panel = createPanel(tracker);
panel.mount();
tracker.start();
},
);
}import.meta.env.DEV is the Vite-style spelling. Other bundlers should replace
an equivalent compile-time constant with false in production. Keep the guard
directly around the dynamic import so dead-code elimination can remove the
entire chunk; a runtime-only setting cannot provide that guarantee.
The package does not inspect the host application's environment. If production code explicitly imports and starts a tracker, it will run. This explicit-opt-in behavior keeps the core bundler-neutral and avoids unreliable environment detection; production exclusion therefore remains the host build's responsibility. Every public entry is side-effect-free, no entry auto-starts, and no browser global is exposed as an alternate initialization path.
The optional panel entry owns presentation behavior so the core remains free of UI side effects.
import { createTracker } from "orbi-tation";
import { createPanel } from "orbi-tation/panel";
const tracker = createTracker();
const panel = createPanel(tracker, {
highlightColor: "#ff0000",
highlightDuration: 3000,
});
panel.mount();
tracker.start();
// Cleanup:
tracker.stop();
panel.unmount();Options:
| Option | Default | Description |
|---|---|---|
root |
document.body |
DOM node or selector observed after start() |
maxEvents |
100 |
Positive integer event-history limit |
dedupeWindowMs |
50 |
Non-negative duplicate suppression window |
onError |
console.error |
Receives normalized record and listener failures |
The returned tracker provides:
start()— begin observing; repeated calls are safe.stop()— disconnect the observer; repeated calls are safe.clear()— clear history and deduplication state without stopping.getEvents()— return a readonly snapshot of normalized events.subscribe(listener)— receive events and return an idempotent unsubscribe function.
The root may be an Element, Document, or ShadowRoot/DocumentFragment.
Only the configured root and its descendants are observed. Changing roots
requires creating a new tracker instance; stop()/start() preserves the root
configured at creation time.
Use root for coarse subtree scoping. Fine-grained include/exclude selector
filters and automatic discovery of open Shadow DOM roots are separate planned
capabilities; for now, pass an open ShadowRoot explicitly when that is the
subtree you want to inspect.
Invalid configuration fails synchronously. Starting without an available root,
with a selector that matches nothing, with an unsupported root node, or without
MutationObserver throws a TrackerError with a stable error code.
TrackerMutationEvent is a discriminated union of:
TrackerAttributeEventTrackerChildListEventTrackerCharacterDataEvent
Every event has a monotonic sequence, ISO timestamp, type, and serializable
target description. Child nodes are represented by compact summaries. Events
do not expose live DOM nodes or native mutation records.
tracker.subscribe((event) => {
if (event.type === "attributes") {
console.log(event.attributeName, event.oldValue, event.newValue);
}
});See docs/ARCHITECTURE.md for the complete public contract and module boundaries.
pnpm build creates:
dist/index.jsanddist/index.cjs— side-effect-free core entrydist/panel.jsanddist/panel.cjs— optional presentation entry.d.ts,.d.cts, declaration maps, and JavaScript source mapssrc/— TypeScript sources referenced by declaration maps
Package contents and both ESM and CommonJS imports are verified from the packed
tarball by pnpm test:package.
Installing dependencies also installs the repository's Lefthook-managed Git hooks. Pre-commit hooks check staged files with Prettier and ESLint and prevent commits on protected branches. Pre-push hooks run type-checking and unit tests. CI repeats these checks as the authoritative merge gate.
pnpm format
pnpm format:check
pnpm typecheck
pnpm test
pnpm test:browser:install
pnpm test:browser
pnpm test:package
pnpm test:production
pnpm --silent benchmarkpnpm verify runs strict type-checking, unit tests, builds, package packing,
ESM/CommonJS smoke imports, the focused Chromium integration suite, and a
production tree-shaking fixture that verifies guarded imports are removed.
pnpm --silent benchmark measures low-volume latency, burst processing,
large-subtree updates, bounded long-running sessions, retained heap and event
payloads, and current panel-presentation overhead in headless Chromium. Results
are environment-specific and are not enforced as brittle shared-CI thresholds.
See docs/PERFORMANCE.md for the baseline environment,
workloads, initial budgets, and interpretation rules.
- Observation defaults to
document.body; passrootfor another DOM node or selector. - Closed Shadow DOM and iframe contents are not observed automatically. Open
ShadowRootinstances can be observed explicitly; automatic discovery is tracked separately. - Selectors describe the target at mutation-processing time and may become stale after later DOM changes.
- Large mutation volumes still have runtime cost despite bounded history and deduplication; measure representative application workloads against the documented benchmark rather than assuming negligible overhead.
The package targets the latest two releases of Chrome, Firefox, Safari, and Edge. Internet Explorer is not supported.
See the product roadmap for milestones and recommended implementation order.
This project is released under the MIT License.