Skip to content

Commit 1c8810d

Browse files
committed
feat(node) Capture SystemError context and remove paths from message
1 parent aab4276 commit 1c8810d

File tree

6 files changed

+152
-0
lines changed

6 files changed

+152
-0
lines changed
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/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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { defineIntegration } from '@sentry/core';
2+
import { getSystemErrorMap } from 'util';
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 getSystemErrorMap().has(error.errno);
26+
}
27+
28+
/**
29+
* Captures context for Node.js SystemError errors.
30+
*/
31+
export const systemErrorIntegration = defineIntegration(() => {
32+
return {
33+
name: INTEGRATION_NAME,
34+
processEvent: (event, hint, client) => {
35+
if (!isSystemError(hint.originalException)) {
36+
return event;
37+
}
38+
39+
const error = hint.originalException;
40+
41+
const errorContext: SystemErrorContext = {
42+
...error,
43+
};
44+
45+
if (!client.getOptions().sendDefaultPii) {
46+
delete errorContext.path;
47+
delete errorContext.dest;
48+
}
49+
50+
event.contexts = {
51+
...event.contexts,
52+
node_system_error: errorContext,
53+
};
54+
55+
for (const exception of event.exception?.values || []) {
56+
if (exception.value) {
57+
if (error.path && exception.value.includes(error.path)) {
58+
exception.value = exception.value.replace(`'${error.path}'`, '');
59+
}
60+
if (error.dest && exception.value.includes(error.dest)) {
61+
exception.value = exception.value.replace(`'${error.dest}'`, '');
62+
}
63+
}
64+
}
65+
66+
return event;
67+
},
68+
};
69+
});

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)