Skip to content

Commit 2c36fd3

Browse files
authored
feat(v9/node): Capture SystemError context and remove paths from message (#17394)
- Backport of #17331
1 parent b3acba0 commit 2c36fd3

File tree

12 files changed

+165
-0
lines changed

12 files changed

+165
-0
lines changed

dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const DEPENDENTS: Dependent[] = [
5252
'NodeClient',
5353
'NODE_VERSION',
5454
'childProcessIntegration',
55+
'systemErrorIntegration',
5556
],
5657
},
5758
{
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import { readFileSync } from 'fs';
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
transport: loggingTransport,
8+
sendDefaultPii: true,
9+
});
10+
11+
readFileSync('non-existent-file.txt');
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import { readFileSync } from 'fs';
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
transport: loggingTransport,
8+
});
9+
10+
readFileSync('non-existent-file.txt');
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { afterAll, describe, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
describe('SystemError integration', () => {
9+
test('sendDefaultPii: false', async () => {
10+
await createRunner(__dirname, 'basic.mjs')
11+
.expect({
12+
event: {
13+
contexts: {
14+
node_system_error: {
15+
errno: -2,
16+
code: 'ENOENT',
17+
syscall: 'open',
18+
},
19+
},
20+
exception: {
21+
values: [
22+
{
23+
type: 'Error',
24+
value: 'ENOENT: no such file or directory, open',
25+
},
26+
],
27+
},
28+
},
29+
})
30+
.start()
31+
.completed();
32+
});
33+
34+
test('sendDefaultPii: true', async () => {
35+
await createRunner(__dirname, 'basic-pii.mjs')
36+
.expect({
37+
event: {
38+
contexts: {
39+
node_system_error: {
40+
errno: -2,
41+
code: 'ENOENT',
42+
syscall: 'open',
43+
path: 'non-existent-file.txt',
44+
},
45+
},
46+
exception: {
47+
values: [
48+
{
49+
type: 'Error',
50+
value: 'ENOENT: no such file or directory, open',
51+
},
52+
],
53+
},
54+
},
55+
})
56+
.start()
57+
.completed();
58+
});
59+
});

packages/astro/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export {
123123
startSession,
124124
startSpan,
125125
startSpanManual,
126+
systemErrorIntegration,
126127
tediousIntegration,
127128
trpcMiddleware,
128129
updateSpanName,

packages/aws-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export {
113113
spanToJSON,
114114
spanToTraceHeader,
115115
spanToBaggageHeader,
116+
systemErrorIntegration,
116117
trpcMiddleware,
117118
updateSpanName,
118119
supabaseIntegration,

packages/google-cloud-serverless/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export {
114114
trpcMiddleware,
115115
updateSpanName,
116116
supabaseIntegration,
117+
systemErrorIntegration,
117118
instrumentSupabaseClient,
118119
zodErrorsIntegration,
119120
profiler,

packages/node-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejec
2121
export { anrIntegration, disableAnrDetectionForCallback } from './integrations/anr';
2222

2323
export { spotlightIntegration } from './integrations/spotlight';
24+
export { systemErrorIntegration } from './integrations/systemError';
2425
export { childProcessIntegration } from './integrations/childProcess';
2526
export { createSentryWinstonTransport } from './integrations/winston';
2627

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as util from 'node:util';
2+
import { defineIntegration } from '@sentry/core';
3+
4+
const INTEGRATION_NAME = 'NodeSystemError';
5+
6+
type SystemErrorContext = {
7+
dest?: string; // If present, the file path destination when reporting a file system error
8+
errno: number; // The system-provided error number
9+
path?: string; // If present, the file path when reporting a file system error
10+
};
11+
12+
type SystemError = Error & SystemErrorContext;
13+
14+
function isSystemError(error: unknown): error is SystemError {
15+
if (!(error instanceof Error)) {
16+
return false;
17+
}
18+
19+
if (!('errno' in error) || typeof error.errno !== 'number') {
20+
return false;
21+
}
22+
23+
// Appears this is the recommended way to check for Node.js SystemError
24+
// https://github.com/nodejs/node/issues/46869
25+
return util.getSystemErrorMap().has(error.errno);
26+
}
27+
28+
type Options = {
29+
/**
30+
* If true, includes the `path` and `dest` properties in the error context.
31+
*/
32+
includePaths?: boolean;
33+
};
34+
35+
/**
36+
* Captures context for Node.js SystemError errors.
37+
*/
38+
export const systemErrorIntegration = defineIntegration((options: Options = {}) => {
39+
return {
40+
name: INTEGRATION_NAME,
41+
processEvent: (event, hint, client) => {
42+
if (!isSystemError(hint.originalException)) {
43+
return event;
44+
}
45+
46+
const error = hint.originalException;
47+
48+
const errorContext: SystemErrorContext = {
49+
...error,
50+
};
51+
52+
if (!client.getOptions().sendDefaultPii && options.includePaths !== true) {
53+
delete errorContext.path;
54+
delete errorContext.dest;
55+
}
56+
57+
event.contexts = {
58+
...event.contexts,
59+
node_system_error: errorContext,
60+
};
61+
62+
for (const exception of event.exception?.values || []) {
63+
if (exception.value) {
64+
if (error.path && exception.value.includes(error.path)) {
65+
exception.value = exception.value.replace(`'${error.path}'`, '').trim();
66+
}
67+
if (error.dest && exception.value.includes(error.dest)) {
68+
exception.value = exception.value.replace(`'${error.dest}'`, '').trim();
69+
}
70+
}
71+
}
72+
73+
return event;
74+
},
75+
};
76+
});

packages/node-core/src/sdk/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexcept
3232
import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection';
3333
import { processSessionIntegration } from '../integrations/processSession';
3434
import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight';
35+
import { systemErrorIntegration } from '../integrations/systemError';
3536
import { makeNodeTransport } from '../transports';
3637
import type { NodeClientOptions, NodeOptions } from '../types';
3738
import { isCjs } from '../utils/commonjs';
@@ -52,6 +53,7 @@ export function getDefaultIntegrations(): Integration[] {
5253
functionToStringIntegration(),
5354
linkedErrorsIntegration(),
5455
requestDataIntegration(),
56+
systemErrorIntegration(),
5557
// Native Wrappers
5658
consoleIntegration(),
5759
httpIntegration(),

0 commit comments

Comments
 (0)