From 1a415bbfc1e208e988d34912ecc4021a39f21a5b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:14:00 +0300 Subject: [PATCH 1/2] Protect beforeSend/beforeBreadcrumb hooks with structuredClone --- packages/javascript/package.json | 2 +- packages/javascript/src/addons/breadcrumbs.ts | 16 +++++++++++++--- packages/javascript/src/catcher.ts | 13 ++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/javascript/package.json b/packages/javascript/package.json index dfb7b94..22e9827 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -1,6 +1,6 @@ { "name": "@hawk.so/javascript", - "version": "3.2.16", + "version": "3.2.17", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" diff --git a/packages/javascript/src/addons/breadcrumbs.ts b/packages/javascript/src/addons/breadcrumbs.ts index 10a7a96..faa14a8 100644 --- a/packages/javascript/src/addons/breadcrumbs.ts +++ b/packages/javascript/src/addons/breadcrumbs.ts @@ -52,8 +52,7 @@ export interface BreadcrumbsOptions { * Hook called before each breadcrumb is stored. * - Return modified breadcrumb — it will be stored instead of the original. * - Return `false` — the breadcrumb will be discarded. - * - Return nothing (`void` / `undefined` / `null`) — the original breadcrumb is stored as-is (a warning is logged). - * - If the hook returns an invalid value, a warning is logged and the original breadcrumb is stored. + * - Any other value is invalid — the original breadcrumb is stored as-is (a warning is logged). */ beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void; @@ -236,7 +235,18 @@ export class BreadcrumbManager { * Apply beforeBreadcrumb hook */ if (this.options.beforeBreadcrumb) { - const breadcrumbClone = structuredClone(bc); + let breadcrumbClone: Breadcrumb; + + try { + breadcrumbClone = structuredClone(bc); + } catch { + /** + * structuredClone may fail on non-cloneable values in breadcrumb.data + * Fall back to passing the original — hook may mutate it, but breadcrumb storage won't crash + */ + breadcrumbClone = bc; + } + const result = this.options.beforeBreadcrumb(breadcrumbClone, hint); /** diff --git a/packages/javascript/src/catcher.ts b/packages/javascript/src/catcher.ts index 0646507..b18d868 100644 --- a/packages/javascript/src/catcher.ts +++ b/packages/javascript/src/catcher.ts @@ -438,7 +438,18 @@ export default class Catcher { * Filter sensitive data */ if (typeof this.beforeSend === 'function') { - const eventPayloadClone = structuredClone(payload); + let eventPayloadClone: HawkJavaScriptEvent; + + try { + eventPayloadClone = structuredClone(payload); + } catch { + /** + * structuredClone may fail on non-cloneable values (functions, DOM nodes, etc.) + * Fall back to passing the original — hook may mutate it, but at least reporting won't crash + */ + eventPayloadClone = payload; + } + const result = this.beforeSend(eventPayloadClone); /** From 46449a70498da5e04d57ed796de15bad5185cd8d Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:30:34 +0300 Subject: [PATCH 2/2] new test --- packages/javascript/tests/before-send.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/javascript/tests/before-send.test.ts b/packages/javascript/tests/before-send.test.ts index e9652a1..304cebd 100644 --- a/packages/javascript/tests/before-send.test.ts +++ b/packages/javascript/tests/before-send.test.ts @@ -142,6 +142,25 @@ describe('beforeSend', () => { ); }); + it('should still send event when structuredClone throws (non-cloneable payload)', async () => { + // Arrange + const { sendSpy, transport } = createTransport(); + const hawk = createCatcher(transport, (event) => event); + const cloneSpy = vi.spyOn(globalThis, 'structuredClone').mockImplementation(() => { + throw new DOMException('could not be cloned', 'DataCloneError'); + }); + + // Act + hawk.send(new Error('non-cloneable')); + await wait(); + + // Assert — event is still sent, reporting didn't crash + expect(sendSpy).toHaveBeenCalledOnce(); + expect(getSentPayload(sendSpy)!.title).toBe('non-cloneable'); + + cloneSpy.mockRestore(); + }); + it('should send event without deleted optional fields', async () => { // Arrange const { sendSpy, transport } = createTransport();