From 4772bd24146cdf7f086eaaccb619e36bd1965d18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:20:19 +0000 Subject: [PATCH 1/5] Initial plan From adc6ab8b7110bb5b372818323b9e7352b57d42eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:25:35 +0000 Subject: [PATCH 2/5] Add error addons extraction to capture additional error fields Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- src/index.ts | 2 ++ src/modules/event.ts | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b09bd89..25fe289 100644 --- a/src/index.ts +++ b/src/index.ts @@ -227,10 +227,12 @@ class Catcher { */ private formatAndSend(err: Error, context?: EventContext, user?: AffectedUser): void { const eventPayload = new EventPayload(err); + const addons = eventPayload.getAddons(); let payload: EventData = { title: eventPayload.getTitle(), type: eventPayload.getType(), backtrace: eventPayload.getBacktrace(), + addons: Object.keys(addons).length > 0 ? addons : undefined, user: this.getUser(user), context: this.getContext(context), release: this.release, diff --git a/src/modules/event.ts b/src/modules/event.ts index 2970685..354f309 100644 --- a/src/modules/event.ts +++ b/src/modules/event.ts @@ -1,4 +1,4 @@ -import type { BacktraceFrame } from '@hawk.so/types'; +import type { BacktraceFrame, NodeJSAddons } from '@hawk.so/types'; import BacktraceHelper from './backtrace.js'; /** @@ -56,4 +56,44 @@ export default class EventPayload { return backtrace.getBacktrace(); } + + /** + * Extract additional error information for NodeJS addons + * Includes error codes, system error fields, and any custom properties + */ + public getAddons(): NodeJSAddons { + if (this.error === undefined) { + return {}; + } + + const addons: NodeJSAddons = {}; + + // Get all own properties of the error (including non-enumerable ones) + const errorProps = Object.getOwnPropertyNames(this.error); + + // Standard properties to skip (already captured elsewhere) + const skipProps = new Set(['name', 'message', 'stack']); + + // Extract all additional properties from the error + for (const prop of errorProps) { + if (skipProps.has(prop)) { + continue; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (this.error as any)[prop]; + + // Only include serializable values + if (value !== undefined && value !== null) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (addons as any)[prop] = value; + } + } catch { + // Skip properties that can't be accessed + } + } + + return addons; + } } From 07f761235f149bab0fe1e100c0e60153e2339622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:27:19 +0000 Subject: [PATCH 3/5] Add comprehensive test for error addons feature Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- playground/src/test-addons.ts | 134 ++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 playground/src/test-addons.ts diff --git a/playground/src/test-addons.ts b/playground/src/test-addons.ts new file mode 100644 index 0000000..bf5e212 --- /dev/null +++ b/playground/src/test-addons.ts @@ -0,0 +1,134 @@ +/** + * Test script for @hawk.so/nodejs - Testing error addons + * This script tests various error types to ensure addons are properly captured + */ +import HawkCatcher from '@hawk.so/nodejs'; +import * as fs from 'fs'; + +/** + * Initialize Hawk catcher + * Replace with your actual integration token + */ +const HAWK_TOKEN = process.env.HAWK_TOKEN || 'eyJpbnRlZ3JhdGlvbklkIjoiNWIwZjBmYmUtNTM2OS00ODM0LWEwMjctNTZkMTM1YmU1OGU3Iiwic2VjcmV0IjoiYWY4ZjY1OTQtNzExOS00MWVmLWI4ZTAtMTcyMDYwZjBmODc2In0='; + +console.log('Initializing Hawk Catcher...'); +HawkCatcher.init({ + token: HAWK_TOKEN, + beforeSend: (event) => { + console.log('\n--- Event being sent to Hawk ---'); + console.log('Title:', event.title); + console.log('Type:', event.type); + console.log('Addons:', JSON.stringify(event.addons, null, 2)); + console.log('---\n'); + return event; + } +}); +console.log('Hawk Catcher initialized successfully'); + +/** + * Test 1: Standard Error (no addons expected) + */ +function testStandardError(): void { + console.log('\n=== Test 1: Standard Error ==='); + try { + throw new Error('Standard error - no additional fields'); + } catch (error) { + if (error instanceof Error) { + console.log('Sending standard error...'); + HawkCatcher.send(error, { test: 'standard-error' }); + } + } +} + +/** + * Test 2: Error with Node.js error code + */ +function testErrorWithCode(): void { + console.log('\n=== Test 2: Error with Node.js Error Code ==='); + try { + const error = new Error('Error with code'); + // @ts-ignore - adding code property + error.code = 'ERR_INVALID_ARG_TYPE'; + throw error; + } catch (error) { + if (error instanceof Error) { + console.log('Sending error with code...'); + HawkCatcher.send(error, { test: 'error-with-code' }); + } + } +} + +/** + * Test 3: SystemError from file system operation + */ +function testSystemError(): void { + console.log('\n=== Test 3: SystemError (File System) ==='); + try { + fs.readFileSync('/nonexistent/path/to/file.txt'); + } catch (error) { + if (error instanceof Error) { + console.log('Sending SystemError...'); + HawkCatcher.send(error, { test: 'system-error' }); + } + } +} + +/** + * Test 4: Custom error with multiple additional properties + */ +function testCustomError(): void { + console.log('\n=== Test 4: Custom Error with Multiple Properties ==='); + try { + const error = new Error('Custom error with additional fields'); + // @ts-ignore - adding custom properties + error.code = 'CUSTOM_ERROR_CODE'; + // @ts-ignore + error.statusCode = 500; + // @ts-ignore + error.details = { + userId: 12345, + action: 'update_profile', + timestamp: new Date().toISOString() + }; + // @ts-ignore + error.retryable = true; + throw error; + } catch (error) { + if (error instanceof Error) { + console.log('Sending custom error...'); + HawkCatcher.send(error, { test: 'custom-error' }); + } + } +} + +/** + * Test 5: TypeError + */ +function testTypeError(): void { + console.log('\n=== Test 5: TypeError ==='); + try { + // @ts-ignore - intentional type error + null.someProperty(); + } catch (error) { + if (error instanceof Error) { + console.log('Sending TypeError...'); + HawkCatcher.send(error, { test: 'type-error' }); + } + } +} + +/** + * Run all tests + */ +console.log('\n=== Hawk Playground - Error Addons Test Suite ===\n'); + +testStandardError(); +setTimeout(() => testErrorWithCode(), 100); +setTimeout(() => testSystemError(), 200); +setTimeout(() => testCustomError(), 300); +setTimeout(() => testTypeError(), 400); + +setTimeout(() => { + console.log('\n=== All tests completed ==='); + console.log('Check the console output above to see the addons for each error type.'); +}, 500); From 68cb0e0e8ec9a5636e9c08efe1a399477b6cea84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:28:59 +0000 Subject: [PATCH 4/5] Add serialization validation to prevent non-serializable values in addons Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- src/modules/event.ts | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/modules/event.ts b/src/modules/event.ts index 354f309..43296da 100644 --- a/src/modules/event.ts +++ b/src/modules/event.ts @@ -86,8 +86,11 @@ export default class EventPayload { // Only include serializable values if (value !== undefined && value !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (addons as any)[prop] = value; + // Check if value is JSON-serializable + if (this.isSerializable(value)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (addons as any)[prop] = value; + } } } catch { // Skip properties that can't be accessed @@ -96,4 +99,40 @@ export default class EventPayload { return addons; } + + /** + * Check if a value is JSON-serializable + * @param value - value to check + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private isSerializable(value: any): boolean { + // Primitives are always serializable + if (value === null || value === undefined) { + return false; + } + + const type = typeof value; + + if (type === 'boolean' || type === 'number' || type === 'string') { + return true; + } + + // Functions and symbols are not serializable + if (type === 'function' || type === 'symbol') { + return false; + } + + // For objects and arrays, try to serialize and catch any errors + if (type === 'object') { + try { + JSON.stringify(value); + return true; + } catch { + // Circular references or other serialization issues + return false; + } + } + + return false; + } } From 5a9da2a6dccad442bf55f275ed034dfde47da950 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 00:30:47 +0000 Subject: [PATCH 5/5] Simplify null check and remove redundant conditions Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- src/modules/event.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/modules/event.ts b/src/modules/event.ts index 43296da..25151c7 100644 --- a/src/modules/event.ts +++ b/src/modules/event.ts @@ -84,8 +84,8 @@ export default class EventPayload { // eslint-disable-next-line @typescript-eslint/no-explicit-any const value = (this.error as any)[prop]; - // Only include serializable values - if (value !== undefined && value !== null) { + // Only include serializable values (checks for both undefined and null) + if (value != null) { // Check if value is JSON-serializable if (this.isSerializable(value)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -106,13 +106,9 @@ export default class EventPayload { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any private isSerializable(value: any): boolean { - // Primitives are always serializable - if (value === null || value === undefined) { - return false; - } - const type = typeof value; + // Primitives are serializable if (type === 'boolean' || type === 'number' || type === 'string') { return true; }