From f8898c86c44be2b0c1f75bcf7fe6ce492ed40f03 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:07:27 +0300 Subject: [PATCH 1/7] Fix beforeSend and beforeBreadcrumb hooks to preserve original payload on invalid return --- package.json | 2 +- src/index.ts | 30 ++++-------- src/modules/breadcrumbs.ts | 20 ++++---- tests/before-send.test.ts | 94 +++++++++++--------------------------- tests/breadcrumbs.test.ts | 66 +++++++++++++------------- 5 files changed, 80 insertions(+), 132 deletions(-) diff --git a/package.json b/package.json index 28daa85..7ac8307 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hawk.so/nodejs", - "version": "3.3.1", + "version": "3.3.2", "description": "Node.js catcher for Hawk", "license": "AGPL-3.0-only", "engines": { diff --git a/src/index.ts b/src/index.ts index 5812d88..863eb56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -294,39 +294,27 @@ class Catcher { * Filter sensitive data */ if (typeof this.beforeSend === 'function') { + const original = structuredClone(payload); const result = this.beforeSend(payload); /** - * Allow user to intentionally drop event by returning false + * false → drop event */ if (result === false) { return; } /** - * If user returned nothing (void/undefined/null) — warn and keep original payload + * Valid event payload → use it */ - if (result === undefined || result === null) { - console.warn(`[Hawk] Invalid beforeSend value: (${String(result)}). It should return event or false. Event is sent without changes.`); - } else if (isValidEventPayload(result)) { + if (isValidEventPayload(result)) { payload = result; } else { - let received: string; - - try { - received = JSON.stringify(result); - } catch { - try { - received = String(result); - } catch { - received = Object.prototype.toString.call(result); - } - } - - console.warn( - '[Hawk] beforeSend produced invalid payload (missing required fields), sending original. ' - + `Received: ${received}` - ); + /** + * Anything else is invalid — warn and send original + */ + console.warn('[Hawk] Invalid beforeSend value. It should return event or false. Event is sent without changes.'); + payload = original; } } diff --git a/src/modules/breadcrumbs.ts b/src/modules/breadcrumbs.ts index 9425a36..04937fa 100644 --- a/src/modules/breadcrumbs.ts +++ b/src/modules/breadcrumbs.ts @@ -66,6 +66,10 @@ function isValidBreadcrumb(v: unknown): v is Breadcrumb { const candidate = v as Record; + if (typeof candidate.message !== 'string' || (candidate.message as string).trim() === '') { + return false; + } + if (candidate.timestamp !== undefined && typeof candidate.timestamp !== 'number') { return false; } @@ -133,6 +137,7 @@ export class BreadcrumbManager { }; if (this.options.beforeBreadcrumb) { + const original = structuredClone(bc); const result = this.options.beforeBreadcrumb(bc, hint); /** @@ -143,17 +148,16 @@ export class BreadcrumbManager { } /** - * void/undefined/null — warn and keep original breadcrumb + * Valid breadcrumb → use it */ - if (result === undefined || result === null) { - console.warn('[Hawk] beforeBreadcrumb returned nothing, storing original breadcrumb.'); - } else if (isValidBreadcrumb(result)) { + if (isValidBreadcrumb(result)) { Object.assign(bc, result); } else { - console.warn( - '[Hawk] beforeBreadcrumb produced invalid breadcrumb (must be an object with numeric timestamp), storing original. ' - + `Received: ${Object.prototype.toString.call(result)}` - ); + /** + * Anything else is invalid — warn and restore original + */ + console.warn('[Hawk] Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.'); + Object.assign(bc, original); } } diff --git a/tests/before-send.test.ts b/tests/before-send.test.ts index 4b7f67f..cacfdb9 100644 --- a/tests/before-send.test.ts +++ b/tests/before-send.test.ts @@ -94,110 +94,70 @@ describe('beforeSend processing', () => { expect(axios.post).not.toHaveBeenCalled(); }); - it('sends original payload and warns when beforeSend returns undefined (no return)', () => { - initWithBeforeSend(() => { - /* no return */ - }); - - HawkCatcher.send(new Error('test-undefined')); - - expect(axios.post).toHaveBeenCalledOnce(); - expect(warnSpy).toHaveBeenCalledOnce(); - expect(warnSpy).toHaveBeenCalledWith( - '[Hawk] Invalid beforeSend value: (undefined). It should return event or false. Event is sent without changes.' - ); - - const payload = getSentPayload(); - - expect(payload.title).toBe('Error: test-undefined'); - expect(payload.backtrace).toBeInstanceOf(Array); - }); - - it('sends original payload and warns when beforeSend returns null', () => { + it.each([ + { label: 'undefined', value: undefined }, + { label: 'null', value: null }, + { label: 'number (42)', value: 42 }, + { label: 'string ("oops")', value: 'oops' }, + { label: 'true', value: true }, + { label: 'empty object', value: {} }, + ])('sends original payload and warns when beforeSend returns $label', ({ value }) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - initWithBeforeSend((() => null) as any); + initWithBeforeSend(() => value as any); - HawkCatcher.send(new Error('test-null')); + HawkCatcher.send(new Error('test-invalid')); expect(axios.post).toHaveBeenCalledOnce(); - expect(warnSpy).toHaveBeenCalledOnce(); expect(warnSpy).toHaveBeenCalledWith( - '[Hawk] Invalid beforeSend value: (null). It should return event or false. Event is sent without changes.' + '[Hawk] Invalid beforeSend value. It should return event or false. Event is sent without changes.' ); const payload = getSentPayload(); - expect(payload.title).toBe('Error: test-null'); - expect(payload.backtrace).toBeInstanceOf(Array); - }); - - it('sends original payload and warns when beforeSend returns invalid value', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - initWithBeforeSend((() => true) as any); - - HawkCatcher.send(new Error('test-invalid')); - - expect(axios.post).toHaveBeenCalledOnce(); - expect(warnSpy).toHaveBeenCalledOnce(); - - const payload = getSentPayload(); - expect(payload.title).toBe('Error: test-invalid'); expect(payload.backtrace).toBeInstanceOf(Array); }); - it('sends original payload and warns when beforeSend returns empty object', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - initWithBeforeSend((() => ({})) as any); - - HawkCatcher.send(new Error('test-empty-obj')); - - expect(axios.post).toHaveBeenCalledOnce(); - expect(warnSpy).toHaveBeenCalledOnce(); - - const payload = getSentPayload(); - - expect(payload.title).toBe('Error: test-empty-obj'); - expect(payload.backtrace).toBeInstanceOf(Array); - }); - - it('warns when beforeSend mutates payload to invalid state', () => { + it('sends original payload and warns when beforeSend mutates title to empty string', () => { initWithBeforeSend((event) => { event.title = ''; + + return event; }); HawkCatcher.send(new Error('test-mutated')); - expect(warnSpy).toHaveBeenCalledOnce(); expect(axios.post).toHaveBeenCalledOnce(); + expect(warnSpy).toHaveBeenCalledWith( + '[Hawk] Invalid beforeSend value. It should return event or false. Event is sent without changes.' + ); + // structuredClone restores original payload const payload = getSentPayload(); - /** - * payload was mutated in-place (title = ''), so the sent object has the corrupted title. - * The warn is the signal — we verify it fired above. - * We still check that backtrace survived to confirm the rest of the payload is intact. - */ + expect(payload.title).toBe('Error: test-mutated'); expect(payload.backtrace).toBeInstanceOf(Array); }); - it('sends event as is and warns when beforeSend deletes required field', () => { + it('sends original payload and warns when beforeSend deletes required field (title)', () => { initWithBeforeSend((event) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (event as any).title; + + return event; }); HawkCatcher.send(new Error('test-deleted')); - expect(warnSpy).toHaveBeenCalledOnce(); expect(axios.post).toHaveBeenCalledOnce(); + expect(warnSpy).toHaveBeenCalledWith( + '[Hawk] Invalid beforeSend value. It should return event or false. Event is sent without changes.' + ); + // structuredClone restores original payload const payload = getSentPayload(); - /** - * title was deleted in-place, so it's undefined on the sent object. - * The warn signals the problem. We verify the rest of the payload is intact. - */ + expect(payload.title).toBe('Error: test-deleted'); expect(payload.backtrace).toBeInstanceOf(Array); }); diff --git a/tests/breadcrumbs.test.ts b/tests/breadcrumbs.test.ts index 8be42c5..d486f76 100644 --- a/tests/breadcrumbs.test.ts +++ b/tests/breadcrumbs.test.ts @@ -124,25 +124,6 @@ describe('BreadcrumbManager', () => { expect(manager.getBreadcrumbs()).toHaveLength(0); }); - it('stores original breadcrumb and warns when hook returns null', () => { - const manager = BreadcrumbManager.getInstance(); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - manager.init({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - beforeBreadcrumb: () => null as any, - }); - - manager.addBreadcrumb({ type: 'debug', message: 'kept', level: 'info' }); - - const crumbs = manager.getBreadcrumbs(); - - expect(crumbs).toHaveLength(1); - expect(crumbs[0].message).toBe('kept'); - expect(warnSpy).toHaveBeenCalledWith('[Hawk] beforeBreadcrumb returned nothing, storing original breadcrumb.'); - warnSpy.mockRestore(); - }); - it('allows modifying breadcrumb in hook', () => { const manager = BreadcrumbManager.getInstance(); @@ -159,13 +140,19 @@ describe('BreadcrumbManager', () => { expect(manager.getBreadcrumbs()[0].message).toBe('modified'); }); - it('stores original breadcrumb and warns when hook returns undefined (no return)', () => { + it.each([ + { label: 'undefined', value: undefined }, + { label: 'null', value: null }, + { label: 'number (42)', value: 42 }, + { label: 'string ("oops")', value: 'oops' }, + { label: 'true', value: true }, + ])('stores original breadcrumb and warns when hook returns $label', ({ value }) => { const manager = BreadcrumbManager.getInstance(); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); manager.init({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - beforeBreadcrumb: () => undefined as any, + beforeBreadcrumb: () => value as any, }); manager.addBreadcrumb({ type: 'debug', message: 'kept', level: 'info' }); @@ -174,47 +161,56 @@ describe('BreadcrumbManager', () => { expect(crumbs).toHaveLength(1); expect(crumbs[0].message).toBe('kept'); - expect(crumbs[0].timestamp).toBeTypeOf('number'); - expect(warnSpy).toHaveBeenCalledWith('[Hawk] beforeBreadcrumb returned nothing, storing original breadcrumb.'); + expect(warnSpy).toHaveBeenCalledWith( + '[Hawk] Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.' + ); warnSpy.mockRestore(); }); - it('stores original breadcrumb and warns when hook returns true (invalid)', () => { + it('stores original breadcrumb and warns when hook returns object with non-numeric timestamp', () => { const manager = BreadcrumbManager.getInstance(); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); manager.init({ // eslint-disable-next-line @typescript-eslint/no-explicit-any - beforeBreadcrumb: () => true as any, + beforeBreadcrumb: () => ({ timestamp: 'not-a-number', message: 'bad' }) as any, }); - manager.addBreadcrumb({ type: 'debug', message: 'kept', level: 'info' }); + manager.addBreadcrumb({ type: 'debug', message: 'original', level: 'info' }); const crumbs = manager.getBreadcrumbs(); expect(crumbs).toHaveLength(1); - expect(crumbs[0].message).toBe('kept'); - expect(warnSpy).toHaveBeenCalledOnce(); + expect(crumbs[0].message).toBe('original'); + expect(crumbs[0].timestamp).toBeTypeOf('number'); + expect(warnSpy).toHaveBeenCalledWith( + '[Hawk] Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.' + ); warnSpy.mockRestore(); }); - it('stores original breadcrumb and warns when hook returns object with non-numeric timestamp', () => { + it('stores original breadcrumb and warns when hook deletes required field (message)', () => { const manager = BreadcrumbManager.getInstance(); const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); manager.init({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - beforeBreadcrumb: () => ({ timestamp: 'not-a-number', message: 'bad' }) as any, + beforeBreadcrumb: (bc) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (bc as any).message; + + return bc; + }, }); - manager.addBreadcrumb({ type: 'debug', message: 'original', level: 'info' }); + manager.addBreadcrumb({ type: 'debug', message: 'keep-me', level: 'info' }); const crumbs = manager.getBreadcrumbs(); expect(crumbs).toHaveLength(1); - expect(crumbs[0].message).toBe('original'); - expect(crumbs[0].timestamp).toBeTypeOf('number'); - expect(warnSpy).toHaveBeenCalledOnce(); + expect(crumbs[0].message).toBe('keep-me'); + expect(warnSpy).toHaveBeenCalledWith( + '[Hawk] Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.' + ); warnSpy.mockRestore(); }); From 7e074256403adf84ecb217e56f5443a69ccef76b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:27:47 +0300 Subject: [PATCH 2/7] Refactor beforeSend and beforeBreadcrumb hooks to use cloned payloads for validation, ensuring original data remains intact on invalid returns. --- src/index.ts | 15 +++++++-------- src/modules/breadcrumbs.ts | 15 +++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index 863eb56..35f281b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -294,27 +294,26 @@ class Catcher { * Filter sensitive data */ if (typeof this.beforeSend === 'function') { - const original = structuredClone(payload); - const result = this.beforeSend(payload); + const eventClone = structuredClone(payload); + const modified = this.beforeSend(eventClone); /** * false → drop event */ - if (result === false) { + if (modified === false) { return; } /** - * Valid event payload → use it + * Valid event payload → use it instead of original */ - if (isValidEventPayload(result)) { - payload = result; + if (isValidEventPayload(modified)) { + payload = modified; } else { /** - * Anything else is invalid — warn and send original + * Anything else is invalid — warn, payload stays untouched (hook only received a clone) */ console.warn('[Hawk] Invalid beforeSend value. It should return event or false. Event is sent without changes.'); - payload = original; } } diff --git a/src/modules/breadcrumbs.ts b/src/modules/breadcrumbs.ts index 04937fa..6c48858 100644 --- a/src/modules/breadcrumbs.ts +++ b/src/modules/breadcrumbs.ts @@ -137,27 +137,26 @@ export class BreadcrumbManager { }; if (this.options.beforeBreadcrumb) { - const original = structuredClone(bc); - const result = this.options.beforeBreadcrumb(bc, hint); + const breadcrumbClone = structuredClone(bc); + const modified = this.options.beforeBreadcrumb(breadcrumbClone, hint); /** * false means discard */ - if (result === false) { + if (modified === false) { return; } /** - * Valid breadcrumb → use it + * Valid breadcrumb → apply changes from hook */ - if (isValidBreadcrumb(result)) { - Object.assign(bc, result); + if (isValidBreadcrumb(modified)) { + Object.assign(bc, modified); } else { /** - * Anything else is invalid — warn and restore original + * Anything else is invalid — warn, bc stays untouched (hook only received a clone) */ console.warn('[Hawk] Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.'); - Object.assign(bc, original); } } From 2c0111a3e5c9ccdeb4a288eca74beed742761c8b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:32:29 +0300 Subject: [PATCH 3/7] fix: lint --- src/index.ts | 8 ++++---- src/modules/breadcrumbs.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 35f281b..fbe84a8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -295,20 +295,20 @@ class Catcher { */ if (typeof this.beforeSend === 'function') { const eventClone = structuredClone(payload); - const modified = this.beforeSend(eventClone); + const result = this.beforeSend(eventClone); /** * false → drop event */ - if (modified === false) { + if (result === false) { return; } /** * Valid event payload → use it instead of original */ - if (isValidEventPayload(modified)) { - payload = modified; + if (isValidEventPayload(result)) { + payload = result; } else { /** * Anything else is invalid — warn, payload stays untouched (hook only received a clone) diff --git a/src/modules/breadcrumbs.ts b/src/modules/breadcrumbs.ts index 6c48858..699a32f 100644 --- a/src/modules/breadcrumbs.ts +++ b/src/modules/breadcrumbs.ts @@ -66,7 +66,7 @@ function isValidBreadcrumb(v: unknown): v is Breadcrumb { const candidate = v as Record; - if (typeof candidate.message !== 'string' || (candidate.message as string).trim() === '') { + if (typeof candidate.message !== 'string' || candidate.message.trim() === '') { return false; } @@ -138,20 +138,20 @@ export class BreadcrumbManager { if (this.options.beforeBreadcrumb) { const breadcrumbClone = structuredClone(bc); - const modified = this.options.beforeBreadcrumb(breadcrumbClone, hint); + const result = this.options.beforeBreadcrumb(breadcrumbClone, hint); /** * false means discard */ - if (modified === false) { + if (result === false) { return; } /** * Valid breadcrumb → apply changes from hook */ - if (isValidBreadcrumb(modified)) { - Object.assign(bc, modified); + if (isValidBreadcrumb(result)) { + Object.assign(bc, result); } else { /** * Anything else is invalid — warn, bc stays untouched (hook only received a clone) From 567114ab7ca31acf7e7538f09abe2e91a17c491f Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 19:40:17 +0300 Subject: [PATCH 4/7] refactor: rename variable for clarity in beforeSend hook --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index fbe84a8..a3e998b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -294,8 +294,8 @@ class Catcher { * Filter sensitive data */ if (typeof this.beforeSend === 'function') { - const eventClone = structuredClone(payload); - const result = this.beforeSend(eventClone); + const eventPayloadClone = structuredClone(payload); + const result = this.beforeSend(eventPayloadClone); /** * false → drop event From 161b3d54ec46eee536a087a2c4a6e721bf061f1a Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:12:02 +0300 Subject: [PATCH 5/7] refactor: update documentation for beforeSend and beforeBreadcrumb hooks to clarify return value handling --- README.md | 3 +-- src/index.ts | 15 +++++++++++++-- src/modules/breadcrumbs.ts | 17 ++++++++++++++--- types/index.ts | 2 +- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8267ce1..9dd1066 100644 --- a/README.md +++ b/README.md @@ -240,8 +240,7 @@ Use the `beforeSend()` hook to filter data before sending to Hawk. - **Return modified event** — the modified event will be sent - **Return `false`** — the event will be dropped entirely -- **Return nothing (`void` / `undefined` / `null`)** — the original event will be sent as-is -- If `beforeSend` returns an invalid payload, a warning is logged and the original event is sent +- **Any other value is invalid** — the original event is sent as-is (a warning is logged) ```js HawkCatcher.init({ diff --git a/src/index.ts b/src/index.ts index a3e998b..5fce8f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,7 +70,7 @@ class Catcher { * * - Return modified event — it will be sent instead of the original. * - Return `false` — the event will be dropped entirely. - * - Return nothing (`void` / `undefined` / `null`) — the original event is sent as-is (a warning is logged). + * - Any other value is invalid — the original event is sent as-is (a warning is logged). */ private readonly beforeSend?: (event: EventData) => EventData | false | void; @@ -294,7 +294,18 @@ class Catcher { * Filter sensitive data */ if (typeof this.beforeSend === 'function') { - const eventPayloadClone = structuredClone(payload); + let eventPayloadClone: EventData; + + try { + eventPayloadClone = structuredClone(payload); + } catch { + /** + * structuredClone may fail on non-cloneable values (functions, class instances, 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); /** diff --git a/src/modules/breadcrumbs.ts b/src/modules/breadcrumbs.ts index 699a32f..ccd7ce2 100644 --- a/src/modules/breadcrumbs.ts +++ b/src/modules/breadcrumbs.ts @@ -29,8 +29,8 @@ 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). + * * @param breadcrumb - Breadcrumb to store (can be mutated and returned) * @param hint - Optional context (e.g. for filtering) */ @@ -137,7 +137,18 @@ export class BreadcrumbManager { }; 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/types/index.ts b/types/index.ts index 1c4e801..ef93dd6 100644 --- a/types/index.ts +++ b/types/index.ts @@ -32,7 +32,7 @@ export interface HawkNodeJSInitialSettings { * * - Return modified event — it will be sent instead of the original. * - Return `false` — the event will be dropped entirely. - * - Return nothing (`void` / `undefined` / `null`) — the original event is sent as-is (a warning is logged). + * - Any other value is invalid — the original event is sent as-is (a warning is logged). */ beforeSend?(event: EventData): EventData | false | void; From cb8b6731b2131c71edfbafc6e32ae20c1f08244b Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:17:15 +0300 Subject: [PATCH 6/7] fix: lint --- src/modules/breadcrumbs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/breadcrumbs.ts b/src/modules/breadcrumbs.ts index ccd7ce2..86d3077 100644 --- a/src/modules/breadcrumbs.ts +++ b/src/modules/breadcrumbs.ts @@ -30,7 +30,6 @@ export interface BreadcrumbsOptions { * - Return modified breadcrumb — it will be stored instead of the original. * - Return `false` — the breadcrumb will be discarded. * - Any other value is invalid — the original breadcrumb is stored as-is (a warning is logged). - * * @param breadcrumb - Breadcrumb to store (can be mutated and returned) * @param hint - Optional context (e.g. for filtering) */ From 4464ec0b1cdc28bf062cc90d8e604abb5ab55326 Mon Sep 17 00:00:00 2001 From: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:30:28 +0300 Subject: [PATCH 7/7] new test --- tests/before-send.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/before-send.test.ts b/tests/before-send.test.ts index cacfdb9..1d08d23 100644 --- a/tests/before-send.test.ts +++ b/tests/before-send.test.ts @@ -161,6 +161,27 @@ describe('beforeSend processing', () => { expect(payload.backtrace).toBeInstanceOf(Array); }); + it('still sends event when structuredClone throws (non-cloneable payload)', () => { + // Arrange + initWithBeforeSend((event) => event); + const cloneSpy = vi.spyOn(globalThis, 'structuredClone').mockImplementation(() => { + throw new DOMException('could not be cloned', 'DataCloneError'); + }); + + // Act + HawkCatcher.send(new Error('non-cloneable')); + + // Assert — event is still sent, reporting didn't crash + expect(axios.post).toHaveBeenCalledOnce(); + + const payload = getSentPayload(); + + expect(payload.title).toBe('Error: non-cloneable'); + expect(payload.backtrace).toBeInstanceOf(Array); + + cloneSpy.mockRestore(); + }); + it('sends event without optional fields when beforeSend deletes them', () => { initWithBeforeSend((event) => { delete event.release;