diff --git a/packages/browser/src/stack-parsers.ts b/packages/browser/src/stack-parsers.ts index 0d01878ae7fe..8e387c66fff4 100644 --- a/packages/browser/src/stack-parsers.ts +++ b/packages/browser/src/stack-parsers.ts @@ -67,7 +67,7 @@ export const chromeStackLineParser: StackLineParser = [CHROME_PRIORITY, chrome]; // generates filenames without a prefix like `file://` the filenames in the stacktrace are just 42.js // We need this specific case for now because we want no other regex to match. const geckoREgex = - /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|capacitor).*?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; + /^\s*(.*?)(?:\((.*?)\))?(?:^|@)?((?:file|https?|blob|chrome|webpack|resource|moz-extension|safari-extension|safari-web-extension|capacitor)?:\/.*?|\[native code\]|[^@]*(?:bundle|\d+\.js)|\/[\w\-. /=]+)(?::(\d+))?(?::(\d+))?\s*$/i; const geckoEvalRegex = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; const gecko: StackLineParserFn = line => { diff --git a/packages/browser/test/unit/tracekit/firefox.test.ts b/packages/browser/test/unit/tracekit/firefox.test.ts index a9f8ee2d50cb..f75dd7ccf010 100644 --- a/packages/browser/test/unit/tracekit/firefox.test.ts +++ b/packages/browser/test/unit/tracekit/firefox.test.ts @@ -310,4 +310,50 @@ describe('Tracekit - Firefox Tests', () => { }, }); }); + + it('should parse Firefox errors with `file` inside an identifier', () => { + const FIREFOX_FILE_IN_IDENTIFIER = { + stack: + 'us@https://www.random_website.com/vendor.d1cae9cfc9917df88de7.js:1:296021\n' + + 'detectChanges@https://www.random_website.com/vendor.d1cae9cfc9917df88de7.js:1:333807\n' + + 'handleProfileResult@https://www.random_website.com/main.4a4119c3cdfd10266d84.js:146:1018410\n', + fileName: 'https://www.random_website.com/main.4a4119c3cdfd10266d84.js', + lineNumber: 5529, + columnNumber: 16, + message: 'this.props.raw[this.state.dataSource].rows is undefined', + name: 'TypeError', + }; + + const stacktrace = exceptionFromError(parser, FIREFOX_FILE_IN_IDENTIFIER); + + expect(stacktrace).toEqual({ + stacktrace: { + frames: [ + { + colno: 1018410, + filename: 'https://www.random_website.com/main.4a4119c3cdfd10266d84.js', + function: 'handleProfileResult', + in_app: true, + lineno: 146, + }, + { + colno: 333807, + filename: 'https://www.random_website.com/vendor.d1cae9cfc9917df88de7.js', + function: 'detectChanges', + in_app: true, + lineno: 1, + }, + { + colno: 296021, + filename: 'https://www.random_website.com/vendor.d1cae9cfc9917df88de7.js', + function: 'us', + in_app: true, + lineno: 1, + }, + ], + }, + type: 'TypeError', + value: 'this.props.raw[this.state.dataSource].rows is undefined', + }); + }); }); diff --git a/packages/integration-tests/suites/stacktraces/init.js b/packages/integration-tests/suites/stacktraces/init.js new file mode 100644 index 000000000000..d8c94f36fdd0 --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/init.js @@ -0,0 +1,7 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); diff --git a/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/subject.js b/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/subject.js new file mode 100644 index 000000000000..b9223bc91ac0 --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/subject.js @@ -0,0 +1,27 @@ +function httpsCall() { + webpackDevServer(); +} + +const webpackDevServer = () => { + Response.httpCode(); +}; + +class Response { + constructor() {} + + static httpCode(params) { + throw new Error('test_err'); + } +} + +const decodeBlob = function() { + (function readFile() { + httpsCall(); + })(); +}; + +try { + decodeBlob(); +} catch (err) { + Sentry.captureException(err); +} diff --git a/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts b/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts new file mode 100644 index 000000000000..191dcdd19385 --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/protocol_containing_fn_identifiers/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest( + 'should parse function identifiers that contain protocol names correctly', + async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const frames = eventData.exception?.values?.[0].stacktrace?.frames; + + runInChromium(() => { + expect(frames).toMatchObject([ + { function: '?' }, + { function: '?' }, + { function: 'decodeBlob' }, + { function: 'readFile' }, + { function: 'httpsCall' }, + { function: 'webpackDevServer' }, + { function: 'Function.httpCode' }, + ]); + }); + + runInFirefox(() => { + expect(frames).toMatchObject([ + { function: '?' }, + { function: '?' }, + { function: 'decodeBlob' }, + { function: 'readFile' }, + { function: 'httpsCall' }, + { function: 'webpackDevServer' }, + { function: 'httpCode' }, + ]); + }); + + runInWebkit(() => { + expect(frames).toMatchObject([ + { function: 'global code' }, + { function: '?' }, + { function: 'decodeBlob' }, + { function: 'readFile' }, + { function: 'httpsCall' }, + { function: 'webpackDevServer' }, + { function: 'httpCode' }, + ]); + }); + }, +); + +sentryTest( + 'should not add any part of the function identifier to beginning of filename', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( + // specifically, we're trying to avoid values like `Blob@file://path/to/file` in frames with function names like `makeBlob` + Array(7).fill({ filename: expect.stringMatching(/^file:\/?/) }), + ); + }, +); diff --git a/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/subject.js b/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/subject.js new file mode 100644 index 000000000000..d7ca8d22e04c --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/subject.js @@ -0,0 +1,27 @@ +function https() { + webpack(); +} + +const webpack = () => { + File.http(); +}; + +class File { + constructor() {} + + static http(params) { + throw new Error('test_err'); + } +} + +const blob = function() { + (function file() { + https(); + })(); +}; + +try { + blob(); +} catch (err) { + Sentry.captureException(err); +} diff --git a/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts b/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts new file mode 100644 index 000000000000..1d09d68f9f08 --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/protocol_fn_identifiers/test.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest( + 'should parse function identifiers that are protocol names correctly', + async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const frames = eventData.exception?.values?.[0].stacktrace?.frames; + + runInChromium(() => { + expect(frames).toMatchObject([ + { function: '?' }, + { function: '?' }, + { function: 'blob' }, + { function: 'file' }, + { function: 'https' }, + { function: 'webpack' }, + { function: 'Function.http' }, + ]); + }); + + runInFirefox(() => { + expect(frames).toMatchObject([ + { function: '?' }, + { function: '?' }, + { function: 'blob' }, + { function: 'file' }, + { function: 'https' }, + { function: 'webpack' }, + { function: 'http' }, + ]); + }); + + runInWebkit(() => { + expect(frames).toMatchObject([ + { function: 'global code' }, + { function: '?' }, + { function: 'blob' }, + { function: 'file' }, + { function: 'https' }, + { function: 'webpack' }, + { function: 'http' }, + ]); + }); + }, +); + +sentryTest( + 'should not add any part of the function identifier to beginning of filename', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( + Array(7).fill({ filename: expect.stringMatching(/^file:\/?/) }), + ); + }, +); diff --git a/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/subject.js b/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/subject.js new file mode 100644 index 000000000000..ff63abc6f3ca --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/subject.js @@ -0,0 +1,29 @@ +function foo() { + bar(); +} + +const bar = () => { + Test.baz(); +}; + +class Test { + constructor() {} + + static baz(params) { + throw new Error('test_err'); + } +} + +const qux = function() { + (() => { + (function() { + foo(); + })(); + })(); +}; + +try { + qux(); +} catch (err) { + Sentry.captureException(err); +} diff --git a/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts b/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts new file mode 100644 index 000000000000..986de9c44bc9 --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/regular_fn_identifiers/test.ts @@ -0,0 +1,68 @@ +import { expect } from '@playwright/test'; +import { Event } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest( + 'should parse function identifiers correctly', + async ({ getLocalTestPath, page, runInChromium, runInFirefox, runInWebkit }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const frames = eventData.exception?.values?.[0].stacktrace?.frames; + + runInChromium(() => { + expect(frames).toMatchObject([ + { function: '?' }, + { function: '?' }, + { function: 'qux' }, + { function: '?' }, + { function: '?' }, + { function: 'foo' }, + { function: 'bar' }, + { function: 'Function.baz' }, + ]); + }); + + runInFirefox(() => { + expect(frames).toMatchObject([ + { function: '?' }, + { function: '?' }, + { function: 'qux' }, + { function: 'qux/<' }, + { function: 'qux/ { + expect(frames).toMatchObject([ + { function: 'global code' }, + { function: '?' }, + { function: 'qux' }, + { function: '?' }, + { function: '?' }, + { function: 'foo' }, + { function: 'bar' }, + { function: 'baz' }, + ]); + }); + }, +); + +sentryTest( + 'should not add any part of the function identifier to beginning of filename', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values?.[0].stacktrace?.frames).toMatchObject( + // specifically, we're trying to avoid values like `Blob@file://path/to/file` in frames with function names like `makeBlob` + Array(8).fill({ filename: expect.stringMatching(/^file:\/?/) }), + ); + }, +); diff --git a/packages/integration-tests/suites/stacktraces/template.hbs b/packages/integration-tests/suites/stacktraces/template.hbs new file mode 100644 index 000000000000..a28a09b7b485 --- /dev/null +++ b/packages/integration-tests/suites/stacktraces/template.hbs @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/integration-tests/utils/fixtures.ts b/packages/integration-tests/utils/fixtures.ts index 845a7637b177..3926bf1f9d74 100644 --- a/packages/integration-tests/utils/fixtures.ts +++ b/packages/integration-tests/utils/fixtures.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-empty-pattern */ import { test as base } from '@playwright/test'; import fs from 'fs'; import path from 'path'; @@ -20,17 +21,20 @@ const getAsset = (assetDir: string, asset: string): string => { return `utils/defaults/${asset}`; }; -export type TestOptions = { - testDir: string; -}; - export type TestFixtures = { testDir: string; - getLocalTestPath: (options: TestOptions) => Promise; + getLocalTestPath: (options: { testDir: string }) => Promise; + runInChromium: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; + runInFirefox: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; + runInWebkit: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; + runInSingleBrowser: ( + browser: 'chromium' | 'firefox' | 'webkit', + fn: (...args: unknown[]) => unknown, + args?: unknown[], + ) => unknown; }; const sentryTest = base.extend({ - // eslint-disable-next-line no-empty-pattern getLocalTestPath: ({}, use, testInfo) => { return use(async ({ testDir }) => { const pagePath = `file:///${path.resolve(testDir, './dist/index.html')}`; @@ -47,6 +51,24 @@ const sentryTest = base.extend({ return pagePath; }); }, + runInChromium: ({ runInSingleBrowser }, use) => { + return use((fn, args) => runInSingleBrowser('chromium', fn, args)); + }, + runInFirefox: ({ runInSingleBrowser }, use) => { + return use((fn, args) => runInSingleBrowser('firefox', fn, args)); + }, + runInWebkit: ({ runInSingleBrowser }, use) => { + return use((fn, args) => runInSingleBrowser('webkit', fn, args)); + }, + runInSingleBrowser: ({ browserName }, use) => { + return use((browser, fn, args = []) => { + if (browserName !== browser) { + return; + } + + return fn(...args); + }); + }, }); export { sentryTest };