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 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/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 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(), 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,