Devtools
What's new in 1.14.0
Devtools graduated to a batteries-included tier in 1.14.0 with a ring-buffered timeline (maxTimelineEntries, default 1000), expanded TimelineEntry payloads, new event types (signal:create / signal:dispose, effect:dispose, component:mount / unmount / render, route:guard, error:caught, measure, mark), filterable + subscribable timelines (filterTimeline, subscribeTimeline), structural diffSignals / diffStores, signal traces (traceSignal / untraceSignal), inspectEffects, snapshot import/export (exportDevtoolsSnapshot / importDevtoolsSnapshot), an installBrowserBridge() for extension panels, and performance helpers (time, measureRender, getPerformanceSummary). See the 1.14.0 release notes.
The devtools module provides lightweight runtime inspection utilities for debugging signals, stores, custom elements, and event timelines during development. It is designed for diagnostics and development feedback — not production analytics.
import {
clearTimeline,
enableDevtools,
generateSignalLabel,
getDevtoolsState,
getTimeline,
inspectComponents,
inspectSignals,
inspectStores,
isDevtoolsEnabled,
logComponents,
logSignals,
logStores,
logTimeline,
recordEvent,
trackSignal,
untrackSignal,
// 1.14+ extensions
filterTimeline,
subscribeTimeline,
diffSignals,
diffStores,
traceSignal,
untraceSignal,
inspectEffects,
exportDevtoolsSnapshot,
importDevtoolsSnapshot,
installBrowserBridge,
time,
measureRender,
getPerformanceSummary,
} from '@bquery/bquery/devtools';What's new in 1.14
Configurable ring buffer
The timeline is capped at maxTimelineEntries events (default 1000) so long-running sessions never grow without bound.
enableDevtools(true, { maxTimelineEntries: 500 });Timeline filtering & subscriptions
const failures = filterTimeline({ types: ['error:caught'], since: Date.now() - 60_000 });
const off = subscribeTimeline((entry) => console.debug(entry));Entries now carry optional payload, source, and duration. New event types: signal:create, signal:dispose, effect:dispose, component:mount, component:unmount, component:render, route:guard, error:caught, measure, mark.
Inspection upgrades
inspectSignals({ includeValues: false }); // privacy-friendly snapshot
diffSignals(prev, next); // structural diff
traceSignal('cart.total');
inspectEffects(); // reactive effects created with effect()Snapshot export / import
const snap = exportDevtoolsSnapshot();
// Save snap to a file, send with a bug report, etc.
const replay = importDevtoolsSnapshot(JSON.stringify(snap));Browser bridge
installBrowserBridge(); // mirrors events to window.__BQUERY_DEVTOOLS__.eventsPerformance helpers
const value = time('expensive', () => compute());
measureRender('my-card', () => render());
getPerformanceSummary(); // counts + averages per event typeGetting Started
Enable devtools once at the start of your application. All devtools functionality is gated by this toggle — tracking, recording, and logging only occur when devtools are active.
import { enableDevtools, isDevtoolsEnabled } from '@bquery/bquery/devtools';
enableDevtools(true, { logToConsole: true });
console.log(isDevtoolsEnabled()); // trueWhen logToConsole is true, every timeline event is also printed to console.log in real time.
Signal Tracking
Register signals with human-readable labels so you can inspect them later. Tracked signals appear in inspectSignals() and logSignals().
trackSignal(label, peek, subscriberCount)
function trackSignal(label: string, peek: () => unknown, subscriberCount: () => number): void;| Parameter | Type | Description |
|---|---|---|
label | string | A non-empty, human-readable label for the signal |
peek | () => unknown | A function that returns the current value without tracking |
subscriberCount | () => number | A function returning the current subscriber count |
Throws: If label is empty.
import { signal } from '@bquery/bquery/reactive';
import { trackSignal } from '@bquery/bquery/devtools';
const count = signal(0);
// Reusing a label replaces the previously tracked entry
trackSignal(
'counter',
() => count.peek(),
() => 0
);untrackSignal(label)
function untrackSignal(label: string): void;Removes a previously tracked signal by its label. Safe to call if the label was never tracked.
import { untrackSignal } from '@bquery/bquery/devtools';
untrackSignal('counter');generateSignalLabel()
function generateSignalLabel(): string;Generates unique, auto-incrementing labels such as signal_0, signal_1, etc. Useful when you need to track signals programmatically without manually naming them.
import { generateSignalLabel, trackSignal } from '@bquery/bquery/devtools';
import { signal } from '@bquery/bquery/reactive';
const s = signal('hello');
const label = generateSignalLabel(); // 'signal_0'
trackSignal(
label,
() => s.peek(),
() => 0
);Runtime Inspection
These functions return snapshot data about the current state of tracked signals, stores, and custom elements.
inspectSignals()
function inspectSignals(): SignalSnapshot[];Returns an array of all tracked signals with their current values.
import { inspectSignals } from '@bquery/bquery/devtools';
const signals = inspectSignals();
// [{ label: 'counter', value: 42, subscriberCount: 3 }]inspectStores()
function inspectStores(): StoreSnapshot[];Lists all stores registered with @bquery/bquery/store, along with their current state.
import { inspectStores } from '@bquery/bquery/devtools';
const stores = inspectStores();
// [{ id: 'user', state: { name: 'Ada', loggedIn: true } }]inspectComponents()
function inspectComponents(): ComponentSnapshot[];Lists custom elements that are both registered and currently instantiated in the DOM, along with instance counts.
import { inspectComponents } from '@bquery/bquery/devtools';
const components = inspectComponents();
// [{ tagName: 'ui-button', instanceCount: 7 }]getDevtoolsState()
function getDevtoolsState(): DevtoolsState;Returns a complete snapshot of the devtools module state: whether it's enabled, the current options, and the full timeline.
import { getDevtoolsState } from '@bquery/bquery/devtools';
const state = getDevtoolsState();
console.log(state.enabled); // true
console.log(state.options.logToConsole); // true
console.log(state.timeline.length); // 5Console Logging
For quick debugging sessions, use the logging helpers which pretty-print data to the browser console as tables.
logSignals()
function logSignals(): void;Prints a formatted table of all tracked signals to the console.
import { logSignals } from '@bquery/bquery/devtools';
logSignals();
// Console table: label | value | subscriberCountlogStores()
function logStores(): void;Prints a formatted table of all stores and their state to the console.
import { logStores } from '@bquery/bquery/devtools';
logStores();
// Console table: id | statelogComponents()
function logComponents(): void;Prints a formatted table of all custom elements to the console.
import { logComponents } from '@bquery/bquery/devtools';
logComponents();
// Console table: tagName | instanceCountTimeline
The timeline records a log of reactive events in your application. This is useful for debugging complex signal/effect/store interactions and understanding the order of operations.
recordEvent(type, detail)
function recordEvent(type: TimelineEventType, detail: string): void;Records a custom event into the timeline. When logToConsole is enabled, the event is also printed immediately.
| Parameter | Type | Description |
|---|---|---|
type | TimelineEventType | One of 'signal:update', 'effect:run', 'store:patch', 'store:action', 'route:change' |
detail | string | A human-readable description of what happened |
import { recordEvent } from '@bquery/bquery/devtools';
recordEvent('signal:update', 'count changed from 0 to 1');
recordEvent('store:action', 'user/login called');
recordEvent('route:change', 'navigated to /dashboard');getTimeline()
function getTimeline(): readonly TimelineEntry[];Returns the full timeline log as a read-only array.
import { getTimeline } from '@bquery/bquery/devtools';
const entries = getTimeline();
for (const entry of entries) {
console.log(`[${entry.type}] ${entry.detail} @ ${entry.timestamp}`);
}logTimeline(last?)
function logTimeline(last?: number): void;Pretty-prints the timeline to the console. Optionally limits output to the last N entries.
import { logTimeline } from '@bquery/bquery/devtools';
logTimeline(); // All entries
logTimeline(10); // Only the 10 most recent entriesclearTimeline()
function clearTimeline(): void;Removes all recorded timeline entries.
import { clearTimeline } from '@bquery/bquery/devtools';
clearTimeline();Type Definitions
SignalSnapshot
interface SignalSnapshot {
readonly label: string;
readonly value: unknown;
readonly subscriberCount: number;
}StoreSnapshot
interface StoreSnapshot {
readonly id: string;
readonly state: Record<string, unknown>;
}ComponentSnapshot
interface ComponentSnapshot {
readonly tagName: string;
readonly instanceCount: number;
}TimelineEventType
type TimelineEventType =
| 'signal:update'
| 'effect:run'
| 'store:patch'
| 'store:action'
| 'route:change';TimelineEntry
interface TimelineEntry {
readonly timestamp: number;
readonly type: TimelineEventType;
readonly detail: string;
}DevtoolsOptions
interface DevtoolsOptions {
/** Whether to log timeline events to console in real time. Default: `false`. */
logToConsole?: boolean;
}DevtoolsState
interface DevtoolsState {
readonly enabled: boolean;
readonly options: Readonly<DevtoolsOptions>;
readonly timeline: readonly TimelineEntry[];
}Full Example
import { signal, effect } from '@bquery/bquery/reactive';
import {
enableDevtools,
trackSignal,
recordEvent,
inspectSignals,
logTimeline,
clearTimeline,
} from '@bquery/bquery/devtools';
// 1. Enable devtools with console logging
enableDevtools(true, { logToConsole: true });
// 2. Create and track a signal
const count = signal(0);
trackSignal(
'count',
() => count.peek(),
() => 0
);
// 3. Record events as your app runs
effect(() => {
recordEvent('signal:update', `count is now ${count.value}`);
});
count.value = 1;
count.value = 2;
// 4. Inspect and log
console.log(inspectSignals());
// [{ label: 'count', value: 2, subscriberCount: 0 }]
logTimeline();
// Prints all recorded events to the console
// 5. Clean up
clearTimeline();Notes
- Intended for development and diagnostics, not production analytics.
- Pairs nicely with
@bquery/bquery/testingwhen you want assertions over reactive behavior. - All inspection methods return snapshot copies, not live references.
- Timeline events include millisecond timestamps for performance analysis.
Pitfalls and gotchas
- Timeline uses a ring buffer (default 1000 entries via
maxTimelineEntries); long sessions overwrite old events. inspectSignals({ includeValues: false })is the privacy-aware default — passtrueonly in trusted dev contexts.installBrowserBridge()opens apostMessagechannel; remove it before going to production.- Snapshot export/import is structural only — it cannot reattach reactive subscribers, just inspect their shape.
- Performance helpers (
time,measureRender,mark,measure) useperformance.mark/performance.measure— they show up in browser devtools.
Performance notes
- Disable devtools in production via tree-shaking by importing only in
import.meta.env.DEVbranches. filterTimeline({ types, since, until, search })is far cheaper than iterating snapshots in user code.
Testing this module
- Combine
traceSignal()/untraceSignal()withbun:testassertions to verify reactive flow. diffSignals/diffStoresmake snapshot diffs reviewable.
Related modules
- Reactive — the signals being inspected.
- Store — store inspection helpers.
- Testing — ships its own reactive harnesses.
Version history
- 1.14.0 — ring-buffered timeline, expanded
TimelineEntry, new event types,filterTimeline,subscribeTimeline, privacy-awareinspectSignals,diffSignals/diffStores,traceSignal/untraceSignal,inspectEffects, snapshot import/export,installBrowserBridge, perf helpers.