Skip to content

Commit d8bd0f5

Browse files
authored
fix(core): Decode filename and module stack frame properties in Node stack parser (#14544)
In ESM, the Node stack trace filenames are URL-encoded. This patch: - decodes the `filename` stack frame property - decodes the `module` property (which is derived from the raw filename) - adds a bunch of unit tests for the module name extraction logic (general tests as well as for encoded file names) - adds unit and integration tests (ESM and CJS) for file names with spaces
1 parent bd94e8d commit d8bd0f5

File tree

8 files changed

+225
-11
lines changed

8 files changed

+225
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
autoSessionTracking: false,
8+
transport: loggingTransport,
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const Sentry = require('@sentry/node');
2+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
autoSessionTracking: false,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.captureException(new Error('Test Error'));
12+
13+
// some more post context
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import * as Sentry from '@sentry/node';
2+
3+
Sentry.captureException(new Error('Test Error'));
4+
5+
// some more post context
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { join } from 'path';
2+
import { conditionalTest } from '../../utils';
3+
import { createRunner } from '../../utils/runner';
4+
5+
conditionalTest({ min: 18 })('ContextLines integration in ESM', () => {
6+
test('reads encoded context lines from filenames with spaces', done => {
7+
expect.assertions(1);
8+
const instrumentPath = join(__dirname, 'instrument.mjs');
9+
10+
createRunner(__dirname, 'scenario with space.mjs')
11+
.withFlags('--import', instrumentPath)
12+
.expect({
13+
event: {
14+
exception: {
15+
values: [
16+
{
17+
value: 'Test Error',
18+
stacktrace: {
19+
frames: expect.arrayContaining([
20+
{
21+
filename: expect.stringMatching(/\/scenario with space.mjs$/),
22+
context_line: "Sentry.captureException(new Error('Test Error'));",
23+
pre_context: ["import * as Sentry from '@sentry/node';", ''],
24+
post_context: ['', '// some more post context'],
25+
colno: 25,
26+
lineno: 3,
27+
function: '?',
28+
in_app: true,
29+
module: 'scenario with space',
30+
},
31+
]),
32+
},
33+
},
34+
],
35+
},
36+
},
37+
})
38+
.start(done);
39+
});
40+
});
41+
42+
describe('ContextLines integration in CJS', () => {
43+
test('reads context lines from filenames with spaces', done => {
44+
expect.assertions(1);
45+
46+
createRunner(__dirname, 'scenario with space.cjs')
47+
.expect({
48+
event: {
49+
exception: {
50+
values: [
51+
{
52+
value: 'Test Error',
53+
stacktrace: {
54+
frames: expect.arrayContaining([
55+
{
56+
filename: expect.stringMatching(/\/scenario with space.cjs$/),
57+
context_line: "Sentry.captureException(new Error('Test Error'));",
58+
pre_context: [
59+
'Sentry.init({',
60+
" dsn: 'https://[email protected]/1337',",
61+
" release: '1.0',",
62+
' autoSessionTracking: false,',
63+
' transport: loggingTransport,',
64+
'});',
65+
'',
66+
],
67+
post_context: ['', '// some more post context'],
68+
colno: 25,
69+
lineno: 11,
70+
function: 'Object.?',
71+
in_app: true,
72+
module: 'scenario with space',
73+
},
74+
]),
75+
},
76+
},
77+
],
78+
},
79+
},
80+
})
81+
.start(done);
82+
});
83+
});

packages/core/src/utils-hoist/node-stack-trace.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function node(getModule?: GetModuleFn): StackLineParserFn {
113113
}
114114

115115
return {
116-
filename,
116+
filename: filename ? decodeURI(filename) : undefined,
117117
module: getModule ? getModule(filename) : undefined,
118118
function: functionName,
119119
lineno: _parseIntOrUndefined(lineMatch[3]),

packages/core/test/utils-hoist/stacktrace.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,64 @@ describe('node', () => {
319319
const result = node(line);
320320
expect(result?.in_app).toBe(true);
321321
});
322+
323+
it('parses frame filename paths with spaces and characters in file name', () => {
324+
const input = 'at myObject.myMethod (/path/to/file with space(1).js:10:5)';
325+
326+
const expectedOutput = {
327+
filename: '/path/to/file with space(1).js',
328+
module: undefined,
329+
function: 'myObject.myMethod',
330+
lineno: 10,
331+
colno: 5,
332+
in_app: true,
333+
};
334+
335+
expect(node(input)).toEqual(expectedOutput);
336+
});
337+
338+
it('parses frame filename paths with spaces and characters in file path', () => {
339+
const input = 'at myObject.myMethod (/path with space(1)/to/file.js:10:5)';
340+
341+
const expectedOutput = {
342+
filename: '/path with space(1)/to/file.js',
343+
module: undefined,
344+
function: 'myObject.myMethod',
345+
lineno: 10,
346+
colno: 5,
347+
in_app: true,
348+
};
349+
350+
expect(node(input)).toEqual(expectedOutput);
351+
});
352+
353+
it('parses encoded frame filename paths with spaces and characters in file name', () => {
354+
const input = 'at myObject.myMethod (/path/to/file%20with%20space(1).js:10:5)';
355+
356+
const expectedOutput = {
357+
filename: '/path/to/file with space(1).js',
358+
module: undefined,
359+
function: 'myObject.myMethod',
360+
lineno: 10,
361+
colno: 5,
362+
in_app: true,
363+
};
364+
365+
expect(node(input)).toEqual(expectedOutput);
366+
});
367+
368+
it('parses encoded frame filename paths with spaces and characters in file path', () => {
369+
const input = 'at myObject.myMethod (/path%20with%20space(1)/to/file.js:10:5)';
370+
371+
const expectedOutput = {
372+
filename: '/path with space(1)/to/file.js',
373+
module: undefined,
374+
function: 'myObject.myMethod',
375+
lineno: 10,
376+
colno: 5,
377+
in_app: true,
378+
};
379+
380+
expect(node(input)).toEqual(expectedOutput);
381+
});
322382
});

