Observability
Observability is a first-class architectural primitive in @banksia/signals. Rather than relying on heavyweight profiler plugins or monkey-patching methods at runtime, the core reactivity engine and microtask scheduler emit low-level, deterministic lifecycle events that can be consumed by any medium:
- Browser DevTools: Inspect state, trace dependency graph topology, and review mutation histories via
window.__BANKSIA_SIGNALS_DEVTOOLS__.
- Agentic MCP Tooling: Connect autonomous LLM coding agents via the Chrome DevTools MCP to safely audit state snapshots and verify reactive invariants.
- Stream-Based Telemetry & APM: Route mutation events and performance durations to OpenTelemetry, Datadog, Prometheus, or custom logging pipelines.
- Analytical & Testing Engines: Create targeted audit loggers to enforce domain model invariants and track transactional state transitions in unit and integration test suites.
How Observability Works
Observability in @banksia/signals is powered by six deterministic lifecycle hooks that fire throughout the reactive execution cycle:
onTrack(source, consumer): Fired when a consumer (an effect or computed) reads a reactive property, capturing the active dependency edge between the state source and consumer.
onNotify(source, change): Fired synchronously whenever a state property or collection item mutates, providing { oldValue, newValue, timestamp } before downstream reactions execute.
onSchedule(consumer): Fired when a subscriber is flagged dirty and queued into the microtask batch scheduler.
onExecute(consumer, context): Fired immediately before (phase: "start") and after (phase: "end") an effect or computed evaluation, measuring execution duration in milliseconds and tracking execution errors.
onBatch(stats): Fired across scheduler microtask cycles (phase: "start" | "flush" | "end"), reporting queued subscriber depths and batch transaction sizes.
onError(error, context): Fired when circular dependencies, infinite mutation cascades, or runtime exceptions occur.
When no hooks or DevTools bridges are registered, the engine uses fast-path boolean counters to completely bypass metadata creation and event dispatch. Unobserved production deployments incur zero string allocations, zero object cloning, and zero runtime performance overhead.
Universal Telemetry Pipeline
┌─────────────────────────────────────────────────────────────┐
│ Reactive Operation │
│ (property read, signal write, collection mutation) │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Internal Hooks Dispatcher │
│ - onTrack (dependency edge registered) │
│ - onNotify (mutation invalidation triggered) │
│ - onSchedule (subscriber queued in batch) │
│ - onExecute (consumer execution start / end) │
│ - onBatch (scheduler batch start / flush / end) │
│ - onError (uncaught exception handling) │
└──────────────┬──────────────────────────────┬───────────────┘
│ │
▼ ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ DevTools & Agent Bridge │ │ Custom Telemetry Hubs │
│ - Ring Buffer (FIFO) │ │ - createReactivityHub() │
│ - Private field redaction │ │ - Metrics & OpenTelemetry │
│ - Chrome DevTools MCP │ │ - Stream analytical engines│
│ - Sandboxed State Queries │ │ - Test audit loggers │
└─────────────────────────────┘ └─────────────────────────────┘
How to Hook into Observability Primitives
Developers can tap into observability across four different levels of granularity:
1. Global Lifecycle Hooks (registerReactivityHooks)
Attach system-wide listeners to pipe reactivity events into APM tools, logging platforms, or metrics aggregators:
import { registerReactivityHooks } from "@banksia/signals";
const unsubscribe = registerReactivityHooks({
// Track dependency edges:
onTrack(source, consumer) {
console.debug(`[Track] ${consumer.name} -> ${source.meta.label}`);
},
// Monitor property mutations:
onNotify(source, change) {
console.log(
`[Mutation] ${source.meta.label}:`,
change.oldValue,
"➜",
change.newValue,
);
},
// Profile execution performance:
onExecute(consumer, context) {
if (context.phase === "end") {
console.debug(`[Executed] ${consumer.name} in ${context.durationMs}ms`);
}
},
// Monitor batch scheduler flushes:
onBatch(stats) {
if (stats.phase === "flush") {
console.debug(`[Batch] Flushed ${stats.flushedCount} reactive reactions`);
}
},
// Catch reactive errors and circular loops:
onError(error, context) {
console.error(`[Reactivity Error in ${context.phase}]`, error);
},
});
// Teardown hooks when done:
unsubscribe();
2. Target-Scoped Observers (observe)
Monitor mutations and dependency tracking for a specific domain model or store instance without noise from the rest of the application:
import { observe, makeReactive } from "@banksia/signals";
const cartStore = makeReactive({ items: [], total: 0 });
const stopObserving = observe(cartStore, {
onNotify(source, change) {
auditLogger.record({
target: source.meta.targetName,
field: source.meta.property,
oldValue: change.oldValue,
newValue: change.newValue,
timestamp: change.timestamp,
});
},
});
3. Dedicated Telemetry Hubs (createReactivityHub)
Create isolated, filterable telemetry pipelines with custom predicates (e.g. routing only public properties or specific event types):
import { createReactivityHub } from "@banksia/signals";
// Create a hub that filters out private or internal domain properties:
const publicAnalyticsHub = createReactivityHub({
filter: (source) => !source.meta.isPrivate,
});
publicAnalyticsHub.use({
onNotify(source, change) {
analyticsClient.track("State Change", {
property: source.meta.label,
value: change.newValue,
});
},
});
// Clear all attached hub hooks:
publicAnalyticsHub.clear();
4. DevTools & Agentic MCP Bridge (initDevTools)
Mount the runtime inspection bridge on window.__BANKSIA_SIGNALS_DEVTOOLS__ for browser DevTools and AI coding agents:
import { initDevTools } from "@banksia/signals/devtools";
import { userStore } from "./stores/user-store";
import { cartStore } from "./stores/cart-store";
const devtools = initDevTools({
maxBufferSize: 500, // Bounded FIFO ring buffer capacity
recordNotifyEvents: true, // Record property mutations
recordExecuteEvents: true, // Profile effect & computed runtimes
recordBatchEvents: true, // Track scheduler flushes
sanitizePrivate: true, // Automatically redact private fields
});
// Register domain stores with recognizable names for indexing:
devtools.registerTarget(userStore, "UserStore");
devtools.registerTarget(cartStore, "CartStore");
DevTools Event Payloads
When the DevTools bridge is active, it records discriminated event payloads into its bounded FIFO ring buffer:
1. DevToolsMutationEvent (type: "notify")
Recorded whenever a reactive signal, property, or collection item mutates:
| Field |
Type |
Description |
type |
"notify" |
Discriminator identifier |
label |
string |
Source diagnostic label (e.g. 'CartStore.items') |
property |
string |
Property key or symbol that changed |
oldValue |
unknown |
Sanitized previous value |
newValue |
unknown |
Sanitized updated value |
timestamp |
number |
High-resolution epoch timestamp (ms) |
2. DevToolsExecuteEvent (type: "execute")
Recorded before and after an effect or computed signal runs:
| Field |
Type |
Description |
type |
"execute" |
Discriminator identifier |
consumerName |
string |
Diagnostic name of the consumer |
consumerType |
string |
'effect' | 'computed' | 'adapter' |
phase |
"start" | "end" |
Execution lifecycle phase |
durationMs |
number? |
Duration in milliseconds (provided on end) |
timestamp |
number |
High-resolution epoch timestamp (ms) |
3. DevToolsBatchEvent (type: "batch")
Recorded during scheduler microtask batch cycles:
| Field |
Type |
Description |
type |
"batch" |
Discriminator identifier |
phase |
"start" | "flush" | "end" |
Batch lifecycle phase |
flushedCount |
number? |
Number of reactions notified (on "flush") |
timestamp |
number |
High-resolution epoch timestamp (ms) |
Sanitization and Privacy
The observability engine enforces privacy by default so sensitive credentials or secrets are never leaked into public telemetry or inspection streams:
- Leading Underscores: Properties beginning with
_ (e.g. _sessionKey, _refreshToken) are automatically replaced with "[REDACTED_PRIVATE]".
- Private Meta Flags: Sources explicitly labeled with
isPrivate: true are redacted.
- Configurable: Pass
sanitizePrivate: false in DevToolsOptions for trusted, local debugging environments.