diff --git a/packages/javascript/package.json b/packages/javascript/package.json index 169a3f5..ddaa457 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -1,6 +1,6 @@ { "name": "@hawk.so/javascript", - "version": "3.2.18", + "version": "3.2.19", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" diff --git a/packages/javascript/src/modules/socket.ts b/packages/javascript/src/modules/socket.ts index 51224cf..10f3fd6 100644 --- a/packages/javascript/src/modules/socket.ts +++ b/packages/javascript/src/modules/socket.ts @@ -111,7 +111,10 @@ export default class Socket if (this.ws === null) { this.eventsQueue.push(message); - return this.init(); + await this.init(); + this.sendQueue(); + + return; } switch (this.ws.readyState) { @@ -218,6 +221,7 @@ export default class Socket await this.init(); log('Successfully reconnected.', 'info'); + this.sendQueue(); } catch (error) { this.reconnectionAttempts--; diff --git a/packages/javascript/tests/socket.test.ts b/packages/javascript/tests/socket.test.ts index 27a37db..ef81a3e 100644 --- a/packages/javascript/tests/socket.test.ts +++ b/packages/javascript/tests/socket.test.ts @@ -4,6 +4,17 @@ import type { CatcherMessage } from '@hawk.so/types'; const MOCK_WEBSOCKET_URL = 'ws://localhost:1234'; +/** + * vi.fn() replacement has no WebSocket.OPEN/CLOSED; Socket uses them in switch — without this, + * `undefined === undefined` always hits the first `case WebSocket.OPEN` and reconnect never runs. + */ +function patchWebSocketMockConstructor(ctor: { CONNECTING?: number; OPEN?: number; CLOSING?: number; CLOSED?: number }): void { + ctor.CONNECTING = 0; + ctor.OPEN = 1; + ctor.CLOSING = 2; + ctor.CLOSED = 3; +} + type MockWebSocket = { url: string; readyState: number; @@ -72,3 +83,94 @@ describe('Socket', () => { expect(WebSocketConstructor).toHaveBeenCalledTimes(2); }); }); + +/** + * Regression: queued events must be flushed after reconnect / init, not only on first constructor connect. + */ +describe('Socket — events queue after connection loss', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + function mockWebSocketFactory(sockets: MockWebSocket[], closeSpy: ReturnType) { + const ctor = vi.fn<(url: string) => void>().mockImplementation(function ( + this: MockWebSocket, + url: string + ) { + this.url = url; + this.readyState = WebSocket.CONNECTING; + this.send = vi.fn(); + this.close = closeSpy; + this.onopen = undefined; + this.onclose = undefined; + this.onerror = undefined; + this.onmessage = undefined; + sockets.push(this); + }); + patchWebSocketMockConstructor(ctor); + + return ctor; + } + + it('should flush queued event after reconnect when socket is CLOSED', async () => { + const sockets: MockWebSocket[] = []; + const closeSpy = vi.fn(function (this: MockWebSocket) { + this.readyState = WebSocket.CLOSED; + this.onclose?.({ code: 1001 } as CloseEvent); + }); + + const WebSocketConstructor = mockWebSocketFactory(sockets, closeSpy); + globalThis.WebSocket = WebSocketConstructor as unknown as typeof WebSocket; + + const socket = new Socket({ + collectorEndpoint: MOCK_WEBSOCKET_URL, + reconnectionTimeout: 10, + }); + + const ws1 = sockets[0]; + ws1.readyState = WebSocket.OPEN; + ws1.onopen?.(new Event('open')); + await Promise.resolve(); + + ws1.readyState = WebSocket.CLOSED; + + const payload = { type: 'errors/javascript', title: 'queued-after-drop' } as unknown as CatcherMessage<'errors/javascript'>; + const sendPromise = socket.send(payload); + + const ws2 = sockets[1]; + expect(ws2).toBeDefined(); + ws2.readyState = WebSocket.OPEN; + ws2.onopen?.(new Event('open')); + await sendPromise; + + expect(ws2.send).toHaveBeenCalledTimes(1); + expect(ws2.send).toHaveBeenCalledWith(JSON.stringify(payload)); + }); + + it('should flush queued event when ws is null after pagehide and send()', async () => { + const closeSpy = vi.fn(function (this: MockWebSocket) { + this.readyState = WebSocket.CLOSED; + this.onclose?.({ code: 1000 } as CloseEvent); + }); + + const sockets: MockWebSocket[] = []; + const WebSocketConstructor = mockWebSocketFactory(sockets, closeSpy); + globalThis.WebSocket = WebSocketConstructor as unknown as typeof WebSocket; + + const socket = new Socket({ collectorEndpoint: MOCK_WEBSOCKET_URL }); + sockets[0].readyState = WebSocket.OPEN; + sockets[0].onopen?.(new Event('open')); + await Promise.resolve(); + + window.dispatchEvent(new Event('pagehide')); + + const queued = { foo: 'bar' } as unknown as CatcherMessage<'errors/javascript'>; + const sendPromise = socket.send(queued); + sockets[1].readyState = WebSocket.OPEN; + sockets[1].onopen?.(new Event('open')); + await sendPromise; + + expect(sockets[1].send).toHaveBeenCalledTimes(1); + expect(sockets[1].send).toHaveBeenCalledWith(JSON.stringify(queued)); + }); +});