-
Notifications
You must be signed in to change notification settings - Fork 4
fix(catchers): validate beforeSend return value to avoid sending invalid payload #150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
9bd6a90
f20a748
da99f8a
17f43e2
14cbeab
e79e9c3
c1a81f2
5505090
e02ffd5
4279d77
7e087f7
93e0bfb
d2b0812
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| }); |
| 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 & beforeBreadcrumb hook tests</h1> | ||
| <p>Each button creates a fresh HawkCatcher with the specific hook. Open DevTools → Console & 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> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void; | |
| beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | null | void; |
Copilot
AI
Feb 11, 2026
There was a problem hiding this comment.
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
AI
Feb 11, 2026
There was a problem hiding this comment.
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.
| * 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. ' |
Uh oh!
There was an error while loading. Please reload this page.