diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 30fe1af4..14a8f200 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,3 +13,5 @@ export { buildElementSelector } from './utils/selector'; export { EventRejectedError } from './errors'; export { isErrorProcessed, markErrorAsProcessed } from './utils/event'; export type { BreadcrumbStore, BreadcrumbsAPI, BreadcrumbHint, BreadcrumbInput } from './breadcrumbs/breadcrumb-store'; +export type { MessageHint, MessageProcessor } from './messages/message-processor'; +export { BreadcrumbsMessageProcessor } from './messages/breadcrumbs-message-processor';; diff --git a/packages/core/src/messages/breadcrumbs-message-processor.ts b/packages/core/src/messages/breadcrumbs-message-processor.ts new file mode 100644 index 00000000..9d43accb --- /dev/null +++ b/packages/core/src/messages/breadcrumbs-message-processor.ts @@ -0,0 +1,25 @@ +import type { CatcherMessagePayload } from '@hawk.so/types'; +import type { MessageHint, MessageProcessor } from './message-processor'; + +/** + * Attaches breadcrumbs snapshot from {@link hint} to payload. + */ +export class BreadcrumbsMessageProcessor implements MessageProcessor<'errors/javascript'> { + /** + * Sets `payload.breadcrumbs` from hint snapshot if non-empty; skips otherwise. + * + * @param payload - event message payload to enrich + * @param hint - hint carrying breadcrumbs snapshot captured at error time + * @returns modified payload with breadcrumbs set, or original payload unchanged + */ + public apply( + payload: CatcherMessagePayload<'errors/javascript'>, + hint?: MessageHint + ): CatcherMessagePayload<'errors/javascript'> | null { + if (hint?.breadcrumbs && hint.breadcrumbs.length > 0) { + payload.breadcrumbs = hint.breadcrumbs; + } + + return payload; + } +} diff --git a/packages/core/src/messages/message-processor.ts b/packages/core/src/messages/message-processor.ts new file mode 100644 index 00000000..a2d0892b --- /dev/null +++ b/packages/core/src/messages/message-processor.ts @@ -0,0 +1,36 @@ +import type { Breadcrumb, CatcherMessagePayload, CatcherMessageType } from '@hawk.so/types'; + +/** + * Snapshot of event context captured synchronously at error time, + * before any processing. + */ +export interface MessageHint { + /** + * Original caught error. + */ + error?: Error | string; + + /** + * Breadcrumbs captured at error time. + */ + breadcrumbs?: Breadcrumb[]; +} + +/** + * Single step in message processing pipeline before message is sent. + * + * @typeParam T - catcher message type this processor handles + */ +export interface MessageProcessor { + /** + * Handles input message. May mutate or replace it. + * + * @param payload - processed event message payload + * @param hint - additional context about original error + * @returns modified payload, or `null` to drop event + */ + apply( + payload: CatcherMessagePayload, + hint?: MessageHint, + ): CatcherMessagePayload | null +} diff --git a/packages/core/src/modules/sanitizer.ts b/packages/core/src/modules/sanitizer.ts index ba2488ed..44289824 100644 --- a/packages/core/src/modules/sanitizer.ts +++ b/packages/core/src/modules/sanitizer.ts @@ -154,7 +154,6 @@ export class Sanitizer { depth: number, seen: WeakSet ): Record | '' | '' { - // If the maximum depth is reached, return a placeholder if (depth > Sanitizer.maxDepth) { return ''; diff --git a/packages/javascript/src/addons/userAgentInfo.ts b/packages/javascript/src/addons/userAgentInfo.ts deleted file mode 100644 index ec7c3706..00000000 --- a/packages/javascript/src/addons/userAgentInfo.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @file Integration for adding User Agent info - */ - -/** - * @param event - event to modify - * @param data - event data - */ -export default function (event, data): void { - data.payload.userAgent = { - name: window.navigator.userAgent, - frame: { - width: window.innerWidth, - height: window.innerHeight, - }, - }; -} diff --git a/packages/javascript/src/catcher.ts b/packages/javascript/src/catcher.ts index e75c4108..ed0cee0d 100644 --- a/packages/javascript/src/catcher.ts +++ b/packages/javascript/src/catcher.ts @@ -4,19 +4,18 @@ import type { CatcherMessage, HawkInitialSettings, HawkJavaScriptEvent, Transpor import { VueIntegration } from './integrations/vue'; import type { AffectedUser, + CatcherMessagePayload, DecodedIntegrationToken, EncodedIntegrationToken, EventContext, - JavaScriptAddons, - Json, VueIntegrationAddons } from '@hawk.so/types'; import type { JavaScriptCatcherIntegrations } from '@/types'; import { ConsoleCatcher } from './addons/consoleCatcher'; import { BrowserBreadcrumbStore } from './addons/breadcrumbs'; -import type { BreadcrumbStore } from '@hawk.so/core'; +import type { BreadcrumbStore, MessageProcessor } from '@hawk.so/core'; import { - EventRejectedError, + BreadcrumbsMessageProcessor, HawkUserManager, isErrorProcessed, isLoggerSet, @@ -32,6 +31,9 @@ import { import { HawkLocalStorage } from './storages/hawk-local-storage'; import { createBrowserLogger } from './logger/logger'; import { BrowserRandomGenerator } from './utils/random'; +import { BrowserMessageProcessor } from './messages/browser-message-processor'; +import { ConsoleCatcherMessageProcessor } from './messages/console-catcher-message-processor'; +import { DebugMessageProcessor } from './messages/debug-message-processor'; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -134,6 +136,11 @@ export default class Catcher { new BrowserRandomGenerator() ); + /** + * Ordered list of message processors applied to every outgoing event message. + */ + private readonly messageProcessors: MessageProcessor[]; + /** * Catcher constructor * @@ -162,6 +169,9 @@ export default class Catcher { settings.consoleTracking !== null && settings.consoleTracking !== undefined ? settings.consoleTracking : true; + this.messageProcessors = [ + new BrowserMessageProcessor(), + ]; if (!this.token) { log( @@ -189,7 +199,7 @@ export default class Catcher { if (this.consoleTracking) { this.consoleCatcher = ConsoleCatcher.getInstance(); - this.consoleCatcher.init(); + this.messageProcessors.push(new ConsoleCatcherMessageProcessor(this.consoleCatcher)); } /** @@ -198,10 +208,15 @@ export default class Catcher { if (settings.breadcrumbs !== false) { this.breadcrumbStore = BrowserBreadcrumbStore.getInstance(); this.breadcrumbStore.init(settings.breadcrumbs ?? {}); + this.messageProcessors.push(new BreadcrumbsMessageProcessor()); } else { this.breadcrumbStore = null; } + if (this.debug) { + this.messageProcessors.push(new DebugMessageProcessor()); + } + /** * Set global handlers */ @@ -361,7 +376,12 @@ export default class Catcher { } /** - * Format and send an error + * Process and sends error message. + * + * Returns early without sending either if + * - error was already processed, + * - message processor drops it + * - {@link beforeSend} hook rejects it * * @param error - error to send * @param integrationAddons - addons spoiled by Integration @@ -385,105 +405,117 @@ export default class Catcher { markErrorAsProcessed(error); } - const errorFormatted = await this.prepareErrorFormatted(error, context); + const hint = { error, + breadcrumbs: this.breadcrumbStore?.get() }; + let payload = await this.buildBasePayload(error, context); + + for (const processor of this.messageProcessors) { + const result = processor.apply(payload, hint); + + if (result === null) { + return; + } + + payload = result; + } - /** - * If this event caught by integration (Vue or other), it can pass extra addons - */ if (integrationAddons) { - this.appendIntegrationAddons(errorFormatted, Sanitizer.sanitize(integrationAddons)); + payload.addons = { + ...(payload.addons ?? {}), + ...Sanitizer.sanitize(integrationAddons), + }; } - this.sendErrorFormatted(errorFormatted); - } catch (e) { - if (e instanceof EventRejectedError) { - /** - * Event was rejected by user using the beforeSend method - */ + const filtered = this.applyBeforeSendHook(payload); + + if (filtered === null) { return; } + this.sendMessage({ + token: this.token, + catcherType: Catcher.type, + payload: filtered, + } as CatcherMessage); + } catch (e) { log('Unable to send error. Seems like it is Hawk internal bug. Please, report it here: https://github.com/codex-team/hawk.javascript/issues/new', 'warn', e); } } /** - * Sends formatted HawkEvent to the Collector + * Builds base event payload with basic fields (title, type, backtrace, user, context, release). * - * @param errorFormatted - formatted error to send + * @param error - caught error or string reason + * @param context - per-call context to merge with instance-level context + * @returns base payload with core data */ - private sendErrorFormatted(errorFormatted: CatcherMessage): void { - this.transport.send(errorFormatted) - .catch((sendingError) => { - log('WebSocket sending error', 'error', sendingError); - }); - } - - /** - * Formats the event - * - * @param error - error to format - * @param context - any additional data passed by user - */ - private async prepareErrorFormatted(error: Error | string, context?: EventContext): Promise> { - let payload: HawkJavaScriptEvent = { + private async buildBasePayload( + error: Error | string, + context?: EventContext + ): Promise> { + return { title: this.getTitle(error), type: this.getType(error), release: this.getRelease(), - breadcrumbs: this.getBreadcrumbsForEvent(), context: this.getContext(context), user: this.getUser(), - addons: this.getAddons(error), backtrace: await this.getBacktrace(error), catcherVersion: this.version, }; + } - /** - * Filter sensitive data - */ - if (typeof this.beforeSend === 'function') { - let eventPayloadClone: HawkJavaScriptEvent; + /** + * Clones {@link payload} and applies user-supplied {@link beforeSend} hook against it. + * + * @param payload - processed event message payload + * @returns possibly modified payload, or null if the event should be dropped + */ + private applyBeforeSendHook( + payload: CatcherMessagePayload + ): CatcherMessagePayload | null { + if (typeof this.beforeSend !== 'function') { + return payload; + } - try { - eventPayloadClone = structuredClone(payload); - } catch { - /** - * structuredClone may fail on non-cloneable values (functions, DOM nodes, etc.) - * Fall back to passing the original — hook may mutate it, but at least reporting won't crash - */ - eventPayloadClone = payload; - } + let clone: CatcherMessagePayload; - const result = this.beforeSend(eventPayloadClone); + try { + clone = structuredClone(payload); + } catch { + // structuredClone may fail on non-cloneable values (functions, DOM nodes, etc.) + // Fall back to passing the original — hook may mutate it, but at least reporting won't crash + clone = payload; + } - /** - * false → drop event - */ - if (result === false) { - throw new EventRejectedError('Event rejected by beforeSend method.'); - } + const result = this.beforeSend(clone); - /** - * Valid event payload → use it instead of original - */ - if (isValidEventPayload(result)) { - payload = result as HawkJavaScriptEvent; - } else { - /** - * 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' - ); - } + // false → drop event + if (result === false) { + return null; } - return { - token: this.token, - catcherType: Catcher.type, - payload, - }; + // Valid event payload → use it instead of original + if (isValidEventPayload(result)) { + return result as CatcherMessagePayload; + } + + // 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' + ); + + return payload; + } + + /** + * Dispatches assembled message over configured transport. + * + * @param message - fully assembled catcher message ready to send + */ + private sendMessage(message: CatcherMessage): void { + this.transport.send(message) + .catch((e) => log('Transport sending error', 'error', e)); } /** @@ -575,39 +607,6 @@ export default class Catcher { return this.userManager.getUser(); } - /** - * Get breadcrumbs for event payload - */ - private getBreadcrumbsForEvent(): HawkJavaScriptEvent['breadcrumbs'] { - const breadcrumbs = this.breadcrumbStore?.get(); - - return breadcrumbs && breadcrumbs.length > 0 ? breadcrumbs : undefined; - } - - /** - * Get parameters - */ - private getGetParams(): Json | null { - const searchString = window.location.search.substr(1); - - if (!searchString) { - return null; - } - - /** - * Create object from get-params string - */ - const pairs = searchString.split('&'); - - return pairs.reduce((accumulator, pair) => { - const [key, value] = pair.split('='); - - accumulator[key] = value; - - return accumulator; - }, {}); - } - /** * Return parsed backtrace information * @@ -632,70 +631,4 @@ export default class Catcher { return undefined; } } - - /** - * Return some details - * - * @param {Error|string} error — caught error - */ - private getAddons(error: Error | string): HawkJavaScriptEvent['addons'] { - const { innerWidth, innerHeight } = window; - const userAgent = window.navigator.userAgent; - const location = window.location.href; - const getParams = this.getGetParams(); - const consoleLogs = this.consoleTracking && this.consoleCatcher?.getConsoleLogStack(); - - const addons: JavaScriptAddons = { - window: { - innerWidth, - innerHeight, - }, - userAgent, - url: location, - }; - - if (getParams) { - addons.get = getParams; - } - - if (this.debug) { - addons.RAW_EVENT_DATA = this.getRawData(error); - } - - if (consoleLogs && consoleLogs.length > 0) { - addons.consoleOutput = consoleLogs; - } - - return addons; - } - - /** - * Compose raw data object - * - * @param {Error|string} error — caught error - */ - private getRawData(error: Error | string): Json | undefined { - if (!(error instanceof Error)) { - return; - } - - const stack = error.stack !== null && error.stack !== undefined ? error.stack : ''; - - return { - name: error.name, - message: error.message, - stack, - }; - } - - /** - * Extend addons object with addons spoiled by integration - * This method mutates original event - * - * @param errorFormatted - Hawk event prepared for sending - * @param integrationAddons - extra addons - */ - private appendIntegrationAddons(errorFormatted: CatcherMessage, integrationAddons: JavaScriptCatcherIntegrations): void { - Object.assign(errorFormatted.payload.addons, integrationAddons); - } } diff --git a/packages/javascript/src/messages/browser-message-processor.ts b/packages/javascript/src/messages/browser-message-processor.ts new file mode 100644 index 00000000..245144a4 --- /dev/null +++ b/packages/javascript/src/messages/browser-message-processor.ts @@ -0,0 +1,62 @@ +import type { MessageProcessor } from '@hawk.so/core'; +import type { CatcherMessagePayload } from '@hawk.so/types'; +import { type Json } from '@hawk.so/types'; + +/** + * Enriches payload with browser environment data: + * viewport dimensions, user agent, current URL, and GET parameters. + */ +export class BrowserMessageProcessor implements MessageProcessor<'errors/javascript'> { + /** + * Reads current browser state (window dimensions, user agent, URL, GET params) and + * merges it into `payload.addons`, preserving any addons already set. + * + * @param payload - event message payload to enrich + * @returns modified payload with browser addons merged in + */ + public apply( + payload: CatcherMessagePayload<'errors/javascript'> + ): CatcherMessagePayload<'errors/javascript'> | null { + const { innerWidth, innerHeight } = window; + const userAgent = window.navigator.userAgent; + const url = window.location.href; + const get = this.parseGetParams(); + + payload.addons = { + ...(payload.addons ?? {}), + window: { + innerWidth, + innerHeight, + }, + userAgent, + url, + get, + }; + + return payload; + } + + /** + * Parses `window.location.search` into plain key-value object. + * + * @returns parsed GET parameters, or `undefined` if URL has no query string + */ + private parseGetParams(): Json | undefined { + const searchString = window.location.search.substring(1); + + if (!searchString) { + return undefined; + } + + // Create object from get-params string + const pairs = searchString.split('&'); + + return pairs.reduce((accumulator, pair) => { + const [key, value] = pair.split('='); + + accumulator[key] = value; + + return accumulator; + }, {}); + } +} diff --git a/packages/javascript/src/messages/console-catcher-message-processor.ts b/packages/javascript/src/messages/console-catcher-message-processor.ts new file mode 100644 index 00000000..bf610b05 --- /dev/null +++ b/packages/javascript/src/messages/console-catcher-message-processor.ts @@ -0,0 +1,43 @@ +import type { MessageProcessor } from '@hawk.so/core'; +import type { CatcherMessagePayload } from '@hawk.so/types'; +import type { ConsoleCatcher } from '../addons/consoleCatcher'; + +/** + * Attaches captured console output to payload addons. + */ +export class ConsoleCatcherMessageProcessor implements MessageProcessor<'errors/javascript'> { + private readonly consoleCatcher: ConsoleCatcher; + + /** + * Stores catcher reference and starts console interception. + * + * @param consoleCatcher - console catcher instance to read logs from + */ + constructor(consoleCatcher: ConsoleCatcher) { + this.consoleCatcher = consoleCatcher; + this.consoleCatcher.init(); + } + + /** + * Attaches current console log stack to `payload.addons.consoleOutput`. + * Skips if `payload.addons` is absent or log stack is empty. + * + * @param payload - event message payload to enrich + * @returns modified payload with console logs attached, or original payload unchanged + */ + public apply( + payload: CatcherMessagePayload<'errors/javascript'> + ): CatcherMessagePayload<'errors/javascript'> | null { + if (!payload.addons) { + return payload; + } + + const logs = this.consoleCatcher?.getConsoleLogStack(); + + if (logs && logs.length > 0) { + payload.addons.consoleOutput = logs; + } + + return payload; + } +} diff --git a/packages/javascript/src/messages/debug-message-processor.ts b/packages/javascript/src/messages/debug-message-processor.ts new file mode 100644 index 00000000..ff3f3145 --- /dev/null +++ b/packages/javascript/src/messages/debug-message-processor.ts @@ -0,0 +1,32 @@ +import type { MessageHint, MessageProcessor } from '@hawk.so/core'; +import type { CatcherMessagePayload } from '@hawk.so/types'; + +/** + * Appends `RAW_EVENT_DATA` to the event addons for debug purposes. + */ +export class DebugMessageProcessor implements MessageProcessor<'errors/javascript'> { + /** + * Writes name, message, and stack from `hint.error` into `payload.addons.RAW_EVENT_DATA`. + * Skips if hint error is not Error instance or `payload.addons` is absent. + * + * @param payload - event message payload to enrich + * @param hint - hint carrying original caught error + * @returns modified payload with RAW_EVENT_DATA set, or original payload unchanged + */ + public apply( + payload: CatcherMessagePayload<'errors/javascript'>, + hint?: MessageHint + ): CatcherMessagePayload<'errors/javascript'> | null { + if (!(hint?.error instanceof Error) || !payload.addons) { + return payload; + } + + payload.addons.RAW_EVENT_DATA = { + name: hint.error.name, + message: hint.error.message, + stack: hint.error.stack ?? '', + }; + + return payload; + } +} diff --git a/packages/javascript/tests/messages/breadcrumbs-message-processor.test.ts b/packages/javascript/tests/messages/breadcrumbs-message-processor.test.ts new file mode 100644 index 00000000..19af2d74 --- /dev/null +++ b/packages/javascript/tests/messages/breadcrumbs-message-processor.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { BreadcrumbsMessageProcessor } from '@hawk.so/core'; +import { makePayload } from './message-processor.helpers'; + +describe('BreadcrumbsMessageProcessor', () => { + const processor = new BreadcrumbsMessageProcessor(); + + it('should attach breadcrumbs from hint to payload', () => { + const breadcrumbs = [{ message: 'click', timestamp: 1 }]; + + const result = processor.apply(makePayload(), { breadcrumbs }); + + expect(result?.breadcrumbs).toEqual(breadcrumbs); + }); + + it('should not set payload breadcrumbs when hint has empty array', () => { + const result = processor.apply(makePayload(), { breadcrumbs: [] }); + + expect(result?.breadcrumbs).toBeUndefined(); + }); + + it('should not set payload breadcrumbs when hint is absent', () => { + const result = processor.apply(makePayload()); + + expect(result?.breadcrumbs).toBeUndefined(); + }); +}); diff --git a/packages/javascript/tests/messages/browser-message-processor.test.ts b/packages/javascript/tests/messages/browser-message-processor.test.ts new file mode 100644 index 00000000..25f46c4d --- /dev/null +++ b/packages/javascript/tests/messages/browser-message-processor.test.ts @@ -0,0 +1,48 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BrowserMessageProcessor } from '../../src/messages/browser-message-processor'; +import { makePayload } from './message-processor.helpers'; + +describe('BrowserMessageProcessor', () => { + beforeEach(() => { + vi.stubGlobal('location', { href: 'http://localhost/', search: '' }); + vi.stubGlobal('navigator', { userAgent: 'test-agent' }); + vi.stubGlobal('innerWidth', 1280); + vi.stubGlobal('innerHeight', 720); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('should set window dimensions, userAgent, and url', () => { + const result = new BrowserMessageProcessor().apply(makePayload()); + + expect(result?.addons).toMatchObject({ + window: { innerWidth: 1280, innerHeight: 720 }, + userAgent: 'test-agent', + url: 'http://localhost/', + }); + }); + + it('should parse GET parameters from the URL', () => { + vi.stubGlobal('location', { href: 'http://localhost/?foo=bar&baz=qux', search: '?foo=bar&baz=qux' }); + + const result = new BrowserMessageProcessor().apply(makePayload()); + + expect(result?.addons?.get).toEqual({ foo: 'bar', baz: 'qux' }); + }); + + it('should omit get when URL has no query string', () => { + const result = new BrowserMessageProcessor().apply(makePayload()); + + expect(result?.addons?.get).toBeUndefined(); + }); + + it('should merge with existing payload addons', () => { + const payload = makePayload({ addons: { consoleOutput: [{ message: 'log' }] } as any }); + const result = new BrowserMessageProcessor().apply(payload); + + expect(result?.addons?.consoleOutput).toBeDefined(); + expect(result?.addons?.userAgent).toBeDefined(); + }); +}); diff --git a/packages/javascript/tests/messages/console-catcher-message-processor.test.ts b/packages/javascript/tests/messages/console-catcher-message-processor.test.ts new file mode 100644 index 00000000..9651eec8 --- /dev/null +++ b/packages/javascript/tests/messages/console-catcher-message-processor.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ConsoleCatcherMessageProcessor } from '../../src/messages/console-catcher-message-processor'; +import { makePayload } from './message-processor.helpers'; + +const makeConsoleCatcher = (logs: any[]) => ({ + init: vi.fn(), + getConsoleLogStack: vi.fn().mockReturnValue(logs), + addErrorEvent: vi.fn(), +}); + +describe('ConsoleCatcherMessageProcessor', () => { + it('should attach console logs to payload addons', () => { + const logs = [{ message: 'hello', type: 'log' }]; + const processor = new ConsoleCatcherMessageProcessor(makeConsoleCatcher(logs) as any); + + const result = processor.apply(makePayload({ addons: {} as any })); + + expect(result?.addons?.consoleOutput).toEqual(logs); + }); + + it('should not add consoleOutput when log stack is empty', () => { + const processor = new ConsoleCatcherMessageProcessor(makeConsoleCatcher([]) as any); + + const result = processor.apply(makePayload({ addons: {} as any })); + + expect(result?.addons?.consoleOutput).toBeUndefined(); + }); + + it('should return payload unchanged when payload has no addons', () => { + const processor = new ConsoleCatcherMessageProcessor(makeConsoleCatcher([{ message: 'x' }]) as any); + const payload = makePayload(); + + const result = processor.apply(payload); + + expect(result).toBe(payload); + expect(result?.addons).toBeUndefined(); + }); +}); diff --git a/packages/javascript/tests/messages/debug-message-processor.test.ts b/packages/javascript/tests/messages/debug-message-processor.test.ts new file mode 100644 index 00000000..e8720c36 --- /dev/null +++ b/packages/javascript/tests/messages/debug-message-processor.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { DebugMessageProcessor } from '../../src/messages/debug-message-processor'; +import { makePayload } from './message-processor.helpers'; + +describe('DebugMessageProcessor', () => { + const processor = new DebugMessageProcessor(); + + it('should add RAW_EVENT_DATA when hint.error is an Error instance', () => { + const error = new TypeError('boom'); + + const result = processor.apply(makePayload({ addons: {} as any }), { error }); + + expect(result?.addons?.RAW_EVENT_DATA).toMatchObject({ + name: 'TypeError', + message: 'boom', + stack: expect.any(String), + }); + }); + + it('should not add RAW_EVENT_DATA when hint.error is a string', () => { + const result = processor.apply(makePayload({ addons: {} as any }), { error: 'string reason' }); + + expect(result?.addons?.RAW_EVENT_DATA).toBeUndefined(); + }); + + it('should not add RAW_EVENT_DATA when hint is absent', () => { + const result = processor.apply(makePayload({ addons: {} as any })); + + expect(result?.addons?.RAW_EVENT_DATA).toBeUndefined(); + }); + + it('should return payload unchanged when payload has no addons', () => { + const payload = makePayload(); + + const result = processor.apply(payload, { error: new Error('x') }); + + expect(result).toBe(payload); + }); +}); diff --git a/packages/javascript/tests/messages/message-processor.helpers.ts b/packages/javascript/tests/messages/message-processor.helpers.ts new file mode 100644 index 00000000..2758c690 --- /dev/null +++ b/packages/javascript/tests/messages/message-processor.helpers.ts @@ -0,0 +1,7 @@ +import type { CatcherMessagePayload } from '@hawk.so/types'; + +export function makePayload( + overrides: Partial> = {} +): CatcherMessagePayload<'errors/javascript'> { + return { title: 'Test error', catcherVersion: '0.0.0', ...overrides }; +} \ No newline at end of file