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
35 changes: 28 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Initialization params:
| `release` | string | optional | Unique identifier of the release. |
| `context` | object | optional | Any data you want to pass with every message. |
| `disableGlobalErrorsHandling` | boolean | optional | Do not initialize global errors handling |
| `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. |
| `breadcrumbs` | `false` or object | optional | Pass `false` to disable. Pass options object to configure (see [Breadcrumbs](#breadcrumbs)). Default: enabled. |


Expand Down Expand Up @@ -190,7 +190,7 @@ HawkCatcher.init({
maxBreadcrumbs: 20,
beforeBreadcrumb: (breadcrumb, hint) => {
if (breadcrumb.category === 'auth' && breadcrumb.data?.userId) {
return null; // Discard
return false; // Discard
}
return breadcrumb;
}
Expand All @@ -203,7 +203,7 @@ HawkCatcher.init({
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `maxBreadcrumbs` | `number` | `15` | Maximum number of breadcrumbs to store. When the limit is reached, oldest breadcrumbs are removed (FIFO). |
| `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. |
| `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 Expand Up @@ -236,19 +236,40 @@ HawkCatcher.breadcrumbs.clear();

### Sensitive data filtering

You can filter any data that you don't want to send to Hawk. Use the `beforeSend()` hook for that reason.
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

```js
HawkCatcher.init({
token: 'INTEGRATION TOKEN',
beforeSend(event){
if (event.user && event.user.name){
beforeSend(event) {
// Strip sensitive user data
if (event.user && event.user.name) {
delete event.user.name;
}

return event;
}
})
});
```

Drop an event entirely:

```js
HawkCatcher.init({
token: 'INTEGRATION TOKEN',
beforeSend(event) {
if (event.title.includes('ignore-me')) {
return false;
}

return event;
}
});
```

## License
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ export default [
},
},
{
ignores: ['dist/**', 'node_modules/**', 'playground/**', 'example/**'],
ignores: ['dist/**', 'node_modules/**', 'playground/**', 'example/**', 'tests/**', 'vitest.config.ts'],
},
];
95 changes: 84 additions & 11 deletions example/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,96 @@
const HawkCatcher = require('../dist/cjs/src/index.js').default;

/**
* Initialize Hawk catcher
* Initialize Hawk catcher with breadcrumbs and beforeSend
*/
const HAWK_TOKEN = 'eyJpbnRlZ3JhdGlvbklkIjoiNWIwZjBmYmUtNTM2OS00ODM0LWEwMjctNTZkMTM1YmU1OGU3Iiwic2VjcmV0IjoiYWY4ZjY1OTQtNzExOS00MWVmLWI4ZTAtMTcyMDYwZjBmODc2In0=';
const HAWK_TOKEN = 'eyJpbnRlZ3JhdGlvbklkIjoiOTU3MmQyOWQtNWJhZS00YmYyLTkwN2MtZDk5ZDg5MGIwOTVmIiwic2VjcmV0IjoiZTExODFiZWItMjdlMS00ZDViLWEwZmEtZmUwYTM1Mzg5OWMyIn0=';

HawkCatcher.init(HAWK_TOKEN);
HawkCatcher.init({
token: HAWK_TOKEN,
breadcrumbs: {
maxBreadcrumbs: 20,
beforeBreadcrumb: (breadcrumb, hint) => {
/**
* Example: discard breadcrumbs with sensitive category
*/
if (breadcrumb.category === 'secret') {
return false;
}

return breadcrumb;
},
},
beforeSend(event) {
/**
* Example: strip user name before sending
*/
if (event.user && event.user.name) {
delete event.user.name;
}

return event;
},
});

/**
* --- Breadcrumbs ---
*/

/**
* Add some breadcrumbs before an error
*/
HawkCatcher.breadcrumbs.add({
type: 'debug',
category: 'startup',
message: 'App initialized',
level: 'info',
});

HawkCatcher.breadcrumbs.add({
type: 'debug',
category: 'db',
message: 'Connected to database',
level: 'info',
data: { host: 'localhost', port: 5432 },
});

/**
* This breadcrumb should be discarded by beforeBreadcrumb hook
*/
HawkCatcher.breadcrumbs.add({
type: 'debug',
category: 'secret',
message: 'This should be filtered out',
level: 'warning',
});

/**
* Check breadcrumbs buffer (should have 2, not 3)
*/
const crumbs = HawkCatcher.breadcrumbs.get();

console.log(`Breadcrumbs count: ${crumbs.length} (expected 2)`);

/**
* Error: Hawk NodeJS Catcher test message
* This event will carry the 2 breadcrumbs above
*/
try {
throw new Error('Hawk NodeJS Catcher test message');
} catch (e) {
HawkCatcher.send(e);
}

/**
* Clear breadcrumbs after handling the error
*/
HawkCatcher.breadcrumbs.clear();
console.log(`Breadcrumbs after clear: ${HawkCatcher.breadcrumbs.get().length} (expected 0)`);

/**
* --- Basic error catching ---
*/

/**
* ReferenceError: qwe is not defined
*/
Expand All @@ -44,17 +119,15 @@ try {
}

/**
* Catching a rejects
* Catching promise rejections manually (try-catch does NOT catch async rejections — use .catch())
*/
try {
/**
* Manual handled reject
*/
Promise.reject(Error('Sample error'));
} catch (e) {
Promise.reject(Error('Sample error')).catch((e) => {
HawkCatcher.send(e);
}
});

/**
* These unhandled rejections are caught automatically by the global unhandledRejection handler
*/
Promise.reject(Error('Unhandled sample error'));

Promise.reject('Unhandled error message passed as string');
Expand Down
2 changes: 1 addition & 1 deletion example/sub-example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const HawkCatcher = require('../dist/index').default;
const HawkCatcher = require('../dist/cjs/src/index.js').default;

try {
undefindedFunction();
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hawk.so/nodejs",
"version": "3.3.0",
"version": "3.3.1",
"description": "Node.js catcher for Hawk",
"license": "AGPL-3.0-only",
"engines": {
Expand All @@ -11,22 +11,24 @@
"types": "./dist/mjs/src/index.d.ts",
"exports": {
".": {
"types": "./dist/mjs/src/index.d.ts",
"import": "./dist/mjs/src/index.js",
"require": "./dist/cjs/src/index.js",
"types": "./dist/mjs/src/index.d.ts"
"require": "./dist/cjs/src/index.js"
}
},
"scripts": {
"prebuild": "node -p \"'export const VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
"build": "yarn clean && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node ./create-packages.js",
"build:watch": "yarn clean && tsc-watch",
"example": "node ./example/example.js",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint . --fix",
"lint:test": "eslint .",
"clean": "rimraf dist"
},
"dependencies": {
"@hawk.so/types": "^0.3.0",
"@hawk.so/types": "^0.5.8",
"axios": "^0.21.1",
"stack-trace": "^0.0.10"
},
Expand All @@ -36,7 +38,8 @@
"eslint-config-codex": "^2.0.3",
"rimraf": "^6.1.2",
"tsc-watch": "^7.2.0",
"typescript": "^5.9.3"
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"repository": {
"type": "git",
Expand Down
87 changes: 77 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
} from '@hawk.so/types';
import EventPayload from './modules/event.js';
import { BreadcrumbManager, type BreadcrumbInput, type BreadcrumbHint } from './modules/breadcrumbs.js';
import { isValidEventPayload } from './utils/validate-event.js';
import type { AxiosResponse } from 'axios';
import axios from 'axios';
import { VERSION } from './version.js';
Expand Down Expand Up @@ -65,9 +66,13 @@ class Catcher {
private readonly context?: EventContext;

/**
* This Method allows developer to filter any data you don't want sending to Hawk
* This Method allows developer to filter any data you don't want sending to Hawk.
*
* - 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).
*/
private readonly beforeSend?: (event: EventData<NodeJSAddons>) => EventData<NodeJSAddons>;
private readonly beforeSend?: (event: EventData<NodeJSAddons>) => EventData<NodeJSAddons> | false | void;

/**
* @param settings - If settings is a string, it means an Integration Token
Expand Down Expand Up @@ -154,6 +159,35 @@ class Catcher {
this.formatAndSend(error, context, user);
}

/**
* Add a breadcrumb to the buffer (no-op when breadcrumbs are disabled)
* @param breadcrumb - Breadcrumb data (type, message, category, level, data)
* @param hint - Optional hint for beforeBreadcrumb callback
*/
public addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: BreadcrumbHint): void {
if (this.breadcrumbsEnabled) {
BreadcrumbManager.getInstance().addBreadcrumb(breadcrumb, hint);
}
}

/**
* Get current breadcrumbs snapshot (oldest to newest)
*/
public getBreadcrumbs(): Breadcrumb[] {
return this.breadcrumbsEnabled
? BreadcrumbManager.getInstance().getBreadcrumbs()
: [];
}

/**
* Clear all breadcrumbs
*/
public clearBreadcrumbs(): void {
if (this.breadcrumbsEnabled) {
BreadcrumbManager.getInstance().clear();
}
}

/**
* Returns integration id from integration token
*/
Expand Down Expand Up @@ -260,7 +294,40 @@ class Catcher {
* Filter sensitive data
*/
if (typeof this.beforeSend === 'function') {
payload = this.beforeSend(payload);
const result = this.beforeSend(payload);

/**
* Allow user to intentionally drop event by returning false
*/
if (result === false) {
return;
}

/**
* If user returned nothing (void/undefined/null) — warn and keep original payload
*/
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)) {
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}`
);
}
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

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

