Skip to content

feat(v9/node): Capture SystemError context and remove paths from message #17394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const DEPENDENTS: Dependent[] = [
'NodeClient',
'NODE_VERSION',
'childProcessIntegration',
'systemErrorIntegration',
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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://[email protected]/1337',
transport: loggingTransport,
sendDefaultPii: true,
});

readFileSync('non-existent-file.txt');
Original file line number Diff line number Diff line change
@@ -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://[email protected]/1337',
transport: loggingTransport,
});

readFileSync('non-existent-file.txt');
Original file line number Diff line number Diff line change
@@ -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();
});
});
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export {
startSession,
startSpan,
startSpanManual,
systemErrorIntegration,
tediousIntegration,
trpcMiddleware,
updateSpanName,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export {
spanToJSON,
spanToTraceHeader,
spanToBaggageHeader,
systemErrorIntegration,
trpcMiddleware,
updateSpanName,
supabaseIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export {
trpcMiddleware,
updateSpanName,
supabaseIntegration,
systemErrorIntegration,
instrumentSupabaseClient,
zodErrorsIntegration,
profiler,
Expand Down
1 change: 1 addition & 0 deletions packages/node-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
76 changes: 76 additions & 0 deletions packages/node-core/src/integrations/systemError.ts
Original file line number Diff line number Diff line change
@@ -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();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Error Message Handling and Context Population Issues

The systemErrorIntegration has two distinct issues:

  1. The logic to remove path and dest from exception messages assumes they are wrapped in single quotes and uses replace() which only removes the first occurrence. This fails if paths are unquoted, use different quotes, contain single quotes, or appear multiple times, potentially leaving malformed messages.
  2. The node_system_error context is populated by spreading the entire Error object, including base Error properties like name, message, and stack. This adds unnecessary data and can lead to unintended information leakage. Only SystemError-specific properties should be included.
Fix in Cursor Fix in Web

}
}

return event;
},
};
});
2 changes: 2 additions & 0 deletions packages/node-core/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -52,6 +53,7 @@ export function getDefaultIntegrations(): Integration[] {
functionToStringIntegration(),
linkedErrorsIntegration(),
requestDataIntegration(),
systemErrorIntegration(),
// Native Wrappers
consoleIntegration(),
httpIntegration(),
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export {
childProcessIntegration,
createSentryWinstonTransport,
SentryContextManager,
systemErrorIntegration,
generateInstrumentOnce,
getSentryRelease,
defaultStackParser,
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export {
startSession,
startSpan,
startSpanManual,
systemErrorIntegration,
tediousIntegration,
trpcMiddleware,
updateSpanName,
Expand Down
Loading