Skip to content

Monitoring WebSockets

3 min read • Posted on December 23, 2024

Introduction

While tools like Sentry or Datadog are amazing to track performance issues of HTML, CSS, JavaScript, and XHR requests, they often don’t provide the best tools when it comes to WebSockets. WebSocket connections themselves may be logged, but the messages/errors sent over them are not captured by these tools.

Why? Because they all are built on top of the native PerformanceObserver, which doesn’t include any events for WebSockets.
You can check https://dyte.io/blog/web-api-performance-monitoring/ for how to set it up manually.

Solution

This solution may not be the cleanest, but to the best of my knowledge, there is no other way to achieve this functionality. The approach involves creating a custom patch of the global WebSocket object, allowing us to hook into WebSocket events and capture the messages sent over them.

Here’s an example:

const GlobalWebSocket = globalThis.WebSocket;
class WebSocketWithHooks extends GlobalWebSocket {
constructor(...args) {
super(...args);
this.addEventListener("message", onMessage);
this.addEventListener("close", onClose);
this.addEventListener("error", onError);
this.addEventListener("open", onOpen);
this.addEventListener(
"close",
() => {
this.removeEventListener("message", onMessage);
this.removeEventListener("close", onClose);
this.removeEventListener("error", onError);
this.removeEventListener("open", onOpen);
},
{ once: true },
);
}
}
globalThis.WebSocket = WebSocketWithHooks;

How does it work?

By overriding the global WebSocket object, you gain control over all WebSocket instances createdAt on the page — including those instantiated by third-party libraries. The key is that this script must run as early as possible in the page’s lifecycle, ideally as the first or one of the first lines of JavaScript executed.

After this, you can create the listeners you want, and even customize them to suit your specific needs.

Examples

Here are a few examples showing how you can monitor WebSocket activity. You can further tailor these examples to suit your specific needs.

Handling errors

const GlobalWebSocket = globalThis.WebSocket;
class WebSocketWithHooks extends GlobalWebSocket {
constructor(...args) {
super(...args);
// Log errors when they occur
const onError = (errorEvent) => {
console.error("WebSocket error occurred", {
creationArgs: args, // Log the arguments used to create the WebSocket
error: errorEvent,
});
};
this.addEventListener("error", onError);
// Clean up listeners when the WebSocket is closed
this.addEventListener(
"close",
() => {
this.removeEventListener("error", onError);
},
{ once: true },
);
}
}
globalThis.WebSocket = WebSocketWithHooks;

Message type & size

const GlobalWebSocket = globalThis.WebSocket;
class WebSocketWithHooks extends GlobalWebSocket {
constructor(...args) {
super(...args);
// Log details about incoming messages
const onMessage = (messageEvent) => {
console.log("Received a message in WebSocket", {
creationArgs: args, // Log WebSocket creation arguments
messageType:
typeof messageEvent.data === "string"
? "string"
: messageEvent.data instanceof ArrayBuffer
? "ArrayBuffer"
: messageEvent.data instanceof Blob
? "Blob"
: "unknown",
size:
typeof messageEvent.data === "string"
? messageEvent.data.length
: messageEvent.data instanceof ArrayBuffer
? messageEvent.data.byteLength
: messageEvent.data instanceof Blob
? messageEvent.data.size
: 0,
});
};
this.addEventListener("message", onMessage);
// Clean up listeners when the WebSocket is closed
this.addEventListener(
"close",
() => {
this.removeEventListener("message", onMessage);
},
{ once: true },
);
}
}
globalThis.WebSocket = WebSocketWithHooks;