Skip to content

Commit 9b65733

Browse files
authored
feat(node): Capture SystemError context and remove paths from message (#17331)
resolves #17332 Node.js has a [`SystemError`](https://nodejs.org/api/errors.html#class-systemerror) error class. This error type contains numerous properties which can be useful for debugging but it also includes many of them in the error message which stops fingerprinting from grouping these errors. This PR adds a default enabled integration that copies the error properties to a new context and strips paths from the error message. If the top-level `sendDefaultPii` is not enabled, the `path` and `dest` properties will not be included.
1 parent a89aa3d commit 9b65733

File tree

13 files changed

+166
-1
lines changed

13 files changed

+166
-1
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ module.exports = [
233233
import: createImport('init'),
234234
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
235235
gzip: true,
236-
limit: '147 KB',
236+
limit: '148 KB',
237237
},
238238
{
239239
name: '@sentry/node - without tracing',

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
@@ -124,6 +124,7 @@ export {
124124
startSession,
125125
startSpan,
126126
startSpanManual,
127+
systemErrorIntegration,
127128
tediousIntegration,
128129
trpcMiddleware,
129130
updateSpanName,

packages/aws-serverless/src/index.ts

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export {
115115
trpcMiddleware,
116116
updateSpanName,
117117
supabaseIntegration,
118+
systemErrorIntegration,
118119
instrumentSupabaseClient,
119120
zodErrorsIntegration,
120121
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+
});

0 commit comments

Comments
 (0)