Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
6 changes: 3 additions & 3 deletions packages/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Initialization settings:
| `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling |
| `consoleTracking` | boolean | optional | Initialize console logs tracking |
| `breadcrumbs` | false or BreadcrumbsOptions object | optional | Configure breadcrumbs tracking (see below) |
| `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk |
| `beforeSend` | function(event) => event \| false \| void | optional | Filter data before sending. Return modified event, `false` to drop the event. |

Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition.

Expand Down Expand Up @@ -187,7 +187,7 @@ const hawk = new HawkCatcher({
beforeBreadcrumb: (breadcrumb, hint) => {
// Filter or modify breadcrumbs before storing
if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) {
return null; // Discard this breadcrumb
return false; // Discard this breadcrumb
}
return breadcrumb;
}
Expand All @@ -203,7 +203,7 @@ const hawk = new HawkCatcher({
| `trackFetch` | `boolean` | `true` | Automatically track `fetch()` and `XMLHttpRequest` calls as breadcrumbs. Captures request URL, method, status code, and response time. |
| `trackNavigation` | `boolean` | `true` | Automatically track navigation events (History API: `pushState`, `replaceState`, `popstate`). Captures route changes. |
| `trackClicks` | `boolean` | `true` | Automatically track UI click events. Captures element selector, coordinates, and other click metadata. |
| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)` and can return modified breadcrumb, `null` to discard it, or the original breadcrumb. Useful for filtering sensitive data or PII. |
| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)`. Return modified breadcrumb to keep it, `false` to discard. |

### Manual Breadcrumbs

Expand Down
131 changes: 131 additions & 0 deletions packages/javascript/example/before-send-tests.js
Comment thread
neSpecc marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* beforeSend tests
*
* Three core scenarios:
* 1. Return modified event → event is sent with changes
* 2. Return false → event is dropped, nothing is sent
* 3. Return nothing (undefined) → original event is sent, warning in console
*/
const bsOutput = document.getElementById('before-send-output');

document.getElementById('btn-bs-modify').addEventListener('click', () => {
bsOutput.textContent = '';

const hawk = new window.HawkCatcher({
token: window.HAWK_TOKEN,
disableGlobalErrorsHandling: true,
beforeSend(event) {
event.context = { sanitized: true };

return event;
},
});

hawk.send(new Error('beforeSend: modify test'));

bsOutput.textContent =
'Expected: event sent with context.sanitized = true\n'
+ 'Check: DevTools → Network tab, outgoing WebSocket message should contain "sanitized"';
});

document.getElementById('btn-bs-drop').addEventListener('click', () => {
bsOutput.textContent = '';

const hawk = new window.HawkCatcher({
token: window.HAWK_TOKEN,
disableGlobalErrorsHandling: true,
beforeSend() {
return false;
},
});

hawk.send(new Error('beforeSend: drop test'));

bsOutput.textContent =
'Expected: event NOT sent (dropped by beforeSend)\n'
+ 'Check: DevTools → Network tab, no new WebSocket message should appear';
});

document.getElementById('btn-bs-void').addEventListener('click', () => {
bsOutput.textContent = '';

const hawk = new window.HawkCatcher({
token: window.HAWK_TOKEN,
disableGlobalErrorsHandling: true,
beforeSend() {
/* no return */
},
});

hawk.send(new Error('beforeSend: void test'));

bsOutput.textContent =
'Expected: original event sent as-is, warning logged\n'
+ 'Check: DevTools → Console should show "[Hawk] Invalid beforeSend value: (undefined)..."';
});

/**
* beforeBreadcrumb test
*
* BreadcrumbManager is a singleton — only the first init() takes effect.
* We test all three scenarios in a single run with one beforeBreadcrumb
* that handles each case based on the breadcrumb message.
*
* Messages:
* - "modify me" → returns modified breadcrumb (message prefixed with "MODIFIED:")
* - "drop me" → returns false (breadcrumb discarded)
* - "void me" → returns undefined (original stored, warning in console)
*/
const bbcOutput = document.getElementById('before-bc-output');

document.getElementById('btn-bbc-run').addEventListener('click', () => {
bbcOutput.textContent = 'Running...';

const hawk = new window.HawkCatcher({
token: window.HAWK_TOKEN,
disableGlobalErrorsHandling: true,
breadcrumbs: {
trackFetch: false,
trackNavigation: false,
trackClicks: false,
beforeBreadcrumb(bc) {
if (bc.message === 'modify me') {
bc.message = 'MODIFIED: ' + bc.message;

return bc;
}

if (bc.message === 'drop me') {
return false;
}

/* "void me" — no return */
},
},
});

hawk.breadcrumbs.clear();

hawk.breadcrumbs.add({ type: 'logic', message: 'modify me', level: 'info' });
hawk.breadcrumbs.add({ type: 'logic', message: 'drop me', level: 'info' });
hawk.breadcrumbs.add({ type: 'logic', message: 'void me', level: 'info' });

const crumbs = hawk.breadcrumbs.get();
const messages = crumbs.map((c) => c.message);

const modifyPass = messages.includes('MODIFIED: modify me');
const dropPass = !messages.includes('drop me');
const voidPass = messages.includes('void me');

const lines = [
`1. Modify: ${modifyPass ? 'PASS' : 'FAIL'} — stored "${messages.find((m) => m.startsWith('MODIFIED:')) || '(not found)'}"`,
`2. Drop: ${dropPass ? 'PASS' : 'FAIL'} — "drop me" ${dropPass ? 'not in list' : 'still present'}`,
`3. Void: ${voidPass ? 'PASS' : 'FAIL'} — "void me" ${voidPass ? 'stored as-is' : 'missing'}`,
'',
'Console should show: [Hawk] beforeBreadcrumb returned nothing...',
'',
`All stored messages: ${JSON.stringify(messages)}`,
];

bbcOutput.textContent = lines.join('\n');
});
113 changes: 113 additions & 0 deletions packages/javascript/example/hooks-tests.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hawk — beforeSend / beforeBreadcrumb tests</title>
<link
href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: Roboto, system-ui, sans-serif;
margin: 0;
padding: 0;
background: #2f3341;
color: #dbe6ff;
font-size: 13px;
}

header {
padding: 30px;
background: #242732;
}

h1 {
font-weight: bold;
font-size: 20px;
margin: 0 0 5px;
}

header p {
margin: 0;
opacity: 0.5;
font-size: 13px;
}

section {
padding: 15px;
border: 1px solid rgba(219, 230, 255, 0.1);
border-radius: 4px;
margin: 15px;
}

h2 {
font-weight: 500;
margin: 0 0 10px;
font-size: 13px;
color: rgba(219, 230, 255, 0.6);
letter-spacing: 0.24px;
text-transform: uppercase;
}

button {
display: inline-block;
padding: 8px 20px;
border: 0;
border-radius: 5px;
background: #4979e4;
color: #dbe6ff;
font-weight: 500;
font-size: 14px;
cursor: pointer;
}

button:hover {
background: #4869d2;
}

.output {
margin-top: 15px;
padding: 10px;
background: rgba(36, 39, 50, 0.68);
border-radius: 3px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
min-height: 20px;
}
</style>
</head>
<body>
<header>
<h1>beforeSend &amp; beforeBreadcrumb hook tests</h1>
<p>Each button creates a fresh HawkCatcher with the specific hook. Open DevTools → Console &amp; Network.</p>
</header>

<section>
<h2>beforeSend</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
<button id="btn-bs-modify">Modify event</button>
<button id="btn-bs-drop">Drop event (false)</button>
<button id="btn-bs-void">No return (void)</button>
</div>
<div id="before-send-output" class="output"></div>
</section>

<section>
<h2>beforeBreadcrumb</h2>
<button id="btn-bbc-run">Run all 3 scenarios</button>
<div id="before-bc-output" class="output"></div>
</section>

<script type="module">
import HawkCatcher from '../src';

const HAWK_TOKEN = 'eyJpbnRlZ3JhdGlvbklkIjoiOTU3MmQyOWQtNWJhZS00YmYyLTkwN2MtZDk5ZDg5MGIwOTVmIiwic2VjcmV0IjoiZTExODFiZWItMjdlMS00ZDViLWEwZmEtZmUwYTM1Mzg5OWMyIn0=';

window.HawkCatcher = HawkCatcher;
window.HAWK_TOKEN = HAWK_TOKEN;
</script>
<script src="before-send-tests.js"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion packages/javascript/example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ <h2>Test Vue integration: &lt;test-component&gt;</h2>
// beforeBreadcrumb: (breadcrumb, hint) => {
// // Filter or modify breadcrumbs before storing
// if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) {
// return null; // Discard this breadcrumb
// return false; // Discard this breadcrumb
// }
// return breadcrumb;
// }
Expand Down
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.15",
"version": "3.2.16",
"description": "JavaScript errors tracking for Hawk.so",
"files": [
"dist"
Expand Down
36 changes: 26 additions & 10 deletions packages/javascript/src/addons/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from
import Sanitizer from '../modules/sanitizer';
import { buildElementSelector } from '../utils/selector';
import log from '../utils/log';
import { isValidBreadcrumb } from '../utils/validation';

/**
* Default maximum number of breadcrumbs to store
Expand Down Expand Up @@ -48,11 +49,13 @@ export interface BreadcrumbsOptions {
maxBreadcrumbs?: number;

/**
* Hook called before each breadcrumb is stored
* Return null to discard the breadcrumb
* Return modified breadcrumb to store it
* 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.
*/
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The JSDoc says beforeBreadcrumb can return null, and the runtime path handles null, but the callback type is Breadcrumb | false | void (no null). Align the public type signature with the documented/runtime behavior (either add null or remove null from docs and handling).

Suggested change
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void;
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | null | void;

Copilot uses AI. Check for mistakes.

/**
* Enable automatic fetch/XHR breadcrumbs
Expand Down Expand Up @@ -91,7 +94,7 @@ interface InternalBreadcrumbsOptions {
trackFetch: boolean;
trackNavigation: boolean;
trackClicks: boolean;
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void;
}

/**
Expand Down Expand Up @@ -235,14 +238,27 @@ export class BreadcrumbManager {
if (this.options.beforeBreadcrumb) {
const result = this.options.beforeBreadcrumb(bc, hint);

if (result === null) {
/**
* Discard breadcrumb
*/
/**
* false means discard
*/
if (result === false) {
return;
}
Comment on lines +242 to 247
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

beforeBreadcrumb previously used null to discard breadcrumbs, but this change switches the sentinel to false and now treats null as “no return” (stores the breadcrumb). That’s a breaking behavior change for existing consumers; consider continuing to treat null as discard (possibly with a deprecation warning) or bump the package with an appropriate breaking-change version.

Copilot uses AI. Check for mistakes.

Object.assign(bc, result);
/**
* void/undefined/null — warn and keep original breadcrumb
*/
if (result === undefined || result === null) {
log('[Hawk] beforeBreadcrumb returned nothing, storing original breadcrumb.', 'warn');
} else if (isValidBreadcrumb(result)) {
Object.assign(bc, result);
} else {
log(
'[Hawk] beforeBreadcrumb produced invalid breadcrumb (must be an object with numeric timestamp), storing original. '
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

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

The log message says it’s “storing original breadcrumb”, but beforeBreadcrumb is called with the live bc object, so in-place mutations will still be stored even when the hook returns undefined/null or an invalid value. Consider cloning bc before invoking the hook or reword the warning to avoid implying the breadcrumb is unchanged.

Suggested change
* void/undefined/null warn and keep original breadcrumb
*/
if (result === undefined || result === null) {
log('[Hawk] beforeBreadcrumb returned nothing, storing original breadcrumb.', 'warn');
} else if (isValidBreadcrumb(result)) {
Object.assign(bc, result);
} else {
log(
'[Hawk] beforeBreadcrumb produced invalid breadcrumb (must be an object with numeric timestamp), storing original. '
* void/undefined/null warn and keep breadcrumb as last modified by hook
*/
if (result === undefined || result === null) {
log('[Hawk] beforeBreadcrumb returned nothing; keeping breadcrumb as last modified by hook.', 'warn');
} else if (isValidBreadcrumb(result)) {
Object.assign(bc, result);
} else {
log(
'[Hawk] beforeBreadcrumb produced invalid breadcrumb (must be an object with numeric timestamp); keeping breadcrumb as last modified by hook. '

Copilot uses AI. Check for mistakes.
+ `Received: ${Object.prototype.toString.call(result)}`,
'warn'
);
}
}

/**
Expand Down
Loading
Loading