Core Features

WebSocket Capture

Reliable instruments window.WebSocket to record one summary row per connection — lifecycle, message and byte counts, reconnect storms — and forwards abnormal closes and post-open errors into the errors pipeline.

How it works#

On init(), Reliable wraps window.WebSocket. Every connection created from that point is observed: the open timestamp, every message event (counted, not stored), every error, the close code, and outbound send() calls.

Counters live in memory for the lifetime of the socket. When the connection closes — cleanly or otherwise — a single POST /websocket event is emitted with the full summary. One row per connection, never one per message.

Why one row per connection?

A chat app or trading desk can push hundreds of messages per second. Per-message capture would balloon ingest costs without meaningfully improving the signal. Aggregate counters + an error feed are enough to detect drops, slow handshakes, and reconnect storms.

Pre-upgrade handshakes#

A WebSocket starts life as an HTTP GET with an Upgrade: websocket header. If that fails with a 4xx or 5xx (auth, CORS, server unavailable) the upgrade never happens — and that request is already captured by the network module as a regular failed HTTP call.

The WebSocket module only takes over after the 101 Switching Protocols response. So a failed connection attempt shows up in Network, while a connection that opened and then dropped shows up in WebSockets.

What gets sent#

NameTypeDefaultDescription
urlstringFull WebSocket URL (ws:// or wss://). Sensitive query params like tokens or keys are scrubbed.
url_templatestringNormalized path — numeric, UUID, and long-hex segments collapse to :id / :uuid / :hex so /chat/users/42 and /chat/users/99 group together.
protocolsstring[]Sub-protocols negotiated at open (the second argument to new WebSocket(url, protocols)).
opened_atstringISO timestamp when the connection was created.
closed_atstringISO timestamp when the connection closed.
duration_msnumberLifetime of the connection in milliseconds.
close_codenumberWebSocket close code (1000 normal, 1001 going away, 1006 abnormal, 1011–1014 server error, 4000–4999 app-defined).
close_reasonstringOptional reason string the peer included with the close frame.
had_errorbooleanServer-computed verdict: true if any error event fired during the lifetime OR the close code is anything other than 1000/1001.
error_countnumberHow many error events fired during the connection's lifetime.
messages_sentnumberTotal messages your code sent over the connection.
messages_receivednumberTotal messages received from the peer.
bytes_sentnumberSum of outbound payload sizes (strings are measured as UTF-8 bytes; Blob/ArrayBuffer use their byte length).
bytes_receivednumberSum of inbound payload sizes.
reconnect_countnumberNumber of prior opens to the same url_template within a 30-second sliding window. A value > 3 signals broken backoff.
pathstringThe page route the user was on when the connection opened.

Errors flow into the errors pipeline#

A WebSocket summary on its own doesn't page anyone — but the failures inside it do. Three classes of events are forwarded to the standard errors pipeline so they participate in incident grouping, alerts, and paging:

  • Post-open error events — anything the WebSocket spec surfaces via the error handler.
  • Abnormal closes — close codes outside 1000 (normal) and 1001 (navigation). This includes 1006 (the socket died without a close frame, usually a network drop) and 1011–1014 (server-side errors).
  • send() on a closed socket — a programming bug worth surfacing, even if the underlying call already throws.

Each forwarded error is tagged with source: "websocket", the URL, and the connection UUID — so you can filter the Errors view to just WebSocket failures, or pivot from a WebSocket summary row into the matching error in one click.

Reconnect storm detection#

When a new connection opens to a URL template that's already opened recently, the SDK stamps the running count onto the new row. Backoff is healthy when this stays at 0–2; values above 3 within the 30-second window mean the reconnect loop is hot and the dashboard will start flagging them.

typescript
// Healthy lifecycle — single open, clean close
{ reconnect_count: 0, close_code: 1000, duration_ms: 480_000 }

// Suspect — repeated opens to the same endpoint inside 30s
{ reconnect_count: 5, close_code: 1006, duration_ms: 1_200 }

Toggling it#

WebSocket capture is enabled by default. Disable it via the captureWebSockets flag if you don't use WebSockets or want to gate it explicitly:

typescript
init({
  publicKey: 'pk_live_rl_...',
  captureWebSockets: false,   // default: true
});

What's NOT captured#

  • Per-message payloads — only counts and total byte sizes. We never store frame contents.
  • Sockets opened by browser extensions or other libraries that bypass window.WebSocket — the wrapper applies at init(); anything created before that, or that holds a reference to the original constructor, escapes capture.
  • Server-Sent Events (SSE) and EventSource — those go through fetch and are covered by the network module instead.

Tip

For socket traffic you'd rather not report (heartbeats, presence pings), filter on the URL template in beforeSend:
typescript
beforeSend(event) {
  if (event.path === '/websocket' && event.url_template?.endsWith('/heartbeat')) {
    return null;
  }
  return event;
}