Перейти к содержимому
Русский

DevTools overview

Это содержимое пока не доступно на вашем языке.

Part of why the BEAM feels the way it does is that you can see the system: observer connects to a live node and shows you the process tree, the mailboxes, the load. DevTools is that idea for actor-ts — an embeddable web UI you attach to a running ActorSystem.

import { ActorSystem } from 'actor-ts';
import { DevTools, DevToolsOptions } from 'actor-ts/devtools';
const system = ActorSystem.create('orders');
const devtoolsOptions = DevToolsOptions.create().withPort(9333);
const devtools = await DevTools.attach(system, devtoolsOptions);
console.log(devtools.url); // http://127.0.0.1:9333

Open that URL and you land on the overview: what this system is, what it is doing, and how that is trending. The nav rail on the left is the way into every other tool.

DevTools is one product assembled from several tools. The nav rail always shows the full roster: a tool this system cannot offer stays greyed out with the reason, so nothing is hidden behind a click that fails.

PanelShows
OverviewSystem identity, uptime, live figures, trends
ActorsLive actor tree, cell states, mailbox depths, busiest actors
ClusterNode topology, shard distribution, membership history
TracingFlame graph and waterfall over recorded message spans
Explain planThe last messages one actor handled, with timings
Time travelBrowse a journal, reconstruct state at any point
ProfilerWhere the actor system spends its time

All of them speak one tap protocol over a single WebSocket, so they share a connection rather than each opening their own.

The overview itself is three sections, in the order you actually ask the questions:

  • Common — actor system, actor-ts version, uptime, runtime, cluster status. Uptime is the system’s, read from the server, so it survives a page reload and a dropped socket. The version is the running framework’s, straight from the handshake — hover it for the tap protocol version.
  • Numbers — actors, messages per second, processed messages, spawn and stop rates, restarts, mailbox backlog, stashed messages, suspended actors, dead letters, mailbox drops, and handler p99.
  • Charts — the same figures over the last few minutes, split across three plots so a level and a rate never share a y-axis, plus the busiest mailboxes right now.

DevTools.attach binds a server of its own — usually what you want, since the DevTools port should be firewalled separately from your application’s.

  1. Attach, ideally behind a flag so production never runs it:

    if (process.env.DEVTOOLS === '1') {
    await DevTools.attach(system); // http://127.0.0.1:9333
    }
  2. Open the URL the returned binding reports. With port: 0 the operating system picks a free port and the binding tells you which.

  3. Detach when you are done. This also happens automatically during CoordinatedShutdown, so a SIGTERM releases the port:

    await devtools.detach();

To put DevTools next to your management endpoints instead of on a port of its own, take the routes and bind them yourself:

import { concat, path } from 'actor-ts';
import { DevTools } from 'actor-ts/devtools';
const routes = concat(
managementRoutes(system, cluster).routes,
path('devtools', DevTools.mount(system)),
);
await system.http(8558, { host: '127.0.0.1' }).bind(routes);

Whatever middleware wraps that subtree applies to DevTools too.

Builder-first, as everywhere else in the framework; a plain object works just as well.

const devtoolsOptions = DevToolsOptions.create()
.withPort(9333)
.withHost('127.0.0.1')
.withPanels({ timeTravel: false });
OptionDefaultMeaning
host'127.0.0.1'Interface to bind. Anything routable needs a gate — see below.
port9333Port to bind; 0 picks a free one.
authMiddleware wrapping the whole tree (e.g. BearerTokenAuth).
ipAllowlistMiddleware wrapping the whole tree.
allowRemotefalseAcknowledge an ungated, routable bind.
backendframework defaultHTTP backend for the DevTools server.
serveUitruefalse leaves the tap without the UI.
allowedOriginssame-originOrigins allowed to open the WebSocket.
panelsall enabledPer-panel switches — see below.
uiDevelopmentRootServe the UI from disk; panel development only.

Each panel can be disabled individually, and a disabled panel is disabled for good — its data never leaves the process, whatever a client asks for:

const devtoolsOptions = DevToolsOptions.create()
.withPanels({ timeTravel: false, profiler: false });

Time travel is the one to think about first: it is the panel that surfaces raw persisted events.