packages/node/src/utils/module.ts

+8-10
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,27 @@ export function createGetModuleFromFilename(
2929
file = file.slice(0, ext.length * -1);
3030
}
3131

32+
// The file name might be URI-encoded which we want to decode to
33+
// the original file name.
34+
const decodedFile = decodeURIComponent(file);
35+
3236
if (!dir) {
3337
// No dirname whatsoever
3438
dir = '.';
3539
}
3640

3741
const n = dir.lastIndexOf('/node_modules');
3842
if (n > -1) {
39-
return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`;
43+
return `${dir.slice(n + 14).replace(/\//g, '.')}:${decodedFile}`;
4044
}
4145

4246
// Let's see if it's a part of the main module
4347
// To be a part of main module, it has to share the same base
4448
if (dir.startsWith(normalizedBase)) {
45-
let moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.');
46-
47-
if (moduleName) {
48-
moduleName += ':';
49-
}
50-
moduleName += file;
51-
52-
return moduleName;
49+
const moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.');
50+
return moduleName ? `${moduleName}:${decodedFile}` : decodedFile;
5351
}
5452

55-
return file;
53+
return decodedFile;
5654
};
5755
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createGetModuleFromFilename } from '../../src';
2+
3+
describe('createGetModuleFromFilename', () => {
4+
it.each([
5+
['/path/to/file.js', 'file'],
6+
['/path/to/file.mjs', 'file'],
7+
['/path/to/file.cjs', 'file'],
8+
['file.js', 'file'],
9+
])('returns the module name from a filename %s', (filename, expected) => {
10+
const getModule = createGetModuleFromFilename();
11+
expect(getModule(filename)).toBe(expected);
12+
});
13+
14+
it('applies the given base path', () => {
15+
const getModule = createGetModuleFromFilename('/path/to/base');
16+
expect(getModule('/path/to/base/file.js')).toBe('file');
17+
});
18+
19+
it('decodes URI-encoded file names', () => {
20+
const getModule = createGetModuleFromFilename();
21+
expect(getModule('/path%20with%space/file%20with%20spaces(1).js')).toBe('file with spaces(1)');
22+
});
23+
24+
it('returns undefined if no filename is provided', () => {
25+
const getModule = createGetModuleFromFilename();
26+
expect(getModule(undefined)).toBeUndefined();
27+
});
28+
29+
it.each([
30+
['/path/to/base/node_modules/@sentry/test/file.js', '@sentry.test:file'],
31+
['/path/to/base/node_modules/somePkg/file.js', 'somePkg:file'],
32+
])('handles node_modules file paths %s', (filename, expected) => {
33+
const getModule = createGetModuleFromFilename();
34+
expect(getModule(filename)).toBe(expected);
35+
});
36+
37+
it('handles windows paths with passed basePath and node_modules', () => {
38+
const getModule = createGetModuleFromFilename('C:\\path\\to\\base', true);
39+
expect(getModule('C:\\path\\to\\base\\node_modules\\somePkg\\file.js')).toBe('somePkg:file');
40+
});
41+
42+
it('handles windows paths with default basePath', () => {
43+
const getModule = createGetModuleFromFilename(undefined, true);
44+
expect(getModule('C:\\path\\to\\base\\somePkg\\file.js')).toBe('file');
45+
});
46+
});

0 commit comments

Comments
 (0)