Skip to content

Commit e70d2c9

Browse files
committed
miss me with that regex
1 parent 4be2e67 commit e70d2c9

3 files changed

Lines changed: 104 additions & 2 deletions

File tree

packages/core/src/utils/stacktrace.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser {
3939

4040
// https://github.com/getsentry/sentry-javascript/issues/7813
4141
// Skip Error: lines
42-
if (cleanedLine.match(/\S*Error: /)) {
42+
// Using includes() instead of a regex to avoid O(n²) backtracking on long lines
43+
// https://github.com/getsentry/sentry-javascript/issues/20052
44+
if (cleanedLine.includes('Error: ')) {
4345
continue;
4446
}
4547

packages/core/test/lib/integrations/third-party-errors-filter.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,4 +672,40 @@ describe('ThirdPartyErrorFilter', () => {
672672
});
673673
});
674674
});
675+
676+
// Regression test for https://github.com/getsentry/sentry-javascript/issues/20052
677+
// The thirdPartyErrorFilterIntegration triggers addMetadataToStackFrames on every error event,
678+
// which calls ensureMetadataStacksAreParsed to parse all _sentryModuleMetadata stack keys.
679+
// This test verifies that metadata is correctly resolved even when the module metadata stacks
680+
// contain long lines (e.g. from minified bundles with long URLs/identifiers).
681+
describe('metadata stack parsing with long stack lines', () => {
682+
it('resolves metadata for frames whose filenames appear in module metadata stacks with long URLs', () => {
683+
const longFilename = 'https://example.com/_next/static/chunks/' + 'a'.repeat(200) + '.js';
684+
685+
// Simulate a module metadata entry with a realistic stack containing a long filename
686+
const fakeStack = [
687+
`Error: Sentry Module Metadata`,
688+
` at Object.<anonymous> (${longFilename}:1:1)`,
689+
].join('\n');
690+
GLOBAL_OBJ._sentryModuleMetadata![fakeStack] = { '_sentryBundlerPluginAppKey:long-url-key': true };
691+
692+
const event: Event = {
693+
exception: {
694+
values: [
695+
{
696+
stacktrace: {
697+
frames: [{ filename: longFilename, function: 'test', lineno: 1, colno: 1 }],
698+
},
699+
},
700+
],
701+
},
702+
};
703+
704+
addMetadataToStackFrames(stackParser, event);
705+
706+
// The frame should have module_metadata attached from the parsed metadata stack
707+
const frame = event.exception!.values![0]!.stacktrace!.frames![0]!;
708+
expect(frame.module_metadata).toEqual({ '_sentryBundlerPluginAppKey:long-url-key': true });
709+
});
710+
});
675711
});

packages/core/test/lib/utils/stacktrace.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,72 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest';
22
import { nodeStackLineParser } from '../../../src/utils/node-stack-trace';
3-
import { stripSentryFramesAndReverse } from '../../../src/utils/stacktrace';
3+
import { createStackParser, stripSentryFramesAndReverse } from '../../../src/utils/stacktrace';
44

55
describe('Stacktrace', () => {
6+
describe('createStackParser()', () => {
7+
it('skips lines that contain "Error: " (e.g. "TypeError: foo")', () => {
8+
const mockParser = vi.fn().mockReturnValue({ filename: 'test.js', function: 'test', lineno: 1, colno: 1 });
9+
const parser = createStackParser([0, mockParser]);
10+
11+
const stack = ['TypeError: foo is not a function', ' at test (test.js:1:1)'].join('\n');
12+
13+
const frames = parser(stack);
14+
15+
// The parser should only be called for the frame line, not the Error line
16+
expect(mockParser).toHaveBeenCalledTimes(1);
17+
expect(frames).toHaveLength(1);
18+
});
19+
20+
it('skips various Error type lines', () => {
21+
const mockParser = vi.fn().mockReturnValue({ filename: 'test.js', function: 'test', lineno: 1, colno: 1 });
22+
const parser = createStackParser([0, mockParser]);
23+
24+
const stack = [
25+
'Error: something went wrong',
26+
'TypeError: foo is not a function',
27+
'RangeError: Maximum call stack size exceeded',
28+
'SomeCustomError: custom message',
29+
' at test (test.js:1:1)',
30+
].join('\n');
31+
32+
const frames = parser(stack);
33+
34+
// Only the frame line should be parsed, all Error lines should be skipped
35+
expect(mockParser).toHaveBeenCalledTimes(1);
36+
expect(frames).toHaveLength(1);
37+
});
38+
39+
// Regression test for https://github.com/getsentry/sentry-javascript/issues/20052
40+
it('processes long non-whitespace lines without hanging', () => {
41+
const mockParser = vi.fn().mockReturnValue(undefined);
42+
const parser = createStackParser([0, mockParser]);
43+
44+
// Long non-whitespace lines (e.g. minified URLs) previously caused O(n²) backtracking
45+
const longLine = 'a'.repeat(2000);
46+
const stack = [longLine, ' at test (test.js:1:1)'].join('\n');
47+
48+
// Should complete without hanging (line gets truncated to 1024 chars internally)
49+
parser(stack);
50+
expect(mockParser).toHaveBeenCalledTimes(2);
51+
});
52+
53+
it('does not skip lines that do not contain "Error: "', () => {
54+
const mockParser = vi.fn().mockReturnValue({ filename: 'test.js', function: 'test', lineno: 1, colno: 1 });
55+
const parser = createStackParser([0, mockParser]);
56+
57+
const stack = [
58+
' at foo (test.js:1:1)',
59+
' at bar (test.js:2:1)',
60+
'ResizeObserver loop completed with undelivered notifications.',
61+
].join('\n');
62+
63+
parser(stack);
64+
65+
// All lines should be attempted by the parser (none contain "Error: ")
66+
expect(mockParser).toHaveBeenCalledTimes(3);
67+
});
68+
});
69+
670
describe('stripSentryFramesAndReverse()', () => {
771
describe('removed top frame if its internally reserved word (public API)', () => {
872
it('reserved captureException', () => {

0 commit comments

Comments
 (0)