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
16 changes: 15 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,24 @@ jobs:
- run: yarn install
- run: yarn lint-test

build:
test:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 2
steps:
- uses: actions/checkout@v1
- name: Use Node.js from .nvmrc
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- run: corepack enable
- run: yarn install
- run: yarn workspace @hawk.so/javascript test

build:
runs-on: ubuntu-latest
env:
CI_JOB_NUMBER: 3
steps:
- uses: actions/checkout@v1
with:
Expand Down
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
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
8 changes: 6 additions & 2 deletions 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 All @@ -20,6 +20,8 @@
"dev": "vite",
"build": "vite build",
"stats": "size-limit > stats.txt",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint --fix \"src/**/*.{js,ts}\""
},
"repository": {
Expand All @@ -40,9 +42,11 @@
"error-stack-parser": "^2.1.4"
},
"devDependencies": {
"@hawk.so/types": "0.5.2",
"@hawk.so/types": "0.5.8",
"jsdom": "^28.0.0",
"vite": "^7.3.1",
"vite-plugin-dts": "^4.2.4",
"vitest": "^4.0.18",
"vue": "^2"
}
}
39 changes: 28 additions & 11 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 @@ -233,16 +236,30 @@ export class BreadcrumbManager {
* Apply beforeBreadcrumb hook
*/
if (this.options.beforeBreadcrumb) {
const result = this.options.beforeBreadcrumb(bc, hint);
const breadcrumbClone = structuredClone(bc);
const result = this.options.beforeBreadcrumb(breadcrumbClone, 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);
/**
* Valid breadcrumb → apply changes from hook
*/
if (isValidBreadcrumb(result)) {
Object.assign(bc, result);
} else {
/**
* Anything else is invalid — warn, bc stays untouched (hook only received a clone)
*/
log(
'Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.',
'warn'
);
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/javascript/src/addons/consoleCatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export class ConsoleCatcher {
* This ensures DevTools will navigate to the user's code, not the interceptor's code.
*
* @param errorStack - Full stack trace string from Error.stack
* @returns Object with userStack (full stack from user code) and fileLine (first frame for DevTools link)
* @returns {object} Object with userStack (full stack from user code) and fileLine (first frame for DevTools link)
*/
private extractUserStack(errorStack: string | undefined): {
userStack: string;
Expand Down Expand Up @@ -250,7 +250,7 @@ export class ConsoleCatcher {
* 4. Store it in the buffer
* 5. Forward the call to the native console (so output still appears in DevTools)
*
* @param {...any} args
* @param args - console method arguments
*/
window.console[method] = (...args: unknown[]): void => {
// Capture full stack trace and extract user code stack
Expand Down
43 changes: 31 additions & 12 deletions packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ import Socket from './modules/socket';
import Sanitizer from './modules/sanitizer';
import log from './utils/log';
import StackParser from './modules/stackParser';
import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI } from './types';
import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI, Transport } from './types';
import { VueIntegration } from './integrations/vue';
import { id } from './utils/id';
import type {
AffectedUser,
EventContext,
JavaScriptAddons,
VueIntegrationAddons,
Json, EncodedIntegrationToken, DecodedIntegrationToken,
Json, EncodedIntegrationToken, DecodedIntegrationToken
} from '@hawk.so/types';
import type { JavaScriptCatcherIntegrations } from './types/integrations';
import { EventRejectedError } from './errors';
import type { HawkJavaScriptEvent } from './types';
import { isErrorProcessed, markErrorAsProcessed } from './utils/event';
import { ConsoleCatcher } from './addons/consoleCatcher';
import { BreadcrumbManager } from './addons/breadcrumbs';
import { validateUser, validateContext } from './utils/validation';
import { validateUser, validateContext, isValidEventPayload } from './utils/validation';

/**
* Allow to use global VERSION, that will be overwritten by Webpack
Expand Down Expand Up @@ -73,16 +73,18 @@ export default class Catcher {
private context: EventContext | undefined;

/**
* This Method allows developer to filter any data you don't want sending to Hawk
* If method returns false, event will not be sent
* 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.
* - Any other value is invalid — the original event is sent as-is (a warning is logged).
*/
private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false);
private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | 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 comment mentions that beforeSend may return null, and the runtime code handles null, but the TypeScript type doesn’t include null (HawkJavaScriptEvent | false | void). Either include null in the return type or remove null from the docs/handling so the public API is consistent.

Suggested change
private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false | void);
private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false | void | null);

Copilot uses AI. Check for mistakes.

/**
* Transport for dialog between Catcher and Collector
* (WebSocket decorator)
* (WebSocket decorator by default, or custom via settings.transport)
*/
private readonly transport: Socket;
private readonly transport: Transport;

/**
* Module for parsing backtrace
Expand Down Expand Up @@ -148,7 +150,7 @@ export default class Catcher {
/**
* Init transport
*/
this.transport = new Socket({
this.transport = settings.transport ?? new Socket({
collectorEndpoint: settings.collectorEndpoint || `wss://${this.getIntegrationId()}.k1.hawk.so:443/ws`,
reconnectionAttempts: settings.reconnectionAttempts,
reconnectionTimeout: settings.reconnectionTimeout,
Expand Down Expand Up @@ -436,12 +438,29 @@ export default class Catcher {
* Filter sensitive data
*/
if (typeof this.beforeSend === 'function') {
const beforeSendResult = this.beforeSend(payload);
const eventPayloadClone = structuredClone(payload);
const result = this.beforeSend(eventPayloadClone);

if (beforeSendResult === false) {
/**
* false → drop event
*/
if (result === false) {
throw new EventRejectedError('Event rejected by beforeSend method.');
}

/**
* Valid event payload → use it instead of original
*/
if (isValidEventPayload(result)) {
payload = result as HawkJavaScriptEvent;
} else {
payload = beforeSendResult;
/**
* Anything else is invalid — warn, payload stays untouched (hook only received a clone)
*/
log(
'Invalid beforeSend value. It should return event or false. Event is sent without changes.',
'warn'
);
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/javascript/src/modules/socket.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import log from '../utils/log';
import type { CatcherMessage } from '@/types';
import type { Transport } from '../types/transport';

/**
* Custom WebSocket wrapper class
*
* @copyright CodeX
*/
export default class Socket {
export default class Socket implements Transport {
/**
* Socket connection endpoint
*/
Expand Down
14 changes: 11 additions & 3 deletions packages/javascript/src/types/hawk-initial-settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { EventContext, AffectedUser } from '@hawk.so/types';
import type { HawkJavaScriptEvent } from './event';
import type { Transport } from './transport';
import type { BreadcrumbsOptions } from '../addons/breadcrumbs';

/**
Expand Down Expand Up @@ -65,10 +66,11 @@ export interface HawkInitialSettings {

/**
* This Method allows you to filter any data you don't want sending to Hawk.
*
* Return `false` to prevent the event from being sent to Hawk.
* - Return modified event — it will be sent instead of the original.
* - Return `false` — the event will be dropped entirely.
* - Any other value is invalid — the original event is sent as-is (a warning is logged).
*/
beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false;
beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | 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.

beforeSend docs mention null as a possible “no return” value, but the declared type is HawkJavaScriptEvent | false | void (no null). If null is intended to be treated like undefined, add null to the signature; otherwise remove it from the docs to keep the API contract consistent.

Suggested change
beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false | void;
beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false | void | null;

Copilot uses AI. Check for mistakes.

/**
* Disable Vue.js error handler
Expand All @@ -90,4 +92,10 @@ export interface HawkInitialSettings {
* @default enabled with default options
*/
breadcrumbs?: false | BreadcrumbsOptions;

/**
* Custom transport for sending events.
* If not provided, default WebSocket transport is used.
*/
transport?: Transport;
}
2 changes: 2 additions & 0 deletions packages/javascript/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { CatcherMessage } from './catcher-message';
import type { HawkInitialSettings } from './hawk-initial-settings';
import type { Transport } from './transport';
import type { HawkJavaScriptEvent } from './event';
import type { VueIntegrationData, NuxtIntegrationData, NuxtIntegrationAddons, JavaScriptCatcherIntegrations } from './integrations';
import type { BreadcrumbsAPI } from './breadcrumbs-api';

export type {
CatcherMessage,
HawkInitialSettings,
Transport,
HawkJavaScriptEvent,
VueIntegrationData,
NuxtIntegrationData,
Expand Down
8 changes: 8 additions & 0 deletions packages/javascript/src/types/transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CatcherMessage } from './catcher-message';

/**
* Transport interface — anything that can send a CatcherMessage
*/
export interface Transport {
send(message: CatcherMessage): Promise<void>;
}
7 changes: 5 additions & 2 deletions packages/javascript/src/utils/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
*
* @param element - HTML element to build selector from
* @param maxDepth - Maximum recursion depth (default: 3)
* @returns CSS selector string (e.g., "div#myId.class1.class2" or ".some-parent button")
* @returns {string} CSS selector string (e.g., "div#myId.class1.class2" or ".some-parent button")
*/
export function buildElementSelector(element: HTMLElement, maxDepth: number = 3): string {
let selector = element.tagName.toLowerCase();

if (element.id) {
selector += `#${element.id}`;

return selector;
}

Expand All @@ -23,7 +24,9 @@ export function buildElementSelector(element: HTMLElement, maxDepth: number = 3)
const classNameStr = String(element.className);

if (classNameStr) {
selector += `.${classNameStr.split(' ').filter(Boolean).join('.')}`;
selector += `.${classNameStr.split(' ').filter(Boolean)
.join('.')}`;

return selector;
}
}
Expand Down
Loading