[MM-64244] Add websocket disconnect reason metric (#31032)

We've recently spent some effort improving websocket reconnection logic. With this commit, I've augmented the websocket reconnect metric to include a disconnect reason. This will help us measure the impact of these changes in production.
Этот коммит содержится в:
David Krauser
2025-05-30 08:15:20 -04:00
коммит произвёл GitHub
родитель 611b2a8e79
Коммит 761584c040
9 изменённых файлов: 215 добавлений и 41 удалений

Просмотреть файл

@@ -49,6 +49,22 @@ if (typeof Event === 'undefined') {
};
}
// Mock CloseEvent class if it's not defined
if (typeof CloseEvent === 'undefined') {
(global as any).CloseEvent = class MockCloseEvent extends (global as any).Event {
code: number;
reason: string;
wasClean: boolean;
constructor(type: string, options?: {code?: number; reason?: string; wasClean?: boolean}) {
super(type);
this.code = options?.code || 0;
this.reason = options?.reason || '';
this.wasClean = options?.wasClean || false;
}
};
}
class MockWebSocket {
readonly binaryType: BinaryType = 'blob';
readonly bufferedAmount: number = 0;

Просмотреть файл

@@ -19,6 +19,10 @@ export type WebSocketClientConfig = {
clientPingInterval: number;
}
// Custom close error codes must be in the range of 4000-4999
const clientPingTimeoutErrCode = 4000;
const clientSequenceMismatchErrCode = 4001;
const defaultWebSocketClientConfig: WebSocketClientConfig = {
maxWebSocketFails: 7,
minWebSocketRetryTime: 3000, // 3 seconds
@@ -45,6 +49,7 @@ export default class WebSocketClient {
private serverSequence: number;
private connectFailCount: number;
private responseCallbacks: {[x: number]: ((msg: any) => void)};
private lastErrCode: string | null;
/**
* @deprecated Use messageListeners instead
@@ -110,6 +115,7 @@ export default class WebSocketClient {
this.config = {...defaultWebSocketClientConfig, ...config};
this.pingInterval = null;
this.waitingForPong = false;
this.lastErrCode = null;
}
// on connect, only send auth cookie and blank state.
@@ -198,19 +204,32 @@ export default class WebSocketClient {
// Add connection id, and last_sequence_number to the query param.
// We cannot use a cookie because it will bleed across tabs.
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
const websocketUrl = `${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}${this.postedAck ? '&posted_ack=true' : ''}`;
let websocketUrl = `${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}`;
if (this.postedAck) {
websocketUrl += '&posted_ack=true';
}
if (this.lastErrCode) {
websocketUrl += `&disconnect_err_code=${encodeURIComponent(this.lastErrCode)}`;
}
if (this.config.newWebSocketFn) {
this.conn = this.config.newWebSocketFn(websocketUrl);
} else {
this.conn = new WebSocket(websocketUrl);
}
const onclose = () => {
const onclose = (event: CloseEvent) => {
this.conn = null;
this.responseSequence = 1;
if (!this.lastErrCode && event && event.code) {
this.lastErrCode = `${event.code}`;
}
if (this.connectFailCount === 0) {
console.log('websocket closed'); //eslint-disable-line no-console
console.log(`websocket closed: ${this.lastErrCode}`); //eslint-disable-line no-console
}
this.connectFailCount++;
@@ -254,6 +273,8 @@ export default class WebSocketClient {
this.sendMessage('authentication_challenge', {token});
}
this.lastErrCode = null;
if (this.connectFailCount > 0) {
console.log('websocket re-established connection'); //eslint-disable-line no-console
@@ -294,6 +315,11 @@ export default class WebSocketClient {
console.log('ping received no response within time limit: re-establishing websocket'); //eslint-disable-line no-console
const closeEvent = new CloseEvent('close', {
code: clientPingTimeoutErrCode,
wasClean: false,
});
// Calling conn.close() will trigger the onclose callback,
// but sometimes with a significant delay. So instead, we
// call the onclose callback ourselves immediately. We also
@@ -303,7 +329,7 @@ export default class WebSocketClient {
this.responseSequence = 1;
this.conn.onclose = () => {};
this.conn.close();
onclose();
onclose(closeEvent);
},
this.config.clientPingInterval);
@@ -369,10 +395,24 @@ export default class WebSocketClient {
// we just disconnect and reconnect.
if (msg.seq !== this.serverSequence) {
console.log('missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence); //eslint-disable-line no-console
// We are not calling this.close() because we need to auto-restart.
const closeEvent = new CloseEvent('close', {
code: clientSequenceMismatchErrCode,
wasClean: false,
});
// Calling conn.close() will trigger the onclose callback,
// but sometimes with a significant delay. So instead, we
// call the onclose callback ourselves immediately. We also
// unset the callback on the old connection to ensure it
// is only called once.
this.connectFailCount = 0;
this.responseSequence = 1;
this.conn?.close(); // Will auto-reconnect after MIN_WEBSOCKET_RETRY_TIME.
if (this.conn) {
this.conn.onclose = () => {};
this.conn.close();
onclose(closeEvent);
}
return;
}
this.serverSequence = msg.seq + 1;
@@ -507,13 +547,14 @@ export default class WebSocketClient {
this.connectFailCount = 0;
this.responseSequence = 1;
this.clearReconnectTimeout();
this.lastErrCode = null;
this.stopPingInterval();
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
this.conn.onclose = () => {};
this.conn.close();
this.conn = null;
console.log('websocket closed'); //eslint-disable-line no-console
console.log('websocket closed manually'); //eslint-disable-line no-console
}
if (this.onlineHandler) {