From 1c8810d48f06882cc88f5b445033d33e0d4a6dbb Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 5 Aug 2025 21:45:47 +0200 Subject: [PATCH 1/7] feat(node) Capture `SystemError` context and remove paths from message --- .../suites/system-error/basic-pii.mjs | 11 +++ .../suites/system-error/basic.mjs | 10 +++ .../suites/system-error/test.ts | 59 ++++++++++++++++ packages/node-core/src/index.ts | 1 + .../node-core/src/integrations/systemError.ts | 69 +++++++++++++++++++ packages/node-core/src/sdk/index.ts | 2 + 6 files changed, 152 insertions(+) create mode 100644 dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/system-error/basic.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/system-error/test.ts create mode 100644 packages/node-core/src/integrations/systemError.ts diff --git a/dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs b/dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs new file mode 100644 index 000000000000..1dd8c40c6ccf --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/system-error/basic-pii.mjs @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { readFileSync } from 'fs'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + sendDefaultPii: true, +}); + +readFileSync('non-existent-file.txt'); diff --git a/dev-packages/node-core-integration-tests/suites/system-error/basic.mjs b/dev-packages/node-core-integration-tests/suites/system-error/basic.mjs new file mode 100644 index 000000000000..5321dd062fa2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/system-error/basic.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { readFileSync } from 'fs'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, +}); + +readFileSync('non-existent-file.txt'); diff --git a/dev-packages/node-core-integration-tests/suites/system-error/test.ts b/dev-packages/node-core-integration-tests/suites/system-error/test.ts new file mode 100644 index 000000000000..05e225562772 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/system-error/test.ts @@ -0,0 +1,59 @@ +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('SystemError integration', () => { + test('sendDefaultPii: false', async () => { + await createRunner(__dirname, 'basic.mjs') + .expect({ + event: { + contexts: { + node_system_error: { + errno: -2, + code: 'ENOENT', + syscall: 'open', + }, + }, + exception: { + values: [ + { + type: 'Error', + value: 'ENOENT: no such file or directory, open ', + } + ] + }, + }, + }) + .start() + .completed(); + }); + + test('sendDefaultPii: true', async () => { + await createRunner(__dirname, 'basic-pii.mjs') + .expect({ + event: { + contexts: { + node_system_error: { + errno: -2, + code: 'ENOENT', + syscall: 'open', + path: 'non-existent-file.txt' + }, + }, + exception: { + values: [ + { + type: 'Error', + value: 'ENOENT: no such file or directory, open ', + } + ] + }, + }, + }) + .start() + .completed(); + }); +}); diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index 6d478ea912e9..cf581bd63b66 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -21,6 +21,7 @@ export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejec export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr'; export { spotlightIntegration } from './integrations/spotlight'; +export { systemErrorIntegration } from './integrations/systemError'; export { childProcessIntegration } from './integrations/childProcess'; export { createSentryWinstonTransport } from './integrations/winston'; diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts new file mode 100644 index 000000000000..a46746d2b2b7 --- /dev/null +++ b/packages/node-core/src/integrations/systemError.ts @@ -0,0 +1,69 @@ +import { defineIntegration } from '@sentry/core'; +import { getSystemErrorMap } from 'util'; + +const INTEGRATION_NAME = 'NodeSystemError'; + +type SystemErrorContext = { + dest?: string; // If present, the file path destination when reporting a file system error + errno: number; // The system-provided error number + path?: string; // If present, the file path when reporting a file system error +}; + +type SystemError = Error & SystemErrorContext; + +function isSystemError(error: unknown): error is SystemError { + if (!(error instanceof Error)) { + return false; + } + + if (!('errno' in error) || typeof error.errno !== 'number') { + return false; + } + + // Appears this is the recommended way to check for Node.js SystemError + // https://github.com/nodejs/node/issues/46869 + return getSystemErrorMap().has(error.errno); +} + +/** + * Captures context for Node.js SystemError errors. + */ +export const systemErrorIntegration = defineIntegration(() => { + return { + name: INTEGRATION_NAME, + processEvent: (event, hint, client) => { + if (!isSystemError(hint.originalException)) { + return event; + } + + const error = hint.originalException; + + const errorContext: SystemErrorContext = { + ...error, + }; + + if (!client.getOptions().sendDefaultPii) { + delete errorContext.path; + delete errorContext.dest; + } + + event.contexts = { + ...event.contexts, + node_system_error: errorContext, + }; + + for (const exception of event.exception?.values || []) { + if (exception.value) { + if (error.path && exception.value.includes(error.path)) { + exception.value = exception.value.replace(`'${error.path}'`, ''); + } + if (error.dest && exception.value.includes(error.dest)) { + exception.value = exception.value.replace(`'${error.dest}'`, ''); + } + } + } + + return event; + }, + }; +}); diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index eb2807193b9b..e5b12166d962 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -32,6 +32,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; import { processSessionIntegration } from '../integrations/processSession'; import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; +import { systemErrorIntegration } from '../integrations/systemError'; import { makeNodeTransport } from '../transports'; import type { NodeClientOptions, NodeOptions } from '../types'; import { isCjs } from '../utils/commonjs'; @@ -52,6 +53,7 @@ export function getDefaultIntegrations(): Integration[] { functionToStringIntegration(), linkedErrorsIntegration(), requestDataIntegration(), + systemErrorIntegration(), // Native Wrappers consoleIntegration(), httpIntegration(), From 8cc1e815986193d5bfe6da2dccba60d8ba80e3db Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 5 Aug 2025 21:47:07 +0200 Subject: [PATCH 2/7] Trim --- .../suites/system-error/test.ts | 14 +++++++------- packages/node-core/src/integrations/systemError.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dev-packages/node-core-integration-tests/suites/system-error/test.ts b/dev-packages/node-core-integration-tests/suites/system-error/test.ts index 05e225562772..1725bd11a0f6 100644 --- a/dev-packages/node-core-integration-tests/suites/system-error/test.ts +++ b/dev-packages/node-core-integration-tests/suites/system-error/test.ts @@ -21,9 +21,9 @@ describe('SystemError integration', () => { values: [ { type: 'Error', - value: 'ENOENT: no such file or directory, open ', - } - ] + value: 'ENOENT: no such file or directory, open', + }, + ], }, }, }) @@ -40,16 +40,16 @@ describe('SystemError integration', () => { errno: -2, code: 'ENOENT', syscall: 'open', - path: 'non-existent-file.txt' + path: 'non-existent-file.txt', }, }, exception: { values: [ { type: 'Error', - value: 'ENOENT: no such file or directory, open ', - } - ] + value: 'ENOENT: no such file or directory, open', + }, + ], }, }, }) diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts index a46746d2b2b7..80b907af8b8e 100644 --- a/packages/node-core/src/integrations/systemError.ts +++ b/packages/node-core/src/integrations/systemError.ts @@ -55,10 +55,10 @@ export const systemErrorIntegration = defineIntegration(() => { for (const exception of event.exception?.values || []) { if (exception.value) { if (error.path && exception.value.includes(error.path)) { - exception.value = exception.value.replace(`'${error.path}'`, ''); + exception.value = exception.value.replace(`'${error.path}'`, '').trim(); } if (error.dest && exception.value.includes(error.dest)) { - exception.value = exception.value.replace(`'${error.dest}'`, ''); + exception.value = exception.value.replace(`'${error.dest}'`, '').trim(); } } } From 6801593bdae20e94d5315d3b3744bec7012d96b2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 7 Aug 2025 11:13:57 +0100 Subject: [PATCH 3/7] PR review --- packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node-core/src/integrations/systemError.ts | 13 ++++++++++--- packages/node/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index a9e81aee7db5..3b2f589f7fc2 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -124,6 +124,7 @@ export { startSession, startSpan, startSpanManual, + systemErrorIntegration, tediousIntegration, trpcMiddleware, updateSpanName, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index b99c481fd1d3..8cbcd31c50a5 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -114,6 +114,7 @@ export { spanToJSON, spanToTraceHeader, spanToBaggageHeader, + systemErrorIntegration, trpcMiddleware, updateSpanName, supabaseIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 8339e95c77a3..26ed56f031d8 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -115,6 +115,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, + systemErrorIntegration, instrumentSupabaseClient, zodErrorsIntegration, profiler, diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts index 80b907af8b8e..69125827cc22 100644 --- a/packages/node-core/src/integrations/systemError.ts +++ b/packages/node-core/src/integrations/systemError.ts @@ -1,5 +1,5 @@ +import { getSystemErrorMap } from 'node:util'; import { defineIntegration } from '@sentry/core'; -import { getSystemErrorMap } from 'util'; const INTEGRATION_NAME = 'NodeSystemError'; @@ -25,10 +25,17 @@ function isSystemError(error: unknown): error is SystemError { return getSystemErrorMap().has(error.errno); } +type Options = { + /** + * If true, includes the `path` and `dest` properties in the error context. + */ + includePaths?: boolean; +} + /** * Captures context for Node.js SystemError errors. */ -export const systemErrorIntegration = defineIntegration(() => { +export const systemErrorIntegration = defineIntegration((options: Options = {}) => { return { name: INTEGRATION_NAME, processEvent: (event, hint, client) => { @@ -42,7 +49,7 @@ export const systemErrorIntegration = defineIntegration(() => { ...error, }; - if (!client.getOptions().sendDefaultPii) { + if (!client.getOptions().sendDefaultPii && options.includePaths !== true) { delete errorContext.path; delete errorContext.dest; } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index bba0f98bc75e..da97071bdd32 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -165,6 +165,7 @@ export { childProcessIntegration, createSentryWinstonTransport, SentryContextManager, + systemErrorIntegration, generateInstrumentOnce, getSentryRelease, defaultStackParser, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 043bad823bb3..56400dcc5423 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -113,6 +113,7 @@ export { startSession, startSpan, startSpanManual, + systemErrorIntegration, tediousIntegration, trpcMiddleware, updateSpanName, From c11eb51b820684301485dfebb39b3f3dd040b507 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 7 Aug 2025 11:14:33 +0100 Subject: [PATCH 4/7] lint --- packages/node-core/src/integrations/systemError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts index 69125827cc22..796ce9d4bb6a 100644 --- a/packages/node-core/src/integrations/systemError.ts +++ b/packages/node-core/src/integrations/systemError.ts @@ -30,7 +30,7 @@ type Options = { * If true, includes the `path` and `dest` properties in the error context. */ includePaths?: boolean; -} +}; /** * Captures context for Node.js SystemError errors. From 5a93302e4b2871c13b3250804bd2357c908a755f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 11 Aug 2025 10:14:39 +0100 Subject: [PATCH 5/7] Fix Bun --- packages/node-core/src/integrations/systemError.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node-core/src/integrations/systemError.ts b/packages/node-core/src/integrations/systemError.ts index 796ce9d4bb6a..f1fd3f4db0dc 100644 --- a/packages/node-core/src/integrations/systemError.ts +++ b/packages/node-core/src/integrations/systemError.ts @@ -1,4 +1,4 @@ -import { getSystemErrorMap } from 'node:util'; +import * as util from 'node:util'; import { defineIntegration } from '@sentry/core'; const INTEGRATION_NAME = 'NodeSystemError'; @@ -22,7 +22,7 @@ function isSystemError(error: unknown): error is SystemError { // Appears this is the recommended way to check for Node.js SystemError // https://github.com/nodejs/node/issues/46869 - return getSystemErrorMap().has(error.errno); + return util.getSystemErrorMap().has(error.errno); } type Options = { From 0ade4a6dafe20fcbaa3a2a73c97a968fa94ce855 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 12 Aug 2025 11:27:25 +0100 Subject: [PATCH 6/7] bump node size limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index d53eaae56712..dd65a987d506 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -233,7 +233,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '147 KB', + limit: '148 KB', }, { name: '@sentry/node - without tracing', From d12cd39fe202077e3a470c3038dfbd8ceab97ac4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 12 Aug 2025 12:43:29 +0100 Subject: [PATCH 7/7] Dont use `systemErrorIntegration` for Bun --- .../node-exports-test-app/scripts/consistentExports.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index f355654bf6a2..596109c0a596 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -52,6 +52,7 @@ const DEPENDENTS: Dependent[] = [ 'NodeClient', 'NODE_VERSION', 'childProcessIntegration', + 'systemErrorIntegration', ], }, {