Skip to content
Open
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
31 changes: 26 additions & 5 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 \| null \| void | optional | Filter data before sending. Return modified event, `null` to drop the event, or `void` to keep original. |
| `breadcrumbs` | `false` or object | optional | Pass `false` to disable. Pass options object to configure (see [Breadcrumbs](#breadcrumbs)). Default: enabled. |


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 `null`** — the event will be dropped entirely
- **Return nothing (`void`)** — 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 null;
}

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 null;
}

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
11 changes: 7 additions & 4 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,16 +11,18 @@
"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"
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
26 changes: 24 additions & 2 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 @@ -67,7 +68,7 @@ class Catcher {
/**
* This Method allows developer to filter any data you don't want sending to Hawk
*/
private readonly beforeSend?: (event: EventData<NodeJSAddons>) => EventData<NodeJSAddons>;
private readonly beforeSend?: (event: EventData<NodeJSAddons>) => EventData<NodeJSAddons> | void | null;

/**
* @param settings - If settings is a string, it means an Integration Token
Expand Down Expand Up @@ -260,7 +261,28 @@ 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
*/
if (result === null) {
return;
}

/**
* If user returned a value — use it; if undefined (no return / in-place mutation) — keep payload reference
*/
const candidate = result ?? payload;

if (isValidEventPayload(candidate)) {
payload = candidate;
} else {
console.warn(
'[Hawk] beforeSend produced invalid payload (missing required fields), sending original. '
+ `Received: ${Object.prototype.toString.call(candidate)}`
);
}
}

void this.sendErrorFormatted({
Expand Down
31 changes: 31 additions & 0 deletions src/utils/validate-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { EventData, NodeJSAddons } from '@hawk.so/types';

/**
* Checks if value is a plain object (not array, Date, etc.)
* @param v - value to check
*/
function isPlainObject(v: unknown): v is Record<string, unknown> {
return Object.prototype.toString.call(v) === '[object Object]';
}

/**
* Runtime check for required EventData fields.
* Per @hawk.so/types EventData, `title` is the only non-optional field.
* Additionally validates `backtrace` shape if present (must be an array).
* @param v - value to validate
*/
export function isValidEventPayload(v: unknown): v is EventData<NodeJSAddons> {
if (!isPlainObject(v)) {
return false;
}

if (typeof v.title !== 'string' || v.title.trim() === '') {
return false;
}

if (v.backtrace !== undefined && !Array.isArray(v.backtrace)) {
return false;
}

return true;
}
Loading
Loading