Skip to main content

Real-time Events (Socket.IO)

The Xenon plugin server fans out state changes to connected clients over Socket.IO. The dashboard uses this channel to render device, session, healing, selector-health, and network-interceptor activity without polling. Custom dashboards, CLI watchers, and notification bridges can consume the same stream.

The Socket.IO endpoint runs on the Xenon plugin's HTTP server (default :4723) on the default namespace (/) — there is no namespaced suffix. Both dashboard clients and remote nodes connect to the same endpoint and are routed into separate broadcast rooms based on how they authenticated.


Connecting

The handshake is gated by the same auth surface as the REST API. A connecting client must present one of:

  • Dashboard credentialsapiKey in the Socket.IO auth payload, or an x-xenon-api-key header, or the xenon_dashboard_session cookie set by the dashboard login flow.
  • Node credentialsnodeSecret in the auth payload, or an x-xenon-node-secret header.

When authDisabled=true, the handshake is unconditionally accepted; this is for local development only.

Dashboard client (browser or Node.js)

import { io } from 'socket.io-client';

const socket = io('http://hub.internal:4723', {
auth: { apiKey: process.env.XENON_API_KEY },
transports: ['websocket'],
});

// Tell the server which broadcast room to put us in
socket.emit('register_dashboard');

socket.on('healing_event', (data) => console.log('heal:', data));
socket.on('selector_resolved', (data) => console.log('resolved:', data));

Node client

const socket = io('http://hub.internal:4723', {
auth: { nodeSecret: process.env.XENON_NODE_SECRET },
transports: ['websocket'],
});

socket.emit('register_node', { host: 'node-1.internal:4723' });

A socket authenticated as a dashboard cannot register as a node (and vice versa) — the room each principal can join is fixed at handshake time, so a stolen credential of one class can't impersonate the other.


Protocol handshake

Optionally, after connecting, a client can verify protocol compatibility:

socket.emit('handshake', {
version: '1.0.0', // must match XENON_PROTOCOL_VERSION
nodeId: 'node-1',
host: 'node-1.internal:4723',
timestamp: Date.now(),
});

If version does not match the server's XENON_PROTOCOL_VERSION, the server logs the mismatch and disconnects the socket. The current protocol version is 1.0.0.


Rooms

Each authenticated socket joins exactly one broadcast room:

RoomJoined byReceives
dashboardclients that emit register_dashboardAll session, healing, selector, and interceptor events
nodesclients that emit register_nodeHub-to-node directives (rare in v1; reserved for future use)

Server emission helpers (emitToDashboard, emitToNodes, broadcast) target these rooms; they are why most events listed below land on dashboard clients only.


Event reference

Event names are stable strings declared in src/enums/SocketEvents.ts. Payload shapes are documented on the originating feature page (linked in the See column).

Connection lifecycle

EventEmitted byPurposeSee
handshakeclient → serverOptional protocol-version exchangeThis page
register_nodeclient → serverJoin the nodes roomThis page
register_dashboardclient → serverJoin the dashboard roomThis page
node_connectedserver → dashboardA node finished registeringRemote Execution
node_disconnectedserver → dashboardA node socket disconnectedRemote Execution

Sessions

EventEmitted byPurpose
session_startedserver → dashboardA new Appium session was allocated to a device
session_stoppedserver → dashboardA session ended (stopped, crashed, or timed out)
session_commandserver → dashboardPer-command stream when the session has command logging enabled

Healing & selector lifecycle

All event payloads carry the (strategy, selector) tuple. See Selector Health for the state machine that produces them.

EventTrigger
healing_eventA findElement was successfully healed (any tier)
selector_fixedUser clicked Mark as Fixed — selector entered Pending
selector_progressVerifier counted one more clean CI build but the threshold isn't met
selector_resolvedVerifier promoted a selector to Resolved after 3 clean builds
selector_regressedA Pending or Resolved selector healed again
selector_cancelledUser backed out of Pending without recording a fix
selector_mutedUser muted a selector
selector_unmutedUser unmuted a selector

Network interceptor

See Network Interceptor for capture semantics.

EventTrigger
interceptor_session_startedA session with xe:interceptor.enabled = true started — the proxy is live
interceptor_requestA request (or response, or failure) was captured
interceptor_session_stoppedThe session ended; archived traffic is now served from disk

Versioning

The wire protocol is versioned via the XENON_PROTOCOL_VERSION constant. Backwards-incompatible changes — adding a required field, renaming an event, changing room semantics — bump the version. Adding new events under existing categories does not.

If you write a long-lived Socket.IO consumer, send the handshake event after connect and treat a disconnect-after-handshake as a signal to bump your version pin.