Skip to content

Devtools

Source: web/sdk/src/devtools/EventMonitor.ts

EventMonitor is a static class — a process-wide event recorder useful for programmatic logging, test assertions, or building custom dev panels. Unlike <UnrealDevPanel>, it has no UI and no React dependency; you call it from anywhere in your app (or browser console).

TIP

For an out-of-the-box visual debugger, use <UnrealDevPanel>. Reach for EventMonitor when you want raw history access — e.g. a Playwright test asserting 'skill:cast' was sent.

Why static?

Every method on EventMonitor is static. There is a single global recorder for the whole page; you don't construct instances. This makes the recorder cheap to enable from anywhere (a test, a console session, a debug build) without threading an object through your component tree.

ts
import { EventMonitor } from 'unreal-react-bridge';

EventMonitor.enable();
// ... interact with the app ...
console.table(EventMonitor.getEventHistory());
EventMonitor.disable();

API

EventMonitor.enable()

ts
static enable(): void;

Turns recording on. Until enabled, recordSentEvent / recordReceivedEvent are no-ops. Idempotent.

EventMonitor.disable()

ts
static disable(): void;

Turns recording off. Existing history is kept until you call clearHistory().

EventMonitor.isMonitoring()

ts
static isMonitoring(): boolean;

Returns the current enabled state.

EventMonitor.recordSentEvent(eventName, data)

ts
static recordSentEvent(eventName: string, data: unknown): void;

Push an outgoing event into history. Used by integrations that wrap bridge.send. No-op when isMonitoring() is false.

EventMonitor.recordReceivedEvent(eventName, data)

ts
static recordReceivedEvent(eventName: string, data: unknown): void;

Push an incoming event into history. Used by integrations that wrap bridge.on (or window.addEventListener). No-op when isMonitoring() is false.

EventMonitor.getEventHistory()

ts
static getEventHistory(): Array<MonitoredEvent>;

Returns a copy of the recorded history (newest entries last). Safe to mutate.

EventMonitor.clearHistory()

ts
static clearHistory(): void;

Empties the history buffer. Does not disable monitoring.

EventMonitor.getEventsByName(eventName)

ts
static getEventsByName(eventName: string): Array<MonitoredEvent>;

Returns only events whose eventName matches exactly. Useful for assertions like "did 'skill:cast' fire exactly twice?".

EventMonitor.getStats()

ts
static getStats(): {
  totalEvents: number;
  sentEvents: number;
  receivedEvents: number;
  uniqueEventNames: string[];
};

One-shot summary of recorded history.

EventMonitor.setMaxHistorySize(size)

ts
static setMaxHistorySize(size: number): void;

Caps how many entries the ring buffer keeps. Default is 100. Values < 1 are clamped to 1. Shrinking below the current length truncates the oldest entries.

MonitoredEvent shape

Every entry in history has this exact shape (inlined in the source):

ts
interface MonitoredEvent {
  type: 'sent' | 'received';
  eventName: string;
  data: unknown;
  timestamp: number; // Date.now() at record time, ms since epoch
}

Wiring it up

EventMonitor does not automatically tap the bridge — you decide what to feed it. The simplest pattern is to wrap bridge.send from a small adapter you mount at app startup:

ts
import { EventMonitor } from 'unreal-react-bridge';

EventMonitor.enable();

// In a setup module: tap outbound sends.
function tapBridge(bridge: { send: (n: string, d: unknown) => void }) {
  const original = bridge.send.bind(bridge);
  bridge.send = (name, data) => {
    EventMonitor.recordSentEvent(name, data);
    original(name, data);
  };
}

// Tap inbound CustomEvents (mirror what UnrealDevPanel does).
const originalDispatch = window.dispatchEvent.bind(window);
window.dispatchEvent = (event: Event): boolean => {
  if (event instanceof CustomEvent && event.type.includes(':')) {
    EventMonitor.recordReceivedEvent(event.type, event.detail);
  }
  return originalDispatch(event);
};

If you only need a UI, prefer <UnrealDevPanel>, which already implements both taps.

Example: console inspection

ts
import { EventMonitor } from 'unreal-react-bridge';

EventMonitor.enable();
EventMonitor.setMaxHistorySize(500);

// later, in the browser devtools:
console.table(EventMonitor.getEventHistory());
console.log(EventMonitor.getStats());
console.log(EventMonitor.getEventsByName('skill:cast'));

EventMonitor.clearHistory();
EventMonitor.disable();

Example: Playwright assertion

ts
const sent = await page.evaluate(() => {
  // unreal-react-bridge is loaded by the app
  return (window as any).EventMonitor?.getEventsByName('skill:cast') ?? [];
});
expect(sent).toHaveLength(1);

(Expose EventMonitor on window from your app's dev build if you want to inspect it from Playwright.)

See also

Released under the MIT License.