When beforeSend returns undefined or null, the implementation sends the original payload without re-validating it. However, if the user mutated the payload in-place (e.g., event.title = '') before returning nothing, the invalid mutated payload will still be sent. Consider adding validation of the payload variable after the beforeSend call to catch in-place mutations that corrupt the payload. This would prevent sending invalid payloads that could cause the same backend errors this PR aims to fix.

Suggested change
/**
* Validate payload after beforeSend to catch in-place mutations that corrupt it
*/
if (!isValidEventPayload(payload)) {
console.warn(
'[Hawk] Event payload is invalid after beforeSend processing, event will not be sent. '
+ `Payload: ${JSON.stringify(payload)}`
);
return;
}

Copilot uses AI. Check for mistakes.
void this.sendErrorFormatted({
Expand Down Expand Up @@ -341,22 +408,22 @@ export default class HawkCatcher {

/**
* Breadcrumbs API (same as in JS catcher: add, get, clear).
* No-op when breadcrumbs were disabled (breadcrumbs: false).
* No-op when breadcrumbs were disabled (breadcrumbs: false) or Catcher is not initialized.
*/
public static get breadcrumbs(): BreadcrumbsAPI {
return {
add: (breadcrumb, hint) => {
if (_instance !== undefined && _instance.breadcrumbsEnabled) {
BreadcrumbManager.getInstance().addBreadcrumb(breadcrumb, hint);
if (_instance !== undefined) {
_instance.addBreadcrumb(breadcrumb, hint);
}
},
get: () =>
_instance !== undefined && _instance.breadcrumbsEnabled
? BreadcrumbManager.getInstance().getBreadcrumbs()
_instance !== undefined
? _instance.getBreadcrumbs()
: [],
clear: () => {
if (_instance !== undefined && _instance.breadcrumbsEnabled) {
BreadcrumbManager.getInstance().clear();
if (_instance !== undefined) {
_instance.clearBreadcrumbs();
}
},
};
Expand Down
Loading
Loading