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
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Initialization params:
| `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 |
| `breadcrumbs` | `false` or object | optional | Pass `false` to disable. Pass options object to configure (see [Breadcrumbs](#breadcrumbs)). Default: enabled. |


## Usage
Expand Down Expand Up @@ -152,6 +153,87 @@ Available fields:
}
```

### Breadcrumbs

Breadcrumbs track events leading up to an error, providing context for debugging. Same API as [@hawk.so/javascript](https://www.npmjs.com/package/@hawk.so/javascript) (add, get, clear); in Node there is no automatic tracking of fetch/navigation/clicks, only manual breadcrumbs.

#### Default configuration

By default, breadcrumbs are enabled (custom breadcrumbs only):

```js
HawkCatcher.init({
token: 'INTEGRATION_TOKEN'
// breadcrumbs enabled by default
});
```

#### Disabling breadcrumbs

To disable breadcrumbs entirely:

```js
HawkCatcher.init({
token: 'INTEGRATION_TOKEN',
breadcrumbs: false
});
```

#### Custom configuration

Configure breadcrumbs (same options as JS where applicable):

```js
HawkCatcher.init({
token: 'INTEGRATION_TOKEN',
breadcrumbs: {
maxBreadcrumbs: 20,
beforeBreadcrumb: (breadcrumb, hint) => {
if (breadcrumb.category === 'auth' && breadcrumb.data?.userId) {
return null; // Discard
}
return breadcrumb;
}
}
});
```

#### Breadcrumbs options

| 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. |

#### Manual breadcrumbs

Add custom breadcrumbs manually. Breadcrumbs accumulate in a buffer and are attached to every event until explicitly cleared via `HawkCatcher.breadcrumbs.clear()`:

```js
HawkCatcher.breadcrumbs.add({
type: 'logic',
category: 'auth',
message: 'User logged in',
level: 'info',
data: { userId: '123' }
});
```

#### Breadcrumb methods

Same as in JS catcher:

```js
// Add a breadcrumb
HawkCatcher.breadcrumbs.add(breadcrumb, hint);

// Get current breadcrumbs
const breadcrumbs = HawkCatcher.breadcrumbs.get();

// Clear all breadcrumbs
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.
Expand Down
2 changes: 1 addition & 1 deletion example/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* HawkCatcher.send(err);
* }
*/
const HawkCatcher = require('../dist/src/index').default;
const HawkCatcher = require('../dist/cjs/src/index.js').default;

/**
* Initialize Hawk catcher
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hawk.so/nodejs",
"version": "3.2.0",
"version": "3.3.0",
"description": "Node.js catcher for Hawk",
"license": "AGPL-3.0-only",
"engines": {
Expand Down
65 changes: 60 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import process from 'process';
import type {
EventContext,
AffectedUser,
Breadcrumb,
EncodedIntegrationToken,
DecodedIntegrationToken,
EventData,
NodeJSAddons,
Json
} from '@hawk.so/types';
import EventPayload from './modules/event.js';
import { BreadcrumbManager, type BreadcrumbInput, type BreadcrumbHint } from './modules/breadcrumbs.js';
import type { AxiosResponse } from 'axios';
import axios from 'axios';
import { VERSION } from './version.js';
Expand All @@ -32,6 +34,11 @@ let _instance: Catcher;
* @copyright CodeX
*/
class Catcher {
/**
* Whether breadcrumbs are enabled (false when settings.breadcrumbs === false)
*/
public readonly breadcrumbsEnabled: boolean;

/**
* Type is a family name of a catcher
*/
Expand Down Expand Up @@ -76,6 +83,15 @@ class Catcher {
this.context = settings.context ?? undefined;
this.release = settings.release ?? undefined;
this.beforeSend = settings.beforeSend?.bind(undefined);
this.breadcrumbsEnabled = settings.breadcrumbs !== false;

if (this.breadcrumbsEnabled) {
BreadcrumbManager.getInstance().init(
typeof settings.breadcrumbs === 'object' && settings.breadcrumbs !== null
? settings.breadcrumbs
: {}
);
}

if (!this.token) {
throw new Error('Integration Token is missed. You can get it on https://hawk.so at Project Settings.');
Expand Down Expand Up @@ -227,15 +243,18 @@ class Catcher {
*/
private formatAndSend(err: Error, context?: EventContext, user?: AffectedUser): void {
const eventPayload = new EventPayload(err);
let payload: EventData<NodeJSAddons> = {
const breadcrumbs = this.breadcrumbsEnabled ? BreadcrumbManager.getInstance().getBreadcrumbs() : [];

let payload = {
title: eventPayload.getTitle(),
type: eventPayload.getType(),
backtrace: eventPayload.getBacktrace(),
user: this.getUser(user),
context: this.getContext(context),
release: this.release,
catcherVersion: Catcher.getVersion(),
};
breadcrumbs: breadcrumbs.length > 0 ? breadcrumbs : null,
} as EventData<NodeJSAddons>;

/**
* Filter sensitive data
Expand Down Expand Up @@ -319,8 +338,44 @@ export default class HawkCatcher {
return _instance.send(error, context, user);
}
}

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

/**
* Breadcrumbs API - same surface as in @hawk.so/javascript (add, get, clear)
*/
export interface BreadcrumbsAPI {
/** Add a breadcrumb to the buffer (attached to every event until cleared) */
add(breadcrumb: BreadcrumbInput, hint?: BreadcrumbHint): void;

/** Get current breadcrumbs snapshot (oldest to newest) */
get(): Breadcrumb[];

/** Clear all breadcrumbs */
clear(): void;
}

export type {
HawkNodeJSInitialSettings
};
export type { BreadcrumbInput, BreadcrumbHint, BreadcrumbsOptions } from './modules/breadcrumbs.js';
export type { HawkNodeJSInitialSettings } from '../types/index.js';
143 changes: 143 additions & 0 deletions src/modules/breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @file Breadcrumbs module - chronological trail of events before an error. Custom breadcrumbs only (HawkCatcher.breadcrumbs.add()). Possible future auto-capture: outgoing HTTP, unhandledRejection/uncaughtException, console.log/intercept, DB query hooks.
*/
import type { Breadcrumb } from '@hawk.so/types';

/**
* Default maximum number of breadcrumbs to store
*/
const DEFAULT_MAX_BREADCRUMBS = 15;

/**
* Hint object passed to beforeBreadcrumb callback (same concept as in JS catcher; in Node no event/response yet, use for custom context)
*/
export interface BreadcrumbHint {
[key: string]: unknown;
}

/**
* Configuration options for breadcrumbs (same shape as @hawk.so/javascript; no trackFetch/trackNavigation/trackClicks in Node)
*/
export interface BreadcrumbsOptions {
/**
* Maximum number of breadcrumbs to store (FIFO). When the limit is reached, oldest are removed.
* @default 15
*/
maxBreadcrumbs?: number;

/**
* Hook called before each breadcrumb is stored. Return null to discard. Return modified breadcrumb to store it.
* @param breadcrumb - Breadcrumb to store (can be mutated and returned)
* @param hint - Optional context (e.g. for filtering)
* @returns Modified breadcrumb to store, or null to discard
*/
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
}

/**
* Breadcrumb input - timestamp optional (auto-generated if omitted). Same as @hawk.so/javascript BreadcrumbInput.
*/
// eslint-disable-next-line jsdoc/require-jsdoc -- type alias documented above
export type BreadcrumbInput = Omit<Breadcrumb, 'timestamp'> & { timestamp?: Breadcrumb['timestamp'] };

/**
* Internal breadcrumbs options (all fields set during init from BreadcrumbsOptions)
*/
interface InternalBreadcrumbsOptions {
/** Maximum number of breadcrumbs to keep (FIFO) */
maxBreadcrumbs: number;

/** Optional hook before storing each breadcrumb */
beforeBreadcrumb?: BreadcrumbsOptions['beforeBreadcrumb'];
}

/**
* Manages breadcrumb buffer and add/get/clear API
*/
export class BreadcrumbManager {
private static instance: BreadcrumbManager | null = null;

private readonly breadcrumbs: Breadcrumb[] = [];

private options: InternalBreadcrumbsOptions = {
maxBreadcrumbs: DEFAULT_MAX_BREADCRUMBS,
};

private isInitialized = false;

/**
* Private constructor for singleton
*/
private constructor() {}

/**
* Get singleton instance (created on first call).
* @returns The shared BreadcrumbManager
*/
public static getInstance(): BreadcrumbManager {
if (BreadcrumbManager.instance === null) {
BreadcrumbManager.instance = new BreadcrumbManager();
}

return BreadcrumbManager.instance;
}

/**
* Initialize with options. Call once when HawkCatcher.init() runs.
* @param options - Configuration (maxBreadcrumbs, beforeBreadcrumb)
*/
public init(options: BreadcrumbsOptions = {}): void {
if (this.isInitialized) {
return;
}

this.options = {
maxBreadcrumbs: options.maxBreadcrumbs ?? DEFAULT_MAX_BREADCRUMBS,
beforeBreadcrumb: options.beforeBreadcrumb,
};

this.isInitialized = true;
}

/**
* Add a breadcrumb. Timestamp is set to Date.now() if omitted.
* @param breadcrumb - Breadcrumb data (type, message, category, level, data)
* @param hint - Optional hint for beforeBreadcrumb callback
*/
public addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: BreadcrumbHint): void {
const bc: Breadcrumb = {
...breadcrumb,
timestamp: breadcrumb.timestamp ?? Date.now(),
};

if (this.options.beforeBreadcrumb) {
const result = this.options.beforeBreadcrumb(bc, hint);

if (result === null) {
return;
}

Object.assign(bc, result);
}

this.breadcrumbs.push(bc);

if (this.breadcrumbs.length > this.options.maxBreadcrumbs) {
this.breadcrumbs.shift();
}
}

/**
* Snapshot of current breadcrumbs (oldest to newest)
*/
public getBreadcrumbs(): Breadcrumb[] {
return [...this.breadcrumbs];
}

/**
* Clear all breadcrumbs (e.g. after sending an event)
*/
public clear(): void {
this.breadcrumbs.length = 0;
}
}
9 changes: 9 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { EventData, NodeJSAddons } from '@hawk.so/types';
import type { BreadcrumbsOptions } from '../src/modules/breadcrumbs.js';

export type { BreadcrumbsOptions };

/**
* Initial settings object
Expand Down Expand Up @@ -34,6 +37,12 @@ export interface HawkNodeJSInitialSettings {
* This options still allow you send events manually
*/
disableGlobalErrorsHandling?: boolean;

/**
* Pass false to disable breadcrumbs. Pass options object to configure maxBreadcrumbs and beforeBreadcrumb hook.
* @default { maxBreadcrumbs: 15 }
*/
breadcrumbs?: false | BreadcrumbsOptions;
}

/**
Expand Down
Loading