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.16",
"version": "3.2.17",
"description": "JavaScript errors tracking for Hawk.so",
"files": [
"dist"
Expand Down
16 changes: 13 additions & 3 deletions packages/javascript/src/addons/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);

/**
Expand Down
13 changes: 12 additions & 1 deletion packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/javascript/tests/before-send.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down