Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/src/utils/stacktrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,37 @@ describe('ThirdPartyErrorFilter', () => {
});
});
});

// Regression test for https://github.com/getsentry/sentry-javascript/issues/20052
// The thirdPartyErrorFilterIntegration triggers addMetadataToStackFrames on every error event,
// which calls ensureMetadataStacksAreParsed to parse all _sentryModuleMetadata stack keys.
// This test verifies that metadata is correctly resolved even when the module metadata stacks
// contain long lines (e.g. from minified bundles with long URLs/identifiers).
describe('metadata stack parsing with long stack lines', () => {
it('resolves metadata for frames whose filenames appear in module metadata stacks with long URLs', () => {
const longFilename = `https://example.com/_next/static/chunks/${'a'.repeat(200)}.js`;

// Simulate a module metadata entry with a realistic stack containing a long filename
const fakeStack = [`Error: Sentry Module Metadata`, ` at Object.<anonymous> (${longFilename}:1:1)`].join('\n');
GLOBAL_OBJ._sentryModuleMetadata![fakeStack] = { '_sentryBundlerPluginAppKey:long-url-key': true };

const event: Event = {
exception: {
values: [
{
stacktrace: {
frames: [{ filename: longFilename, function: 'test', lineno: 1, colno: 1 }],
},
},
],
},
};

addMetadataToStackFrames(stackParser, event);

// The frame should have module_metadata attached from the parsed metadata stack
const frame = event.exception!.values![0]!.stacktrace!.frames![0]!;
expect(frame.module_metadata).toEqual({ '_sentryBundlerPluginAppKey:long-url-key': true });
});
});
});
66 changes: 65 additions & 1 deletion packages/core/test/lib/utils/stacktrace.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,72 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { nodeStackLineParser } from '../../../src/utils/node-stack-trace';
import { stripSentryFramesAndReverse } from '../../../src/utils/stacktrace';
import { createStackParser, stripSentryFramesAndReverse } from '../../../src/utils/stacktrace';

describe('Stacktrace', () => {
describe('createStackParser()', () => {
it('skips lines that contain "Error: " (e.g. "TypeError: foo")', () => {
const mockParser = vi.fn().mockReturnValue({ filename: 'test.js', function: 'test', lineno: 1, colno: 1 });
const parser = createStackParser([0, mockParser]);

const stack = ['TypeError: foo is not a function', ' at test (test.js:1:1)'].join('\n');

const frames = parser(stack);

// The parser should only be called for the frame line, not the Error line
expect(mockParser).toHaveBeenCalledTimes(1);
expect(frames).toHaveLength(1);
});

it('skips various Error type lines', () => {
const mockParser = vi.fn().mockReturnValue({ filename: 'test.js', function: 'test', lineno: 1, colno: 1 });
const parser = createStackParser([0, mockParser]);

const stack = [
'Error: something went wrong',
'TypeError: foo is not a function',
'RangeError: Maximum call stack size exceeded',
'SomeCustomError: custom message',
' at test (test.js:1:1)',
].join('\n');

const frames = parser(stack);

// Only the frame line should be parsed, all Error lines should be skipped
expect(mockParser).toHaveBeenCalledTimes(1);
expect(frames).toHaveLength(1);
});

// Regression test for https://github.com/getsentry/sentry-javascript/issues/20052
it('processes long non-whitespace lines without hanging', () => {
const mockParser = vi.fn().mockReturnValue(undefined);
const parser = createStackParser([0, mockParser]);

// Long non-whitespace lines (e.g. minified URLs) previously caused O(n²) backtracking
const longLine = 'a'.repeat(2000);
const stack = [longLine, ' at test (test.js:1:1)'].join('\n');

// Should complete without hanging (line gets truncated to 1024 chars internally)
parser(stack);
expect(mockParser).toHaveBeenCalledTimes(2);
});

it('does not skip lines that do not contain "Error: "', () => {
const mockParser = vi.fn().mockReturnValue({ filename: 'test.js', function: 'test', lineno: 1, colno: 1 });
const parser = createStackParser([0, mockParser]);

const stack = [
' at foo (test.js:1:1)',
' at bar (test.js:2:1)',
'ResizeObserver loop completed with undelivered notifications.',
].join('\n');

parser(stack);

// All lines should be attempted by the parser (none contain "Error: ")
expect(mockParser).toHaveBeenCalledTimes(3);
});
});

describe('stripSentryFramesAndReverse()', () => {
describe('removed top frame if its internally reserved word (public API)', () => {
it('reserved captureException', () => {
Expand Down
Loading