From 703e4a8acd7e02279ad48a8efbb4d13226b0c926 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 12 Aug 2025 15:55:13 +0100 Subject: [PATCH] feat(v9/node): Capture `SystemError` context and remove paths from message --- .../scripts/consistentExports.ts | 1 + .../suites/system-error/basic-pii.mjs | 11 +++ .../suites/system-error/basic.mjs | 10 +++ .../suites/system-error/test.ts | 59 ++++++++++++++ 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/index.ts | 1 + .../node-core/src/integrations/systemError.ts | 76 +++++++++++++++++++ packages/node-core/src/sdk/index.ts | 2 + packages/node/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 12 files changed, 165 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/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', ], }, { 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..1725bd11a0f6 --- /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/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 0b92c8a4a6f8..5b27c1fd5f8d 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -123,6 +123,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 7cf8e17f0dd7..5970ccdb36f2 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -113,6 +113,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 ba6d9640a8b5..bc0c3641b010 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -114,6 +114,7 @@ export { trpcMiddleware, updateSpanName, supabaseIntegration, + systemErrorIntegration, instrumentSupabaseClient, zodErrorsIntegration, profiler, 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..f1fd3f4db0dc --- /dev/null +++ b/packages/node-core/src/integrations/systemError.ts @@ -0,0 +1,76 @@ +import * as util from 'node:util'; +import { defineIntegration } from '@sentry/core'; + +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 util.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((options: Options = {}) => { + 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 && options.includePaths !== true) { + 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}'`, '').trim(); + } + if (error.dest && exception.value.includes(error.dest)) { + exception.value = exception.value.replace(`'${error.dest}'`, '').trim(); + } + } + } + + return event; + }, + }; +}); diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index 7d9bf6e90fd7..067942665e3b 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(), diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 4e7a8482c474..60886c685bea 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -164,6 +164,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,