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
16 changes: 11 additions & 5 deletions packages/javascript/src/addons/consoleCatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @file Module for intercepting console logs with stack trace capture
*/
import type { ConsoleLogEvent } from '@hawk.so/types';
import { getErrorFromErrorEvent, getTitleFromError, getTypeFromError } from '../utils/error';
import { Sanitizer } from '@hawk.so/core';

/**
Expand Down Expand Up @@ -195,13 +196,18 @@ export class ConsoleCatcher {
* @param event - The error event or promise rejection event to convert
*/
private createConsoleEventFromError(event: ErrorEvent | PromiseRejectionEvent): ConsoleLogEvent {
const errorSource = getErrorFromErrorEvent(event);
const sanitizedError = Sanitizer.sanitize(errorSource.rawError);
const message = getTitleFromError(sanitizedError) ?? errorSource.fallbackTitle ?? '<unknown error>';
const type = getTypeFromError(sanitizedError) ?? errorSource.fallbackType ?? 'Error';

if (event instanceof ErrorEvent) {
return {
method: 'error',
timestamp: new Date(),
type: event.error?.name || 'Error',
message: event.error?.message || event.message,
stack: event.error?.stack || '',
type,
message,
stack: (errorSource.rawError as Error)?.stack || '',
fileLine: event.filename
? `${event.filename}:${event.lineno}:${event.colno}`
: '',
Expand All @@ -212,8 +218,8 @@ export class ConsoleCatcher {
method: 'error',
timestamp: new Date(),
type: 'UnhandledRejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack || '',
message,
stack: (errorSource.rawError as Error)?.stack || '',
fileLine: '',
};
}
Expand Down
109 changes: 29 additions & 80 deletions packages/javascript/src/catcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
EventContext,
JavaScriptAddons,
Json,
VueIntegrationAddons
VueIntegrationAddons,
} from '@hawk.so/types';
import type { JavaScriptCatcherIntegrations } from '@/types';
import { ConsoleCatcher } from './addons/consoleCatcher';
Expand All @@ -31,6 +31,7 @@ import {
import { HawkLocalStorage } from './storages/hawk-local-storage';
import { createBrowserLogger } from './logger/logger';
import { BrowserRandomGenerator } from './utils/random';
import { type ErrorSource, getErrorFromErrorEvent, getTitleFromError, getTypeFromError } from './utils/error';

/**
* Allow to use global VERSION, that will be overwritten by Webpack
Expand Down Expand Up @@ -230,7 +231,7 @@ export default class Catcher {
* @param [context] - any additional data to send
*/
public send(message: Error | string, context?: EventContext): void {
void this.formatAndSend(message, undefined, context);
void this.formatAndSend({ rawError: message }, undefined, context);
}

/**
Expand All @@ -242,7 +243,7 @@ export default class Catcher {
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public captureError(error: Error | string, addons?: JavaScriptCatcherIntegrations): void {
void this.formatAndSend(error, addons);
void this.formatAndSend({ rawError: error }, addons);
}

/**
Expand All @@ -255,7 +256,7 @@ export default class Catcher {
this.vue = new VueIntegration(
vue,
(error: Error, addons: VueIntegrationAddons) => {
void this.formatAndSend(error, {
void this.formatAndSend({ rawError: error }, {
vue: addons,
});
},
Expand Down Expand Up @@ -340,21 +341,7 @@ export default class Catcher {
this.consoleCatcher!.addErrorEvent(event);
}

/**
* Promise rejection reason is recommended to be an Error, but it can be a string:
* - Promise.reject(new Error('Reason message')) ——— recommended
* - Promise.reject('Reason message')
*/
let error = (event as ErrorEvent).error || (event as PromiseRejectionEvent).reason;

/**
* Case when error triggered in external script
* We can't access event error object because of CORS
* Event message will be 'Script error.'
*/
if (event instanceof ErrorEvent && error === undefined) {
error = (event as ErrorEvent).message;
}
const error = getErrorFromErrorEvent(event);

void this.formatAndSend(error);
}
Expand All @@ -367,21 +354,21 @@ export default class Catcher {
* @param context - any additional data passed by user
*/
private async formatAndSend(
error: Error | string,
error: ErrorSource,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
integrationAddons?: JavaScriptCatcherIntegrations,
context?: EventContext
): Promise<void> {
try {
const isAlreadySentError = isErrorProcessed(error);
const isAlreadySentError = isErrorProcessed(error.rawError);

if (isAlreadySentError) {
/**
* @todo add debug build and log this case
*/
return;
} else {
markErrorAsProcessed(error);
markErrorAsProcessed(error.rawError);
}

const errorFormatted = await this.prepareErrorFormatted(error, context);
Expand Down Expand Up @@ -424,16 +411,22 @@ export default class Catcher {
* @param error - error to format
* @param context - any additional data passed by user
*/
private async prepareErrorFormatted(error: Error | string, context?: EventContext): Promise<CatcherMessage<typeof Catcher.type>> {
private async prepareErrorFormatted(error: ErrorSource, context?: EventContext): Promise<CatcherMessage<typeof Catcher.type>> {
const { rawError, fallbackTitle, fallbackType } = error;
const sanitizedError = Sanitizer.sanitize(rawError);
const throwableError = rawError instanceof Error ? rawError : undefined;
const title = getTitleFromError(sanitizedError) ?? fallbackTitle ?? '<unknown error>';
const type = getTypeFromError(sanitizedError) ?? fallbackType;

let payload: HawkJavaScriptEvent = {
title: this.getTitle(error),
type: this.getType(error),
title,
type,
release: this.getRelease(),
breadcrumbs: this.getBreadcrumbsForEvent(),
context: this.getContext(context),
user: this.getUser(),
addons: this.getAddons(error),
backtrace: await this.getBacktrace(error),
addons: this.getAddons(throwableError),
backtrace: await this.getBacktrace(throwableError),
catcherVersion: this.version,
};

Expand Down Expand Up @@ -485,44 +478,6 @@ export default class Catcher {
};
}

/**
* Return event title
*
* @param error - event from which to get the title
*/
private getTitle(error: Error | string): string {
const notAnError = !(error instanceof Error);

/**
* Case when error is 'reason' of PromiseRejectionEvent
* and reject() provided with text reason instead of Error()
*/
if (notAnError) {
return error.toString() as string;
}

return (error as Error).message;
}

/**
* Return event type: TypeError, ReferenceError etc
*
* @param error - caught error
*/
private getType(error: Error | string): HawkJavaScriptEvent['type'] {
const notAnError = !(error instanceof Error);

/**
* Case when error is 'reason' of PromiseRejectionEvent
* and reject() provided with text reason instead of Error()
*/
if (notAnError) {
return undefined;
}

return (error as Error).name;
}

/**
* Release version
*/
Expand Down Expand Up @@ -610,21 +565,15 @@ export default class Catcher {
/**
* Return parsed backtrace information
*
* @param error - event from which to get backtrace
* @param {Error} error - event from which to get backtrace
*/
private async getBacktrace(error: Error | string): Promise<HawkJavaScriptEvent['backtrace']> {
const notAnError = !(error instanceof Error);

/**
* Case when error is 'reason' of PromiseRejectionEvent
* and reject() provided with text reason instead of Error()
*/
if (notAnError) {
private async getBacktrace(error?: Error): Promise<HawkJavaScriptEvent['backtrace']> {
if (!error) {
return undefined;
}

try {
return await this.stackParser.parse(error as Error);
return await this.stackParser.parse(error);
} catch (e) {
log('Can not parse stack:', 'warn', e);

Expand All @@ -635,9 +584,9 @@ export default class Catcher {
/**
* Return some details
*
* @param {Error|string} error — caught error
* @param {Error} error — caught error
*/
private getAddons(error: Error | string): HawkJavaScriptEvent['addons'] {
private getAddons(error?: Error): HawkJavaScriptEvent['addons'] {
const { innerWidth, innerHeight } = window;
const userAgent = window.navigator.userAgent;
const location = window.location.href;
Expand Down Expand Up @@ -671,10 +620,10 @@ export default class Catcher {
/**
* Compose raw data object
*
* @param {Error|string} error — caught error
* @param {Error} error — caught error
*/
private getRawData(error: Error | string): Json | undefined {
if (!(error instanceof Error)) {
private getRawData(error?: Error): Json | undefined {
if (!error) {
return;
}

Expand Down
94 changes: 94 additions & 0 deletions packages/javascript/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { HawkJavaScriptEvent } from '@/types';

/**
* Represents a raw error source before title/type normalization.
* Fallback values are provided by the event itself when raw error data is missing.
*/
export type ErrorSource = {
/** The original unsanitized value — use for instanceof checks and backtrace parsing only */
rawError: unknown;
/** Fallback human-readable title used when rawError does not provide one */
fallbackTitle?: string;
/** Fallback error type provided by the caller */
fallbackType?: HawkJavaScriptEvent['type'];
};

/**
* Extracts a human-readable title from an unknown value.
* Prefers `.message` on objects, falls back to the value itself for strings,
* and serializes everything else.
*
* @param value - Any already-safe value prepared by the caller
* @returns The error title string, or undefined if absent or empty
*/
export function getTitleFromError(value: unknown): string | undefined {
if (value == null) {
return undefined;
}

let message: unknown = value;
if (typeof value === 'object' && 'message' in value) {
message = (value as {message?: unknown}).message;
}

if (typeof message === 'string') {
return message || undefined;
}

try {
return JSON.stringify(message);
} catch {
/**
* If no JSON global is available or serialization fails,
* fall back to string conversion
*/
return String(message);
}
}

/**
* Extracts an error type name from an unknown value.
*
* @param value - Any already-safe value prepared by the caller
* @returns The error name string, or undefined if absent or empty
*/
export function getTypeFromError(value: unknown): HawkJavaScriptEvent['type'] | undefined {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it will be simpler

if (typeof value !== 'object' || value === null || !('name' in value)) {
return undefined;
}

const name = (value as {name?: unknown}).name;

return typeof name === 'string' && name ? name : undefined;
}

/**
* Extracts raw error data and event-level fallbacks from ErrorEvent or PromiseRejectionEvent.
* Handles CORS-restricted errors (where event.error is undefined) by falling back to event.message.
*
* @param event - The error or promise rejection event
* @returns Raw error source with optional event-level fallback values
*/
export function getErrorFromErrorEvent(event: ErrorEvent | PromiseRejectionEvent): ErrorSource {
if (event.type === 'error') {
event = event as ErrorEvent;

return {
rawError: event.error,
fallbackTitle: event.message
? (event.filename ? `'${event.message}' at ${event.filename}:${event.lineno}:${event.colno}` : event.message)
: undefined,
};
}

if (event.type === 'unhandledrejection') {
Comment thread
FeironoX5 marked this conversation as resolved.
event = event as PromiseRejectionEvent;

return {
rawError: event.reason,
fallbackType: 'UnhandledRejection',
};
}

return { rawError: undefined };
}
Loading
Loading