Skip to content
Open
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
88 changes: 88 additions & 0 deletions packages/javascript/tests/socket.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, it, expect, beforeEach, 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;
};

function createMockWebSocket() {
const instances: MockWebSocket[] = [];

const closeSpy = vi.fn(function (this: MockWebSocket) {
this.readyState = WebSocket.CLOSED;
this.onclose?.({ code: 1000 } as CloseEvent);
});

const ctor = vi.fn<(url: string) => MockWebSocket>().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;

instances.push(this);
});

return { ctor, closeSpy, instances };
}

describe('Socket', () => {
let ctor: ReturnType<typeof vi.fn>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is not clear

let closeSpy: ReturnType<typeof vi.fn>;
let instances: MockWebSocket[];

let addSpy: ReturnType<typeof vi.spyOn>;
let removeSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
({ ctor, closeSpy, instances } = createMockWebSocket());
(globalThis as any).WebSocket = ctor;

addSpy = vi.spyOn(window, 'addEventListener');
removeSpy = vi.spyOn(window, 'removeEventListener');
});

afterEach(() => {
vi.restoreAllMocks();
});

it('should close websocket on pagehide and recreate connection on next send()', async () => {
const socket = new Socket({ collectorEndpoint: MOCK_WEBSOCKET_URL });

const first = instances[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first

name is not clear

first.readyState = WebSocket.OPEN;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you create socket but check instances?

I'd suggest to create Socket mock right here, it will be more straightforward

first.onopen?.(new Event('open'));
const pagehideHandler = addSpy.mock.calls.find(c => c[0] === 'pagehide')![1] as EventListener;

window.dispatchEvent(new Event('pagehide'));

expect(closeSpy).toHaveBeenCalledOnce();
expect(removeSpy).toHaveBeenCalledWith('pagehide', pagehideHandler, { capture: true });

const sendPromise = socket.send({ foo: 'bar' } as CatcherMessage);

const second = instances[1];
second.readyState = WebSocket.OPEN;
second.onopen?.(new Event('open'));
await sendPromise;

expect(ctor).toHaveBeenCalledTimes(2);
expect(second.url).toBe(MOCK_WEBSOCKET_URL);
});
});