DevTools is a debugger, and the defaults treat it as one.

  • Loopback by default. host is 127.0.0.1, so nothing outside the machine can reach it.

  • A routable bind must be deliberate. Setting host to anything that is not loopback throws unless you also pass auth, ipAllowlist, or allowRemote: true. The error names all three, so a typo in a host string cannot quietly publish your actor state.

  • Gates cover everything. auth and ipAllowlist wrap the UI, the JSON endpoints and the WebSocket upgrade — a gate on only the JSON would leave the actual data channel open.

  • Same-origin sockets. The tap accepts a WebSocket upgrade whose Origin names the tap itself, and rejects any other — so a page you visit cannot dial your local DevTools. This is on by default and needs no configuration: the UI is served by the tap, so its origin always matches. allowedOrigins widens the rule for a UI served from somewhere else; it does not replace it. A request with no Origin at all is allowed, because CSWSH needs a browser and a browser always sends one.

    Binding to loopback is not a substitute for this. A WebSocket handshake is not subject to the same-origin policy, so any page in the developer’s browser can reach 127.0.0.1.

If you do expose it, put it behind the same auth as your management endpoints and treat the credentials as production secrets:

import { BearerTokenAuth } from 'actor-ts';
const devtoolsOptions = DevToolsOptions.create()
.withHost('0.0.0.0')
.withAuth(BearerTokenAuth({ tokens: [process.env.DEVTOOLS_TOKEN!] }));

Every example in the repository is wired for DevTools, and none of them pay for it unless you ask:

Terminal window
bun run examples/hello-world.ts --devtools

--devtools works in every shell. DEVTOOLS=1 bun run … does the same on a POSIX shell, but VAR=value command is a parser error in PowerShell — there, set it separately:

Terminal window
$env:DEVTOOLS = '1'; bun run examples/hello-world.ts

Most examples are scripts that finish in a few hundred milliseconds — too fast to open a browser. With DevTools enabled they park just before shutting down, print the URL, and wait for Ctrl+C, so there is something to look at. Without it, their timing and output are exactly as before.

Multi-system examples give each system its own port, counting up from --devtools-port / DEVTOOLS_PORT (default 9333) — so a three-node cluster demo is 9333, 9334, 9335:

Terminal window
bun run examples/cluster/singleton-hello.ts --devtools

A cluster built from separate terminals works the same way, even though each process has its own copy of that counter: the first free port in the range is taken, so three bun run examples/voice/backend/main.ts --devtools give you 9333, 9334 and 9335 without any flags. If none is free — or anything else about DevTools fails — the example logs a warning and starts anyway. A debugger that cannot bind is not a reason for the program under debug to die.

The bind interface is 127.0.0.1. --devtools-host / DEVTOOLS_HOST changes it — the flag for when the browser is not on the machine running the example: a container, a VM, a WSL or remote dev box.

Terminal window
bun run examples/hello-world.ts --devtools --devtools-host 0.0.0.0

Typing a non-loopback host there is the deliberate act Security asks for, so the example pairs it with allowRemote and DevTools binds — and warns that it is reachable without auth — instead of refusing and leaving you to guess why. That trade is fine for an example and for nothing else: anything with real state in it wants auth or an ipAllowlist in front of the port. A wildcard bind reports itself back as a loopback URL, since http://0.0.0.0:9333 is not an address a browser can open.

Long-running examples (the chat and voice backends, the HTTP services) do not park — they already run until you stop them. The wiring lives in examples/devtools.ts, which is about thirty lines if you want to copy the pattern into your own app.

Every panel keeps showing the last thing it was told — the final reading before a node died is usually the one worth having — so a lost connection would otherwise look exactly like a healthy system. After a couple of seconds without an answer a dialog opens saying No node reachable, counting how long that has been true, and it closes itself the moment something answers. Dismiss it to read the last figures anyway — the panel stays dimmed while the connection is down, so they cannot be mistaken for live ones, and the uptime counter freezes at its last reading rather than counting up past the death of the system it measures.

Each node serves its own DevTools, so another node’s port may still answer while the one you have open does not.

Unused, none. Creating the extension does nothing: no port, no taps, no instrumentation. Everything starts at attach(), and each panel’s data collection only runs while a browser is actually subscribed to it — an open overview does not make the system record spans.

While attached, DevTools switches the metrics registry on if you had not, because message throughput, mailbox drops and handler latency are counted by the framework itself and read 0 against the default noop registry. That makes every framework metric live, cluster counters included, for as long as DevTools is attached; detach() puts the noop back. A registry you enabled yourself is left alone in both directions — DevTools reads it and never disables it.

The UI is vanilla TypeScript bundled by Bun.build and embedded into the published package, so it needs no UI framework at runtime and no network access. To work on a panel:

Terminal window
bun run build:ui -- --dev --watch
Terminal window
bun run dev:devtools

The first rebuilds on save into devtools-ui/.dev; the second boots a demo system serving that directory via uiDevelopmentRoot. The loop is save → refresh, with no TypeScript build and no server restart.