Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/javascript/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hawk.so/javascript",
"version": "3.2.17",
"version": "3.2.18",
"description": "JavaScript errors tracking for Hawk.so",
"files": [
"dist"
Expand Down
39 changes: 38 additions & 1 deletion packages/javascript/src/modules/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export default class Socket implements Transport {
*/
private reconnectionAttempts: number;

/**
* Page hide event handler reference (for removal)
*/
private pageHideHandler: () => void;

/**
* Creates new Socket instance. Setup initial socket params.
*
Expand All @@ -77,6 +82,10 @@ export default class Socket implements Transport {
this.reconnectionTimeout = reconnectionTimeout;
this.reconnectionAttempts = reconnectionAttempts;

this.pageHideHandler = () => {
this.close();
};

this.eventsQueue = [];
this.ws = null;

Expand Down Expand Up @@ -120,7 +129,21 @@ export default class Socket implements Transport {
}

/**
* Create new WebSocket connection and setup event listeners
* Setup window event listeners
*/
private setupListeners(): void {
window.addEventListener('pagehide', this.pageHideHandler, { capture: true });
}

/**
* Remove window event listeners
*/
private destroyListeners(): void {
window.removeEventListener('pagehide', this.pageHideHandler, { capture: true });
}

/**
* Create new WebSocket connection and setup socket event listeners
*/
private init(): Promise<void> {
return new Promise((resolve, reject) => {
Expand All @@ -139,6 +162,8 @@ export default class Socket implements Transport {
* @param event - websocket event on closing
*/
this.ws.onclose = (event: CloseEvent): void => {
this.destroyListeners();

if (typeof this.onClose === 'function') {
this.onClose(event);
}
Expand All @@ -154,6 +179,8 @@ export default class Socket implements Transport {
};

this.ws.onopen = (event: Event): void => {
this.setupListeners();

if (typeof this.onOpen === 'function') {
this.onOpen(event);
}
Expand All @@ -163,6 +190,16 @@ export default class Socket implements Transport {
});
}

/**
* Closes socket connection
*/
private close(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}

/**
* Tries to reconnect to the server for specified number of times with the interval
*
Expand Down
74 changes: 74 additions & 0 deletions packages/javascript/tests/socket.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, it, expect, afterEach, vi } from 'vitest';
import Socket from '../src/modules/socket';
import type { CatcherMessage } from '@hawk.so/types';

const MOCK_WEBSOCKET_URL = 'ws://localhost:1234';

type MockWebSocket = {
url: string;
readyState: number;
send: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
onopen?: (event: Event) => void;
onclose?: (event: CloseEvent) => void;
onerror?: (event: Event) => void;
onmessage?: (event: MessageEvent) => void;
};

describe('Socket', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('should close websocket on pagehide and recreate connection on next send()', async () => {
const closeSpy = vi.fn(function (this: MockWebSocket) {
this.readyState = WebSocket.CLOSED;
this.onclose?.({ code: 1000 } as CloseEvent);
});

let webSocket!: MockWebSocket;
const WebSocketConstructor = 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;
webSocket = this;
});
globalThis.WebSocket = WebSocketConstructor;

const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');

// initialize socket and open fake websocket connection
const socket = new Socket({ collectorEndpoint: MOCK_WEBSOCKET_URL });
webSocket.readyState = WebSocket.OPEN;
webSocket.onopen?.(new Event('open'));

// capture pagehide handler to verify it's properly removed
const pagehideCall = addEventListenerSpy.mock.calls.find(([event]) => event === 'pagehide');
expect(pagehideCall).toBeDefined();
const pagehideHandler = pagehideCall![1] as EventListener;

// trigger pagehide event
window.dispatchEvent(new Event('pagehide'));

// websocket connection should be closed
expect(closeSpy).toHaveBeenCalledOnce();
expect(removeEventListenerSpy).toHaveBeenCalledWith('pagehide', pagehideHandler, { capture: true });

// send socket method should make websocket reconnect
const sendPromise = socket.send({ foo: 'bar' } as CatcherMessage);
webSocket.readyState = WebSocket.OPEN;
webSocket.onopen?.(new Event('open'));
await sendPromise;

expect(WebSocketConstructor).toHaveBeenCalledTimes(2);
});
});