diff --git a/.craft.yml b/.craft.yml index b5646547f113..44d245311312 100644 --- a/.craft.yml +++ b/.craft.yml @@ -142,10 +142,12 @@ targets: id: '@sentry-internal/eslint-config-sdk' includeNames: /^sentry-internal-eslint-config-sdk-\d.*\.tgz$/ - # AWS Lambda Layer target + # TODO(v9): Remove this target + # NOTE: We publish the v8 layer under its own name so people on v8 can still get patches + # whenever we release a new v8 version—otherwise we would overwrite the current major lambda layer. - name: aws-lambda-layer includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ - layerName: SentryNodeServerlessSDK + layerName: SentryNodeServerlessSDKv8 compatibleRuntimes: - name: node versions: @@ -157,13 +159,10 @@ targets: - nodejs20.x license: MIT - # NOTE: We publish the v8 layer under its own name so people on v8 can still get patches - # whenever we release a new v8 version—otherwise we would overwrite the current major lambda layer. - # AWS Lambda Layer target - name: aws-lambda-layer includeNames: /^sentry-node-serverless-\d+.\d+.\d+(-(beta|alpha|rc)\.\d+)?\.zip$/ - layerName: SentryNodeServerlessSDKv8 + layerName: SentryNodeServerlessSDK compatibleRuntimes: - name: node versions: diff --git a/.size-limit.js b/.size-limit.js index 844d57447495..6e73c9234c09 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -147,7 +147,7 @@ module.exports = [ path: 'packages/svelte/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '24 KB', + limit: '25 KB', }, // Browser CDN bundles { diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b6bd23a095c..dd6332f4c125 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,23 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.45.0 + +- feat(core): Add `handled` option to `captureConsoleIntegration` ([#14664](https://github.com/getsentry/sentry-javascript/pull/14664)) +- feat(browser): Attach virtual stack traces to `HttpClient` events ([#14515](https://github.com/getsentry/sentry-javascript/pull/14515)) +- feat(replay): Upgrade rrweb packages to 2.31.0 ([#14689](https://github.com/getsentry/sentry-javascript/pull/14689)) +- fix(aws-serverless): Remove v8 layer as it overwrites the current layer for docs ([#14679](https://github.com/getsentry/sentry-javascript/pull/14679)) +- fix(browser): Mark stack trace from `captureMessage` with `attachStacktrace: true` as synthetic ([#14668](https://github.com/getsentry/sentry-javascript/pull/14668)) +- fix(core): Mark stack trace from `captureMessage` with `attatchStackTrace: true` as synthetic ([#14670](https://github.com/getsentry/sentry-javascript/pull/14670)) +- fix(core): Set `level` in server runtime `captureException` ([#10587](https://github.com/getsentry/sentry-javascript/pull/10587)) +- fix(profiling-node): Guard invocation of native profiling methods ([#14676](https://github.com/getsentry/sentry-javascript/pull/14676)) +- fix(nuxt): Inline nitro-utils function ([#14680](https://github.com/getsentry/sentry-javascript/pull/14680)) +- fix(profiling-node): Ensure profileId is added to transaction event ([#14681](https://github.com/getsentry/sentry-javascript/pull/14681)) +- fix(react): Add React Router Descendant Routes support ([#14304](https://github.com/getsentry/sentry-javascript/pull/14304)) +- fix: Disable ANR and Local Variables if debugger is enabled via CLI args ([#14643](https://github.com/getsentry/sentry-javascript/pull/14643)) + +Work in this release was contributed by @anonrig and @Zih0. Thank you for your contributions! + ## 8.44.0 ### Deprecations diff --git a/biome.json b/biome.json index 49b865345e39..db56e24f80f0 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,8 @@ "vcs": { "enabled": true, "clientKind": "git", - "useIgnoreFile": true + "useIgnoreFile": true, + "defaultBranch": "develop" }, "organizeImports": { "enabled": true @@ -17,13 +18,15 @@ "noUnusedVariables": "error", "noPrecisionLoss": "error" }, + "complexity": { + "useRegexLiterals": "error" + }, "suspicious": { "all": false, "noControlCharactersInRegex": "error" }, "nursery": { - "noUnusedImports": "error", - "useRegexLiterals": "error" + "noUnusedImports": "error" }, "performance": { "all": true, @@ -92,6 +95,10 @@ "json": { "formatter": { "enabled": true + }, + "parser": { + "allowComments": true, + "allowTrailingCommas": true } } } diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index b41d9e55cfb2..e9da36325bf7 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -42,7 +42,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.44.1", - "@sentry-internal/rrweb": "2.30.0", + "@sentry-internal/rrweb": "2.31.0", "@sentry/browser": "8.44.0", "axios": "1.7.7", "babel-loader": "^8.2.2", diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts index 7630d2a7824f..6f8cfc20f4aa 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts @@ -30,6 +30,7 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p extra: { arguments: ['console log'], }, + message: 'console log', }), ); expect(logEvent?.exception).toBeUndefined(); @@ -40,6 +41,7 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p extra: { arguments: ['console warn'], }, + message: 'console warn', }), ); expect(warnEvent?.exception).toBeUndefined(); @@ -50,6 +52,7 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p extra: { arguments: ['console info'], }, + message: 'console info', }), ); expect(infoEvent?.exception).toBeUndefined(); @@ -60,6 +63,7 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p extra: { arguments: ['console error'], }, + message: 'console error', }), ); expect(errorEvent?.exception).toBeUndefined(); @@ -70,6 +74,7 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p extra: { arguments: ['console trace'], }, + message: 'console trace', }), ); expect(traceEvent?.exception).toBeUndefined(); @@ -90,6 +95,11 @@ sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, p }), ); expect(errorWithErrorEvent?.exception?.values?.[0].value).toBe('console error with error object'); + expect(errorWithErrorEvent?.exception?.values?.[0].mechanism).toEqual({ + // TODO (v9): Adjust to true after changing the integration's default value + handled: false, + type: 'console', + }); expect(traceWithErrorEvent).toEqual( expect.objectContaining({ level: 'log', diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/axios/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/axios/test.ts index d4e7ba222b30..19161be50ad3 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/axios/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/axios/test.ts @@ -40,6 +40,15 @@ sentryTest( type: 'http.client', handled: false, }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + filename: 'http://sentry-test.io/subject.bundle.js', + function: '?', + in_app: true, + }), + ]), + }, }, ], }, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts index 09390fffd573..c92419aae72f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts @@ -42,6 +42,15 @@ sentryTest( type: 'http.client', handled: false, }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + filename: 'http://sentry-test.io/subject.bundle.js', + function: '?', + in_app: true, + }), + ]), + }, }, ], }, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts index f1e23bbd080b..4ceec6d51c38 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts @@ -38,6 +38,15 @@ sentryTest('works with a Request passed in', async ({ getLocalTestUrl, page }) = type: 'http.client', handled: false, }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + filename: 'http://sentry-test.io/subject.bundle.js', + function: '?', + in_app: true, + }), + ]), + }, }, ], }, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts index ba34c4fdfa0f..9c408c8ade50 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts @@ -40,6 +40,15 @@ sentryTest( type: 'http.client', handled: false, }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + filename: 'http://sentry-test.io/subject.bundle.js', + function: '?', + in_app: true, + }), + ]), + }, }, ], }, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts index 0ea8fc7bc0cb..92e31819673f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts @@ -38,6 +38,15 @@ sentryTest('works with a Request (without body) & options passed in', async ({ g type: 'http.client', handled: false, }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + filename: 'http://sentry-test.io/subject.bundle.js', + function: '?', + in_app: true, + }), + ]), + }, }, ], }, diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/test.ts index d342e9abf748..d323c73725d6 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/xhr/test.ts @@ -40,6 +40,15 @@ sentryTest( type: 'http.client', handled: false, }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + filename: 'http://sentry-test.io/subject.bundle.js', + function: '?', + in_app: true, + }), + ]), + }, }, ], }, diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js new file mode 100644 index 000000000000..99ffd9f0ab31 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + attachStacktrace: true, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/subject.js new file mode 100644 index 000000000000..cf462c59a2fb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/subject.js @@ -0,0 +1 @@ +Sentry.captureMessage('foo'); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts new file mode 100644 index 000000000000..0e769cca73fe --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest( + 'captures a simple message string with stack trace if `attachStackTrace` is `true`', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.message).toBe('foo'); + expect(eventData.level).toBe('info'); + expect(eventData.exception?.values?.[0]).toEqual({ + mechanism: { + handled: true, + type: 'generic', + synthetic: true, + }, + stacktrace: { + frames: expect.arrayContaining([expect.any(Object), expect.any(Object)]), + }, + value: 'foo', + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js index ed6db5b5afe2..64524952dfa7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/subject.js @@ -1,17 +1,19 @@ -const blockUI = (delay = 70) => e => { - const startTime = Date.now(); +const blockUI = + (delay = 70) => + e => { + const startTime = Date.now(); - function getElasped() { - const time = Date.now(); - return time - startTime; - } + function getElasped() { + const time = Date.now(); + return time - startTime; + } - while (getElasped() < delay) { - // - } + while (getElasped() < delay) { + // + } - e.target.classList.add('clicked'); -}; + e.target.classList.add('clicked'); + }; document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js index ed6db5b5afe2..64524952dfa7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/subject.js @@ -1,17 +1,19 @@ -const blockUI = (delay = 70) => e => { - const startTime = Date.now(); +const blockUI = + (delay = 70) => + e => { + const startTime = Date.now(); - function getElasped() { - const time = Date.now(); - return time - startTime; - } + function getElasped() { + const time = Date.now(); + return time - startTime; + } - while (getElasped() < delay) { - // - } + while (getElasped() < delay) { + // + } - e.target.classList.add('clicked'); -}; + e.target.classList.add('clicked'); + }; document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js index ed6db5b5afe2..64524952dfa7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/subject.js @@ -1,17 +1,19 @@ -const blockUI = (delay = 70) => e => { - const startTime = Date.now(); +const blockUI = + (delay = 70) => + e => { + const startTime = Date.now(); - function getElasped() { - const time = Date.now(); - return time - startTime; - } + function getElasped() { + const time = Date.now(); + return time - startTime; + } - while (getElasped() < delay) { - // - } + while (getElasped() < delay) { + // + } - e.target.classList.add('clicked'); -}; + e.target.classList.add('clicked'); + }; document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js index ed6db5b5afe2..64524952dfa7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js @@ -1,17 +1,19 @@ -const blockUI = (delay = 70) => e => { - const startTime = Date.now(); +const blockUI = + (delay = 70) => + e => { + const startTime = Date.now(); - function getElasped() { - const time = Date.now(); - return time - startTime; - } + function getElasped() { + const time = Date.now(); + return time - startTime; + } - while (getElasped() < delay) { - // - } + while (getElasped() < delay) { + // + } - e.target.classList.add('clicked'); -}; + e.target.classList.add('clicked'); + }; document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index c3b515ada32d..b9b4dcb4c1f3 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -176,7 +176,6 @@ class SentryScenarioGenerationPlugin { } : {}; - // Checking if the current scenario has imported `@sentry/integrations`. compiler.hooks.normalModuleFactory.tap(this._name, factory => { factory.hooks.parser.for('javascript/auto').tap(this._name, parser => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 91c96298fbce..03c654c22eb1 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -134,13 +134,10 @@ export const countEnvelopes = async ( page.on('request', requestHandler); - setTimeout( - () => { - page.off('request', requestHandler); - resolve(reqCount); - }, - options?.timeout || 1000, - ); + setTimeout(() => { + page.off('request', requestHandler); + resolve(reqCount); + }, options?.timeout || 1000); }); if (options?.url) { diff --git a/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json index 374cc9d294aa..84f1f992d275 100644 --- a/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json +++ b/dev-packages/e2e-tests/test-applications/angular-17/tsconfig.app.json @@ -5,10 +5,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/angular-18/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-18/tsconfig.app.json index 374cc9d294aa..84f1f992d275 100644 --- a/dev-packages/e2e-tests/test-applications/angular-18/tsconfig.app.json +++ b/dev-packages/e2e-tests/test-applications/angular-18/tsconfig.app.json @@ -5,10 +5,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json index 3775b37e3bbc..8886e903f8d0 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json @@ -6,10 +6,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json index 5fb748d9207a..e00e30e6d4fb 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json @@ -4,12 +4,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine"] }, - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json index 877f7b3990f9..55663457968a 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tsconfig.app.json @@ -18,15 +18,11 @@ ], } }, - "include": [ - "app/**/*", - "types/**/*" - ], + "include": ["app/**/*", "types/**/*"], "exclude": ["tests/**/*"], "ts-node": { "compilerOptions": { "module": "CommonJS" } } - } diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json index 919403ddcda8..bdfe8763ecc2 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tsconfig.app.json @@ -18,15 +18,11 @@ ], } }, - "include": [ - "app/**/*", - "types/**/*" - ], + "include": ["app/**/*", "types/**/*"], "exclude": ["tests/**/*"], "ts-node": { "compilerOptions": { "module": "CommonJS" } } - } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts index bdd1ea1f6102..80a667d155d8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/tailwind.config.ts @@ -1,7 +1,7 @@ import { type Config } from 'tailwindcss'; import { fontFamily } from 'tailwindcss/defaultTheme'; -export default ({ +export default { content: ['./src/**/*.tsx'], theme: { extend: { @@ -11,4 +11,4 @@ export default ({ }, }, plugins: [], -} satisfies Config); +} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json new file mode 100644 index 000000000000..ec6d7b05fee3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/package.json @@ -0,0 +1,55 @@ +{ + "name": "react-router-6-descendant-routes", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@types/react": "18.0.0", + "@types/react-dom": "18.0.0", + "express": "4.20.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.28.0", + "react-scripts": "5.0.1", + "typescript": "4.9.5" + }, + "scripts": { + "build": "react-scripts build", + "start": "run-p start:client start:server", + "start:client": "node server/app.js", + "start:server": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:assert": "pnpm test" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "serve": "14.0.1", + "npm-run-all2": "^6.2.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/public/index.html b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/server/app.js b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/server/app.js new file mode 100644 index 000000000000..5a8cdb3929a1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/server/app.js @@ -0,0 +1,47 @@ +const express = require('express'); + +const app = express(); +const PORT = 8080; + +const wait = time => { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, time); + }); +}; + +async function sseHandler(request, response, timeout = false) { + response.headers = { + 'Content-Type': 'text/event-stream', + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Access-Control-Allow-Origin': '*', + }; + + response.setHeader('Cache-Control', 'no-cache'); + response.setHeader('Content-Type', 'text/event-stream'); + response.setHeader('Access-Control-Allow-Origin', '*'); + response.setHeader('Connection', 'keep-alive'); + + response.flushHeaders(); + + await wait(2000); + + for (let index = 0; index < 10; index++) { + response.write(`data: ${new Date().toISOString()}\n\n`); + if (timeout) { + await wait(10000); + } + } + + response.end(); +} + +app.get('/sse', (req, res) => sseHandler(req, res)); + +app.get('/sse-timeout', (req, res) => sseHandler(req, res, true)); + +app.listen(PORT, () => { + console.log(`SSE service listening at http://localhost:${PORT}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/globals.d.ts new file mode 100644 index 000000000000..ffa61ca49acc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/globals.d.ts @@ -0,0 +1,5 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; + sentryReplayId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx new file mode 100644 index 000000000000..f6694a954915 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/index.tsx @@ -0,0 +1,73 @@ +import * as Sentry from '@sentry/react'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { + BrowserRouter, + Route, + Routes, + createRoutesFromChildren, + matchRoutes, + useLocation, + useNavigationType, +} from 'react-router-dom'; +import Index from './pages/Index'; + +const replay = Sentry.replayIntegration(); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.REACT_APP_E2E_TEST_DSN, + integrations: [ + Sentry.reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + trackFetchStreamPerformance: true, + }), + replay, + ], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + tunnel: 'http://localhost:3031', +}); + +const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes); + +const DetailsRoutes = () => ( + + Details} /> + +); + +const ViewsRoutes = () => ( + + Views} /> + } /> + +); + +const ProjectsRoutes = () => ( + + }> + No Match Page} /> + +); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + } /> + }> + + , +); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx new file mode 100644 index 000000000000..aa99b61f89ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/pages/Index.tsx @@ -0,0 +1,15 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +const Index = () => { + return ( + <> + + navigate + + + ); +}; + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/start-event-proxy.mjs new file mode 100644 index 000000000000..abd4db1ea605 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-router-6-descendant-routes', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts new file mode 100644 index 000000000000..23bc0aaabe95 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tests/transactions.test.ts @@ -0,0 +1,67 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/projects/123/views/234/567`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/projects/:projectId/views/:viewId/:detailId', + transaction_info: { + source: 'route', + }, + }); +}); + +test('sends a navigation transaction with a parameterized URL', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const navigationTxnPromise = waitForTransaction('react-router-6-descendant-routes', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + const pageloadTxn = await pageloadTxnPromise; + + expect(pageloadTxn).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.react.reactrouter_v6', + }, + }, + transaction: '/', + transaction_info: { + source: 'route', + }, + }); + + const linkElement = page.locator('id=navigation'); + + const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.react.reactrouter_v6', + }, + }, + transaction: '/projects/:projectId/views/:viewId/:detailId', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-6-descendant-routes/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "tests"] +} diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index fd15df4bd0b8..b1750b308d28 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -52,6 +52,35 @@ const ANR_EVENT = { }, }; +const ANR_EVENT_WITHOUT_STACKTRACE = { + // Ensure we have context + contexts: { + device: { + arch: expect.any(String), + }, + app: { + app_start_time: expect.any(String), + }, + os: { + name: expect.any(String), + }, + culture: { + timezone: expect.any(String), + }, + }, + // and an exception that is our ANR + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: 'Application Not Responding for at least 100 ms', + mechanism: { type: 'ANR' }, + stacktrace: {}, + }, + ], + }, +}; + const ANR_EVENT_WITH_SCOPE = { ...ANR_EVENT, user: { @@ -98,11 +127,11 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => createRunner(__dirname, 'indefinite.mjs').withMockSentryServer().expect({ event: ANR_EVENT }).start(done); }); - test('With --inspect', done => { + test("With --inspect the debugger isn't used", done => { createRunner(__dirname, 'basic.mjs') .withMockSentryServer() .withFlags('--inspect') - .expect({ event: ANR_EVENT_WITH_SCOPE }) + .expect({ event: ANR_EVENT_WITHOUT_STACKTRACE }) .start(done); }); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts new file mode 100644 index 000000000000..b56cb5cae8e6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts @@ -0,0 +1,11 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + attachStacktrace: true, +}); + +Sentry.captureMessage('Message'); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts new file mode 100644 index 000000000000..85b29fbcc239 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts @@ -0,0 +1,25 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('capture a simple message string with a stack trace if `attachStackTrace` is `true`', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'Message', + level: 'info', + exception: { + values: [ + { + mechanism: { synthetic: true, type: 'generic', handled: true }, + value: 'Message', + stacktrace: { frames: expect.any(Array) }, + }, + ], + }, + }, + }) + .start(done); +}); diff --git a/package.json b/package.json index 780c68c65f33..e948ae773c72 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "dev-packages/rollup-utils" ], "devDependencies": { - "@biomejs/biome": "^1.4.0", + "@biomejs/biome": "^1.5.2", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-esm-shim": "^0.1.5", "@rollup/plugin-json": "^6.1.0", diff --git a/packages/angular/test/errorhandler.test.ts b/packages/angular/test/errorhandler.test.ts index d2a65e86b51c..acaa0d201435 100644 --- a/packages/angular/test/errorhandler.test.ts +++ b/packages/angular/test/errorhandler.test.ts @@ -24,7 +24,10 @@ class CustomError extends Error { } class ErrorLikeShapedClass implements Partial { - constructor(public name: string, public message: string) {} + constructor( + public name: string, + public message: string, + ) {} } function createErrorEvent(message: string, innerError: any): ErrorEvent { diff --git a/packages/browser-utils/src/instrument/xhr.ts b/packages/browser-utils/src/instrument/xhr.ts index 506cc59a7bbf..cf44434ff4f8 100644 --- a/packages/browser-utils/src/instrument/xhr.ts +++ b/packages/browser-utils/src/instrument/xhr.ts @@ -31,6 +31,13 @@ export function instrumentXHR(): void { // eslint-disable-next-line @typescript-eslint/unbound-method xhrproto.open = new Proxy(xhrproto.open, { apply(originalOpen, xhrOpenThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, xhrOpenArgArray) { + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the error, that was caused by your XHR call did not + // have a stack trace. If you are using HttpClient integration, + // this is the expected behavior, as we are using this virtual error to capture + // the location of your XHR call, and group your HttpClient events accordingly. + const virtualError = new Error(); + const startTimestamp = timestampInSeconds() * 1000; // open() should always be called with two or more arguments @@ -74,6 +81,7 @@ export function instrumentXHR(): void { endTimestamp: timestampInSeconds() * 1000, startTimestamp, xhr: xhrOpenThisArg, + virtualError, }; triggerHandlers('xhr', handlerData); } diff --git a/packages/browser-utils/tsconfig.types.json b/packages/browser-utils/tsconfig.types.json index 775c9b91fe20..cf096d99a06a 100644 --- a/packages/browser-utils/tsconfig.types.json +++ b/packages/browser-utils/tsconfig.types.json @@ -4,11 +4,7 @@ // the fact that it introduces a dependency on `@sentry/browser` which doesn't exist anywhere else in the SDK, which // then prevents us from building that and this at the same time when doing a parallellized build from the repo root // level. - "exclude": [ - "src/index.bundle.ts", - "src/index.bundle.feedback.ts", - "src/index.bundle.replay.ts" - ], + "exclude": ["src/index.bundle.ts", "src/index.bundle.feedback.ts", "src/index.bundle.replay.ts"], "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 7f7539f5cf5f..ce34be0de707 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -347,6 +347,7 @@ function eventFromString( values: [{ value: message, stacktrace: { frames } }], }; } + addExceptionMechanism(event, { synthetic: true }); } if (isParameterizedString(message)) { diff --git a/packages/browser/src/integrations/browserapierrors.ts b/packages/browser/src/integrations/browserapierrors.ts index e1328a90831e..dc0662500d7b 100644 --- a/packages/browser/src/integrations/browserapierrors.ts +++ b/packages/browser/src/integrations/browserapierrors.ts @@ -171,7 +171,7 @@ function _wrapEventTarget(target: string): void { return; } - fill(proto, 'addEventListener', function (original: VoidFunction,): ( + fill(proto, 'addEventListener', function (original: VoidFunction): ( ...args: Parameters ) => ReturnType { return function (this: unknown, eventName, fn, options): VoidFunction { @@ -217,7 +217,7 @@ function _wrapEventTarget(target: string): void { }; }); - fill(proto, 'removeEventListener', function (originalRemoveEventListener: VoidFunction,): ( + fill(proto, 'removeEventListener', function (originalRemoveEventListener: VoidFunction): ( this: unknown, ...args: Parameters ) => ReturnType { diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts index eec0141bb97e..7eea7c2ccb6a 100644 --- a/packages/browser/src/integrations/httpclient.ts +++ b/packages/browser/src/integrations/httpclient.ts @@ -73,6 +73,7 @@ function _fetchResponseHandler( requestInfo: RequestInfo, response: Response, requestInit?: RequestInit, + error?: unknown, ): void { if (_shouldCaptureResponse(options, response.status, response.url)) { const request = _getRequest(requestInfo, requestInit); @@ -92,6 +93,7 @@ function _fetchResponseHandler( responseHeaders, requestCookies, responseCookies, + error, }); captureEvent(event); @@ -130,6 +132,7 @@ function _xhrResponseHandler( xhr: XMLHttpRequest, method: string, headers: Record, + error?: unknown, ): void { if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) { let requestHeaders, responseCookies, responseHeaders; @@ -162,6 +165,7 @@ function _xhrResponseHandler( // Can't access request cookies from XHR responseHeaders, responseCookies, + error, }); captureEvent(event); @@ -291,15 +295,15 @@ function _wrapFetch(client: Client, options: HttpClientOptions): void { return; } - const { response, args } = handlerData; + const { response, args, error, virtualError } = handlerData; const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; if (!response) { return; } - _fetchResponseHandler(options, requestInfo, response as Response, requestInit); - }); + _fetchResponseHandler(options, requestInfo, response as Response, requestInit, error || virtualError); + }, false); } /** @@ -315,6 +319,8 @@ function _wrapXHR(client: Client, options: HttpClientOptions): void { return; } + const { error, virtualError } = handlerData; + const xhr = handlerData.xhr as SentryWrappedXMLHttpRequest & XMLHttpRequest; const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; @@ -326,7 +332,7 @@ function _wrapXHR(client: Client, options: HttpClientOptions): void { const { method, request_headers: headers } = sentryXhrData; try { - _xhrResponseHandler(options, xhr, method, headers); + _xhrResponseHandler(options, xhr, method, headers, error || virtualError); } catch (e) { DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); } @@ -361,7 +367,12 @@ function _createEvent(data: { responseCookies?: Record; requestHeaders?: Record; requestCookies?: Record; + error?: unknown; }): SentryEvent { + const client = getClient(); + const virtualStackTrace = client && data.error && data.error instanceof Error ? data.error.stack : undefined; + // Remove the first frame from the stack as it's the HttpClient call + const stack = virtualStackTrace && client ? client.getOptions().stackParser(virtualStackTrace, 0, 1) : undefined; const message = `HTTP Client Error with status code: ${data.status}`; const event: SentryEvent = { @@ -371,6 +382,7 @@ function _createEvent(data: { { type: 'Error', value: message, + stacktrace: stack ? { frames: stack } : undefined, }, ], }, diff --git a/packages/browser/src/integrations/reportingobserver.ts b/packages/browser/src/integrations/reportingobserver.ts index 81db7d0932d1..647bdee3e548 100644 --- a/packages/browser/src/integrations/reportingobserver.ts +++ b/packages/browser/src/integrations/reportingobserver.ts @@ -54,7 +54,10 @@ interface ReportingObserverOptions { /** This is experimental and the types are not included with TypeScript, sadly. */ interface ReportingObserverClass { - new (handler: (reports: Report[]) => void, options: { buffered?: boolean; types?: ReportTypes[] }): { + new ( + handler: (reports: Report[]) => void, + options: { buffered?: boolean; types?: ReportTypes[] }, + ): { observe: () => void; }; } diff --git a/packages/browser/test/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts index 31112abbfc7e..f9949800ee94 100644 --- a/packages/browser/test/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { defaultStackParser } from '../src'; -import { eventFromUnknownInput, extractMessage, extractType } from '../src/eventbuilder'; +import { eventFromMessage, eventFromUnknownInput, extractMessage, extractType } from '../src/eventbuilder'; vi.mock('@sentry/core', async requireActual => { return { @@ -231,3 +231,33 @@ describe('extractName', () => { expect(name).toBeUndefined(); }); }); + +describe('eventFromMessage ', () => { + it('creates an event from a string message', async () => { + const event = await eventFromMessage(defaultStackParser, 'Test message'); + expect(event).toEqual({ + level: 'info', + message: 'Test message', + }); + }); + + it('creates an event with a synthetic stack trace if attachStacktrace is true', async () => { + const syntheticException = new Error('Test message'); + const event = await eventFromMessage(defaultStackParser, 'Test message', 'info', { syntheticException }, true); + expect(event.exception?.values?.[0]).toEqual( + expect.objectContaining({ + mechanism: { handled: true, synthetic: true, type: 'generic' }, + stacktrace: { + frames: expect.arrayContaining([expect.any(Object), expect.any(Object)]), + }, + value: 'Test message', + }), + ); + }); + + it("doesn't add a synthetic stack trace if attachStacktrace is false, even if one is passed-", async () => { + const syntheticException = new Error('Test message'); + const event = await eventFromMessage(defaultStackParser, 'Test message', 'info', { syntheticException }, false); + expect(event.exception).toBeUndefined(); + }); +}); diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/index.test.ts index 42492cd36747..c0e79481788b 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -235,6 +235,7 @@ describe('SentryBrowser', () => { await flush(2000); const event = beforeSend.mock.calls[0]?.[0]; + expect(event.level).toBe('error'); expect(event.exception).toBeDefined(); expect(event.exception.values[0]).toBeDefined(); expect(event.exception.values[0]?.type).toBe('Error'); @@ -242,20 +243,20 @@ describe('SentryBrowser', () => { expect(event.exception.values[0]?.stacktrace.frames).not.toHaveLength(0); }); - it('should capture a message', () => - new Promise(resolve => { - const options = getDefaultBrowserClientOptions({ - beforeSend: event => { - expect(event.message).toBe('test'); - expect(event.exception).toBeUndefined(); - resolve(); - return event; - }, - dsn, - }); - setCurrentClient(new BrowserClient(options)); - captureMessage('test'); - })); + it('should capture a message', done => { + const options = getDefaultBrowserClientOptions({ + beforeSend: (event: Event): Event | null => { + expect(event.level).toBe('info'); + expect(event.message).toBe('test'); + expect(event.exception).toBeUndefined(); + done(); + return event; + }, + dsn, + }); + setCurrentClient(new BrowserClient(options)); + captureMessage('test'); + }); it('should capture an event', () => new Promise(resolve => { diff --git a/packages/core/src/integrations/captureconsole.ts b/packages/core/src/integrations/captureconsole.ts index 563296511fd6..c180dcbe99e7 100644 --- a/packages/core/src/integrations/captureconsole.ts +++ b/packages/core/src/integrations/captureconsole.ts @@ -11,12 +11,24 @@ import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; interface CaptureConsoleOptions { levels?: string[]; + + // TODO(v9): Flip default value to `true` and adjust JSDoc! + /** + * By default, Sentry will mark captured console messages as unhandled. + * Set this to `true` if you want to mark them as handled instead. + * + * Note: in v9 of the SDK, this option will default to `true`, meaning the default behavior will change to mark console messages as handled. + * @default false + */ + handled?: boolean; } const INTEGRATION_NAME = 'CaptureConsole'; const _captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { const levels = options.levels || CONSOLE_LEVELS; + // TODO(v9): Flip default value to `true` + const handled = !!options.handled; return { name: INTEGRATION_NAME, @@ -30,7 +42,7 @@ const _captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { return; } - consoleHandler(args, level); + consoleHandler(args, level, handled); }); }, }; @@ -41,7 +53,7 @@ const _captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { */ export const captureConsoleIntegration = defineIntegration(_captureConsoleIntegration); -function consoleHandler(args: unknown[], level: string): void { +function consoleHandler(args: unknown[], level: string, handled: boolean): void { const captureContext: CaptureContext = { level: severityLevelFromString(level), extra: { @@ -54,7 +66,7 @@ function consoleHandler(args: unknown[], level: string): void { event.logger = 'console'; addExceptionMechanism(event, { - handled: false, + handled, type: 'console', }); diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index c683ad0d2d54..e1d89c1d067b 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -60,7 +60,10 @@ export class ServerRuntimeClient< * @inheritDoc */ public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromUnknownInput(this, this._options.stackParser, exception, hint)); + const event = eventFromUnknownInput(this, this._options.stackParser, exception, hint); + event.level = 'error'; + + return resolvedSyncPromise(event); } /** diff --git a/packages/core/src/types-hoist/instrument.ts b/packages/core/src/types-hoist/instrument.ts index f0b239e86b14..420482579dd9 100644 --- a/packages/core/src/types-hoist/instrument.ts +++ b/packages/core/src/types-hoist/instrument.ts @@ -32,6 +32,9 @@ export interface HandlerDataXhr { xhr: SentryWrappedXMLHttpRequest; startTimestamp?: number; endTimestamp?: number; + error?: unknown; + // This is to be consumed by the HttpClient integration + virtualError?: unknown; } interface SentryFetchData { @@ -56,6 +59,8 @@ export interface HandlerDataFetch { headers: WebFetchHeaders; }; error?: unknown; + // This is to be consumed by the HttpClient integration + virtualError?: unknown; } export interface HandlerDataDom { diff --git a/packages/core/src/utils-hoist/error.ts b/packages/core/src/utils-hoist/error.ts index 03fc404656dc..622aaff9cf80 100644 --- a/packages/core/src/utils-hoist/error.ts +++ b/packages/core/src/utils-hoist/error.ts @@ -7,7 +7,10 @@ export class SentryError extends Error { public logLevel: ConsoleLevel; - public constructor(public message: string, logLevel: ConsoleLevel = 'warn') { + public constructor( + public message: string, + logLevel: ConsoleLevel = 'warn', + ) { super(message); this.name = new.target.prototype.constructor.name; diff --git a/packages/core/src/utils-hoist/eventbuilder.ts b/packages/core/src/utils-hoist/eventbuilder.ts index 42d23927f081..84d6e722ad7b 100644 --- a/packages/core/src/utils-hoist/eventbuilder.ts +++ b/packages/core/src/utils-hoist/eventbuilder.ts @@ -193,6 +193,7 @@ export function eventFromMessage( }, ], }; + addExceptionMechanism(event, { synthetic: true }); } } diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/utils-hoist/instrument/fetch.ts index 39c8862ba618..954ab50a7536 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/utils-hoist/instrument/fetch.ts @@ -48,6 +48,15 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void { return function (...args: any[]): void { + // We capture the error right here and not in the Promise error callback because Safari (and probably other + // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless. + + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the error, that was caused by your fetch call did not + // have a stack trace, so the SDK backfilled the stack trace so + // you can see which fetch call failed. + const virtualError = new Error(); + const { method, url } = parseFetchArgs(args); const handlerData: HandlerDataFetch = { args, @@ -56,6 +65,8 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat url, }, startTimestamp: timestampInSeconds() * 1000, + // // Adding the error to be able to fingerprint the failed fetch event in HttpClient instrumentation + virtualError, }; // if there is no callback, fetch is instrumented directly @@ -65,15 +76,6 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat }); } - // We capture the stack right here and not in the Promise error callback because Safari (and probably other - // browsers too) will wipe the stack trace up to this point, only leaving us with this file which is useless. - - // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the error, that was caused by your fetch call did not - // have a stack trace, so the SDK backfilled the stack trace so - // you can see which fetch call failed. - const virtualStackTrace = new Error().stack; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(GLOBAL_OBJ, args).then( async (response: Response) => { @@ -101,7 +103,7 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat // it means the error, that was caused by your fetch call did not // have a stack trace, so the SDK backfilled the stack trace so // you can see which fetch call failed. - error.stack = virtualStackTrace; + error.stack = virtualError.stack; addNonEnumerableProperty(error, 'framesToPop', 1); } diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils-hoist/object.ts index c18247a62f55..7d779cf6e211 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils-hoist/object.ts @@ -109,9 +109,7 @@ export function urlEncode(object: { [key: string]: any }): string { * @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor * an Error. */ -export function convertToPlainObject( - value: V, -): +export function convertToPlainObject(value: V): | { [ownProps: string]: unknown; type: string; diff --git a/packages/core/test/lib/integrations/captureconsole.test.ts b/packages/core/test/lib/integrations/captureconsole.test.ts index 3c646d378f88..4d480757fff1 100644 --- a/packages/core/test/lib/integrations/captureconsole.test.ts +++ b/packages/core/test/lib/integrations/captureconsole.test.ts @@ -305,29 +305,78 @@ describe('CaptureConsole setup', () => { }).not.toThrow(); }); - it("marks captured exception's mechanism as unhandled", () => { - // const addExceptionMechanismSpy = jest.spyOn(utils, 'addExceptionMechanism'); + describe('exception mechanism', () => { + // TODO (v9): Flip this below after adjusting the default value for `handled` in the integration + it("marks captured exception's mechanism as unhandled by default", () => { + const captureConsole = captureConsoleIntegration({ levels: ['error'] }); + captureConsole.setup?.(mockClient); - const captureConsole = captureConsoleIntegration({ levels: ['error'] }); - captureConsole.setup?.(mockClient); + const someError = new Error('some error'); + GLOBAL_OBJ.console.error(someError); - const someError = new Error('some error'); - GLOBAL_OBJ.console.error(someError); + const addedEventProcessor = (mockScope.addEventProcessor as jest.Mock).mock.calls[0][0]; + const someEvent: Event = { + exception: { + values: [{}], + }, + }; + addedEventProcessor(someEvent); - const addedEventProcessor = (mockScope.addEventProcessor as jest.Mock).mock.calls[0][0]; - const someEvent: Event = { - exception: { - values: [{}], - }, - }; - addedEventProcessor(someEvent); + expect(captureException).toHaveBeenCalledTimes(1); + expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); - expect(captureException).toHaveBeenCalledTimes(1); - expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); + expect(someEvent.exception?.values?.[0]?.mechanism).toEqual({ + handled: false, + type: 'console', + }); + }); + + it("marks captured exception's mechanism as handled if set in the options", () => { + const captureConsole = captureConsoleIntegration({ levels: ['error'], handled: true }); + captureConsole.setup?.(mockClient); - expect(someEvent.exception?.values?.[0]?.mechanism).toEqual({ - handled: false, - type: 'console', + const someError = new Error('some error'); + GLOBAL_OBJ.console.error(someError); + + const addedEventProcessor = (mockScope.addEventProcessor as jest.Mock).mock.calls[0][0]; + const someEvent: Event = { + exception: { + values: [{}], + }, + }; + addedEventProcessor(someEvent); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); + + expect(someEvent.exception?.values?.[0]?.mechanism).toEqual({ + handled: true, + type: 'console', + }); + }); + + it("marks captured exception's mechanism as unhandled if set in the options", () => { + const captureConsole = captureConsoleIntegration({ levels: ['error'], handled: false }); + captureConsole.setup?.(mockClient); + + const someError = new Error('some error'); + GLOBAL_OBJ.console.error(someError); + + const addedEventProcessor = (mockScope.addEventProcessor as jest.Mock).mock.calls[0][0]; + const someEvent: Event = { + exception: { + values: [{}], + }, + }; + addedEventProcessor(someEvent); + + expect(captureException).toHaveBeenCalledTimes(1); + expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); + + expect(someEvent.exception?.values?.[0]?.mechanism).toEqual({ + handled: false, + type: 'console', + }); }); }); }); diff --git a/packages/core/test/lib/serverruntimeclient.test.ts b/packages/core/test/lib/serverruntimeclient.test.ts index 40be09bf011c..bdf1c5242b80 100644 --- a/packages/core/test/lib/serverruntimeclient.test.ts +++ b/packages/core/test/lib/serverruntimeclient.test.ts @@ -154,4 +154,52 @@ describe('ServerRuntimeClient', () => { expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); }); }); + + describe('captureException', () => { + it('sends an exception event with level error', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + client = new ServerRuntimeClient(options); + + const sendEnvelopeSpy = jest.spyOn(client, 'sendEnvelope'); + + client.captureException(new Error('foo')); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + expect.objectContaining({ + level: 'error', + }), + ], + ], + ]); + }); + }); + + describe('captureMessage', () => { + it('sends a message event with level info', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + client = new ServerRuntimeClient(options); + + const sendEnvelopeSpy = jest.spyOn(client, 'sendEnvelope'); + + client.captureMessage('foo'); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + expect.objectContaining({ + level: 'info', + }), + ], + ], + ]); + }); + }); }); diff --git a/packages/core/test/utils-hoist/eventbuilder.test.ts b/packages/core/test/utils-hoist/eventbuilder.test.ts index afc193a343e3..2aea3b6192d9 100644 --- a/packages/core/test/utils-hoist/eventbuilder.test.ts +++ b/packages/core/test/utils-hoist/eventbuilder.test.ts @@ -1,5 +1,5 @@ import type { Client } from '../../src/types-hoist'; -import { eventFromUnknownInput } from '../../src/utils-hoist/eventbuilder'; +import { eventFromMessage, eventFromUnknownInput } from '../../src/utils-hoist/eventbuilder'; import { nodeStackLineParser } from '../../src/utils-hoist/node-stack-trace'; import { createStackParser } from '../../src/utils-hoist/stacktrace'; @@ -154,3 +154,63 @@ describe('eventFromUnknownInput', () => { expect(event.exception?.values?.[0]?.value).toBe('Object captured as exception with keys: foo, prop'); }); }); + +describe('eventFromMessage', () => { + it('creates an event from a string message', () => { + const event = eventFromMessage(stackParser, 'Test Message'); + expect(event).toEqual({ + event_id: undefined, // this is undefined because the hint isn't passed + level: 'info', + message: 'Test Message', + }); + }); + + it('attaches a synthetic exception if passed and `attachStackTrace` is true', () => { + const syntheticException = new Error('Test Message'); + const event = eventFromMessage( + stackParser, + 'Test Message', + 'info', + { syntheticException, event_id: '123abc' }, + true, + ); + + expect(event).toEqual({ + event_id: '123abc', + exception: { + values: [ + { + mechanism: { + handled: true, + synthetic: true, + type: 'generic', + }, + stacktrace: { + frames: expect.any(Array), + }, + value: 'Test Message', + }, + ], + }, + level: 'info', + message: 'Test Message', + }); + }); + + it("doesn't attach a synthetic exception if `attachStackTrace` is false", () => { + const syntheticException = new Error('Test Message'); + const event = eventFromMessage( + stackParser, + 'Test Message', + 'info', + { syntheticException, event_id: '123abc' }, + false, + ); + + expect(event).toEqual({ + event_id: '123abc', + level: 'info', + message: 'Test Message', + }); + }); +}); diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index 937eae2893f8..f9f3006c34c4 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -101,6 +101,7 @@ snapshot[`captureException 1`] = ` }, ], }, + level: "error", platform: "javascript", sdk: { integrations: [ @@ -134,6 +135,7 @@ snapshot[`captureMessage 1`] = ` { category: "sentry.event", event_id: "{{id}}", + level: "error", message: "Error: Some unhandled error", timestamp: 0, }, @@ -204,6 +206,7 @@ snapshot[`captureMessage twice 1`] = ` { category: "sentry.event", event_id: "{{id}}", + level: "error", message: "Error: Some unhandled error", timestamp: 0, }, @@ -281,6 +284,7 @@ snapshot[`captureMessage twice 2`] = ` { category: "sentry.event", event_id: "{{id}}", + level: "error", message: "Error: Some unhandled error", timestamp: 0, }, diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts index b08bdad5e9ab..810df8005c48 100644 --- a/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts @@ -5,7 +5,9 @@ interface FunctionComponent { } interface ClassComponent { - new (...args: unknown[]): { + new ( + ...args: unknown[] + ): { props?: unknown; render(...args: unknown[]): unknown; }; diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index c3d7b499fabb..855700c13dd6 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -12,7 +12,7 @@ export type ValueInjectionLoaderOptions = { // This regex is shamelessly stolen from: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/7f984482c73e4284e8b12a08dfedf23b5a82f0af/packages/bundler-plugin-core/src/index.ts#L535-L539 const SKIP_COMMENT_AND_DIRECTIVE_REGEX = // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. - // biome-ignore lint/nursery/useRegexLiterals: No user input + // biome-ignore lint/complexity/useRegexLiterals: No user input new RegExp('^(?:\\s*|/\\*(?:.|\\r|\\n)*?\\*/|//.*[\\n\\r])*(?:"[^"]*";?|\'[^\']*\';?)?'); /** diff --git a/packages/nitro-utils/package.json b/packages/nitro-utils/package.json index a81d03a3ea4c..c893d643579b 100644 --- a/packages/nitro-utils/package.json +++ b/packages/nitro-utils/package.json @@ -6,6 +6,7 @@ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro-utils", "author": "Sentry", "license": "MIT", + "private": true, "engines": { "node": ">=16.20" }, @@ -35,9 +36,6 @@ ] } }, - "publishConfig": { - "access": "public" - }, "dependencies": { "@sentry/core": "8.44.0" }, @@ -55,7 +53,6 @@ "build:dev:watch": "run-p build:transpile:watch build:types:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", - "build:tarball": "npm pack", "clean": "rimraf build coverage sentry-internal-nitro-utils-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index b93fcfd66612..f0cef4d60831 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -14,6 +14,7 @@ import { } from '@sentry/core'; import { NODE_VERSION } from '../../nodeVersion'; import type { NodeClient } from '../../sdk/client'; +import { isDebuggerEnabled } from '../../utils/debug'; import type { AnrIntegrationOptions, WorkerStartData } from './common'; const { isPromise } = types; @@ -98,9 +99,14 @@ const _anrIntegration = ((options: Partial = {}) => { }); } }, - setup(initClient: NodeClient) { + async setup(initClient: NodeClient) { client = initClient; + if (options.captureStackTrace && (await isDebuggerEnabled())) { + logger.warn('ANR captureStackTrace has been disabled because the debugger was already enabled'); + options.captureStackTrace = false; + } + // setImmediate is used to ensure that all other integrations have had their setup called first. // This allows us to call into all integrations to fetch the full context setImmediate(() => this.startWorker()); diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node/src/integrations/local-variables/local-variables-async.ts index 89d92e46bd59..e1e0ebadf755 100644 --- a/packages/node/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node/src/integrations/local-variables/local-variables-async.ts @@ -2,6 +2,7 @@ import { Worker } from 'node:worker_threads'; import type { Event, EventHint, Exception, IntegrationFn } from '@sentry/core'; import { defineIntegration, logger } from '@sentry/core'; import type { NodeClient } from '../../sdk/client'; +import { isDebuggerEnabled } from '../../utils/debug'; import type { FrameVariables, LocalVariablesIntegrationOptions, LocalVariablesWorkerArgs } from './common'; import { LOCAL_VARIABLES_KEY, functionNamesMatch } from './common'; @@ -101,13 +102,18 @@ export const localVariablesAsyncIntegration = defineIntegration((( return { name: 'LocalVariablesAsync', - setup(client: NodeClient) { + async setup(client: NodeClient) { const clientOptions = client.getOptions(); if (!clientOptions.includeLocalVariables) { return; } + if (await isDebuggerEnabled()) { + logger.warn('Local variables capture has been disabled because the debugger was already enabled'); + return; + } + const options: LocalVariablesWorkerArgs = { ...integrationOptions, debug: logger.isEnabled(), diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index 4de0fe8aa478..3416dbf47347 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -3,6 +3,7 @@ import type { Event, Exception, IntegrationFn, StackFrame, StackParser } from '@ import { LRUMap, defineIntegration, getClient, logger } from '@sentry/core'; import { NODE_MAJOR } from '../../nodeVersion'; import type { NodeClient } from '../../sdk/client'; +import { isDebuggerEnabled } from '../../utils/debug'; import type { FrameVariables, LocalVariablesIntegrationOptions, @@ -289,7 +290,7 @@ const _localVariablesSyncIntegration = (( return { name: INTEGRATION_NAME, - setupOnce() { + async setupOnce() { const client = getClient(); const clientOptions = client?.getOptions(); @@ -306,6 +307,11 @@ const _localVariablesSyncIntegration = (( return; } + if (await isDebuggerEnabled()) { + logger.warn('Local variables capture has been disabled because the debugger was already enabled'); + return; + } + AsyncSession.create(sessionOverride).then( session => { function handlePaused( diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index b51237a328ea..74f509ac42e7 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -109,13 +109,10 @@ export class NodeClient extends ServerRuntimeClient { this._flushOutcomes(); }; - this._clientReportInterval = setInterval( - () => { - DEBUG_BUILD && logger.log('Flushing client reports based on interval.'); - this._flushOutcomes(); - }, - clientOptions.clientReportFlushInterval ?? DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS, - ) + this._clientReportInterval = setInterval(() => { + DEBUG_BUILD && logger.log('Flushing client reports based on interval.'); + this._flushOutcomes(); + }, clientOptions.clientReportFlushInterval ?? DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS) // Unref is critical for not preventing the process from exiting because the interval is active. .unref(); diff --git a/packages/node/src/utils/debug.ts b/packages/node/src/utils/debug.ts new file mode 100644 index 000000000000..71df5e761230 --- /dev/null +++ b/packages/node/src/utils/debug.ts @@ -0,0 +1,18 @@ +let cachedDebuggerEnabled: boolean | undefined; + +/** + * Was the debugger enabled when this function was first called? + */ +export async function isDebuggerEnabled(): Promise { + if (cachedDebuggerEnabled === undefined) { + try { + // Node can be built without inspector support + const inspector = await import('node:inspector'); + cachedDebuggerEnabled = !!inspector.url(); + } catch (_) { + cachedDebuggerEnabled = false; + } + } + + return cachedDebuggerEnabled; +} diff --git a/packages/nuxt/build.config.ts b/packages/nuxt/build.config.ts index 4cb00345dc43..5e69aa734a40 100644 --- a/packages/nuxt/build.config.ts +++ b/packages/nuxt/build.config.ts @@ -1,7 +1,4 @@ import { defineBuildConfig } from 'unbuild'; // Build Config for the Nuxt Module Builder: https://github.com/nuxt/module-builder -export default defineBuildConfig({ - // The devDependency "@sentry-internal/nitro-utils" triggers "Inlined implicit external", but it's not external - failOnWarn: false, -}); +export default defineBuildConfig({}); diff --git a/packages/nuxt/generate-build-stubs.bash b/packages/nuxt/generate-build-stubs.bash new file mode 100644 index 000000000000..914468f0c4a9 --- /dev/null +++ b/packages/nuxt/generate-build-stubs.bash @@ -0,0 +1,19 @@ +# The Nuxt package is built in 2 steps and the nuxt-module-builder shows a warning if one of the files specified in the package.json is missing. +# unbuild checks for this: https://github.com/unjs/unbuild/blob/8c647ec005a02f852e56aeef6076a35eede17df1/src/validate.ts#L81 + +# The runtime folder (which is built with the nuxt-module-builder) is separate from the rest of the package and therefore we can ignore those warnings +# as those files are generated in the other build step. + +# Create the directories if they do not exist +mkdir -p build/cjs +mkdir -p build/esm +mkdir -p build/types + +# Write files if they do not exist +[ ! -f build/cjs/index.server.js ] && echo "module.exports = {}" > build/cjs/index.server.js +[ ! -f build/cjs/index.client.js ] && echo "module.exports = {}" > build/cjs/index.client.js +[ ! -f build/esm/index.server.js ] && echo "export {}" > build/esm/index.server.js +[ ! -f build/esm/index.client.js ] && echo "export {}" > build/esm/index.client.js +[ ! -f build/types/index.types.d.ts ] && echo "export {}" > build/types/index.types.d.ts + +echo "Created build stubs for missing files" diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index e2293f05ccd4..837c50740c40 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -53,13 +53,12 @@ }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", - "@sentry-internal/nitro-utils": "8.44.0", "nuxt": "^3.13.2" }, "scripts": { "build": "run-s build:types build:transpile", "build:dev": "yarn build", - "build:nuxt-module": "nuxt-module-build build --outDir build/module", + "build:nuxt-module": "bash ./generate-build-stubs.bash && nuxt-module-build build --outDir build/module", "build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:nuxt-module", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", diff --git a/packages/nuxt/rollup.npm.config.mjs b/packages/nuxt/rollup.npm.config.mjs index d124ba8a7844..a94a4b5af253 100644 --- a/packages/nuxt/rollup.npm.config.mjs +++ b/packages/nuxt/rollup.npm.config.mjs @@ -15,20 +15,4 @@ export default [ }, }), ), - /* The Nuxt module plugins are also built with the @nuxt/module-builder. - This rollup setup is still left here for an easier switch between the setups while - manually testing different built outputs (module-builder vs. rollup only) */ - ...makeNPMConfigVariants( - makeBaseNPMConfig({ - entrypoints: ['src/runtime/plugins/sentry.client.ts', 'src/runtime/plugins/sentry.server.ts'], - - packageSpecificConfig: { - external: ['nuxt/app', 'nitropack/runtime', 'h3'], - output: { - // Preserve the original file structure (i.e., so that everything is still relative to `src`) - entryFileNames: 'runtime/[name].js', - }, - }, - }), - ), ]; diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index f85e69883bb8..b748115f5c81 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,7 +1,15 @@ -import { patchEventHandler } from '@sentry-internal/nitro-utils'; -import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core'; +import { + GLOBAL_OBJ, + flush, + getClient, + getDefaultIsolationScope, + getIsolationScope, + logger, + vercelWaitUntil, + withIsolationScope, +} from '@sentry/core'; import * as Sentry from '@sentry/node'; -import { H3Error } from 'h3'; +import { type EventHandler, H3Error } from 'h3'; import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { addSentryTracingMetaTags, extractErrorContext } from '../utils'; @@ -66,3 +74,27 @@ async function flushWithTimeout(): Promise { isDebug && logger.log('Error while flushing events:\n', e); } } + +// copied from '@sentry-internal/nitro-utils' - the nuxt-module-builder does not inline devDependencies +function patchEventHandler(handler: EventHandler): EventHandler { + return new Proxy(handler, { + async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { + const isolationScope = getIsolationScope(); + const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + logger.log( + `Patched h3 event handler. ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, + ); + + return withIsolationScope(newIsolationScope, async () => { + try { + return await handlerTarget.apply(handlerThisArg, handlerArgs); + } finally { + await flushIfServerless(); + } + }); + }, + }); +} diff --git a/packages/profiling-node/src/cpu_profiler.ts b/packages/profiling-node/src/cpu_profiler.ts index 4897745ededa..ed4ad83e7b31 100644 --- a/packages/profiling-node/src/cpu_profiler.ts +++ b/packages/profiling-node/src/cpu_profiler.ts @@ -172,6 +172,12 @@ class Bindings implements V8CpuProfilerBindings { return; } + if (typeof PrivateCpuProfilerBindings.startProfiling !== 'function') { + DEBUG_BUILD && + logger.log('[Profiling] Native startProfiling function is not available, ignoring call to startProfiling.'); + return; + } + return PrivateCpuProfilerBindings.startProfiling(name); } @@ -187,6 +193,12 @@ class Bindings implements V8CpuProfilerBindings { return null; } + if (typeof PrivateCpuProfilerBindings.stopProfiling !== 'function') { + DEBUG_BUILD && + logger.log('[Profiling] Native stopProfiling function is not available, ignoring call to stopProfiling.'); + return null; + } + return PrivateCpuProfilerBindings.stopProfiling( name, format as unknown as any, diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 8aed8eb3a305..c9b9fe12056d 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -85,7 +85,7 @@ function setupAutomatedSpanProfiling(client: NodeClient): void { // Unref timeout so it doesn't keep the process alive. timeout.unref(); - getCurrentScope().setContext('profile', { profile_id }); + getIsolationScope().setContext('profile', { profile_id }); spanToProfileIdMap.set(span, profile_id); } }); diff --git a/packages/profiling-node/src/types.ts b/packages/profiling-node/src/types.ts index 2423ca94651b..9b8f039b3c95 100644 --- a/packages/profiling-node/src/types.ts +++ b/packages/profiling-node/src/types.ts @@ -52,15 +52,15 @@ export interface RawChunkCpuProfile extends BaseProfile { } export interface PrivateV8CpuProfilerBindings { - startProfiling(name: string): void; + startProfiling?: (name: string) => void; - stopProfiling( + stopProfiling?( name: string, format: ProfileFormat.THREAD, threadId: number, collectResources: boolean, ): RawThreadCpuProfile | null; - stopProfiling( + stopProfiling?( name: string, format: ProfileFormat.CHUNK, threadId: number, diff --git a/packages/profiling-node/test/cpu_profiler.test.ts b/packages/profiling-node/test/cpu_profiler.test.ts index 4db0b98891a3..1719b570e28e 100644 --- a/packages/profiling-node/test/cpu_profiler.test.ts +++ b/packages/profiling-node/test/cpu_profiler.test.ts @@ -69,28 +69,28 @@ const assertValidMeasurements = (measurement: RawThreadCpuProfile['measurements' describe('Private bindings', () => { it('does not crash if collect resources is false', async () => { - PrivateCpuProfilerBindings.startProfiling('profiled-program'); + PrivateCpuProfilerBindings.startProfiling!('profiled-program'); await wait(100); expect(() => { - const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, 0, false); + const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', 0, 0, false); if (!profile) throw new Error('No profile'); }).not.toThrow(); }); it('throws if invalid format is supplied', async () => { - PrivateCpuProfilerBindings.startProfiling('profiled-program'); + PrivateCpuProfilerBindings.startProfiling!('profiled-program'); await wait(100); expect(() => { - const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', Number.MAX_SAFE_INTEGER, 0, false); + const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', Number.MAX_SAFE_INTEGER, 0, false); if (!profile) throw new Error('No profile'); }).toThrow('StopProfiling expects a valid format type as second argument.'); }); it('collects resources', async () => { - PrivateCpuProfilerBindings.startProfiling('profiled-program'); + PrivateCpuProfilerBindings.startProfiling!('profiled-program'); await wait(100); - const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, 0, true); + const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', 0, 0, true); if (!profile) throw new Error('No profile'); expect(profile.resources.length).toBeGreaterThan(0); @@ -104,10 +104,10 @@ describe('Private bindings', () => { }); it('does not collect resources', async () => { - PrivateCpuProfilerBindings.startProfiling('profiled-program'); + PrivateCpuProfilerBindings.startProfiling!('profiled-program'); await wait(100); - const profile = PrivateCpuProfilerBindings.stopProfiling('profiled-program', 0, 0, false); + const profile = PrivateCpuProfilerBindings.stopProfiling!('profiled-program', 0, 0, false); if (!profile) throw new Error('No profile'); expect(profile.resources.length).toBe(0); @@ -337,4 +337,27 @@ describe('Profiler bindings', () => { const hasDeoptimizedFrame = profile.frames.some(f => f.deopt_reasons && f.deopt_reasons.length > 0); expect(hasDeoptimizedFrame).toBe(true); }); + + it('does not crash if the native startProfiling function is not available', async () => { + const original = PrivateCpuProfilerBindings.startProfiling; + PrivateCpuProfilerBindings.startProfiling = undefined; + + expect(() => { + CpuProfilerBindings.startProfiling('profiled-program'); + }).not.toThrow(); + + PrivateCpuProfilerBindings.startProfiling = original; + }); + + it('does not crash if the native stopProfiling function is not available', async () => { + // eslint-disable-next-line @typescript-eslint/unbound-method + const original = PrivateCpuProfilerBindings.stopProfiling; + PrivateCpuProfilerBindings.stopProfiling = undefined; + + expect(() => { + CpuProfilerBindings.stopProfiling('profiled-program', 0); + }).not.toThrow(); + + PrivateCpuProfilerBindings.stopProfiling = original; + }); }); diff --git a/packages/react/package.json b/packages/react/package.json index 1ac944b84493..b031e7cb4576 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -67,8 +67,7 @@ "react-router-3": "npm:react-router@3.2.0", "react-router-4": "npm:react-router@4.1.0", "react-router-5": "npm:react-router@5.0.0", - "react-router-6": "npm:react-router@6.3.0", - "react-router-6.4": "npm:react-router@6.4.2", + "react-router-6": "npm:react-router@6.28.0", "redux": "^4.0.5" }, "scripts": { diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx index 7752e49d69a1..c17ea1bb190f 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -100,7 +100,13 @@ export function createV6CompatibleWrapCreateBrowserRouter< router.subscribe((state: RouterState) => { const location = state.location; if (state.historyAction === 'PUSH' || state.historyAction === 'POP') { - handleNavigation(location, routes, state.historyAction, version, undefined, basename); + handleNavigation({ + location, + routes, + navigationType: state.historyAction, + version, + basename, + }); } }); @@ -174,13 +180,14 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio return origUseRoutes; } - let isMountRenderPass: boolean = true; + const allRoutes: RouteObject[] = []; const SentryRoutes: React.FC<{ children?: React.ReactNode; routes: RouteObject[]; locationArg?: Partial | string; }> = (props: { children?: React.ReactNode; routes: RouteObject[]; locationArg?: Partial | string }) => { + const isMountRenderPass = React.useRef(true); const { routes, locationArg } = props; const Routes = origUseRoutes(routes, locationArg); @@ -198,11 +205,21 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio const normalizedLocation = typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; - if (isMountRenderPass) { - updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes); - isMountRenderPass = false; + if (isMountRenderPass.current) { + routes.forEach(route => { + allRoutes.push(...getChildRoutesRecursively(route)); + }); + + updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes, undefined, undefined, allRoutes); + isMountRenderPass.current = false; } else { - handleNavigation(normalizedLocation, routes, navigationType, version); + handleNavigation({ + location: normalizedLocation, + routes, + navigationType, + version, + allRoutes, + }); } }, [navigationType, stableLocationParam]); @@ -215,14 +232,17 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio }; } -export function handleNavigation( - location: Location, - routes: RouteObject[], - navigationType: Action, - version: V6CompatibleVersion, - matches?: AgnosticDataRouteMatch, - basename?: string, -): void { +export function handleNavigation(opts: { + location: Location; + routes: RouteObject[]; + navigationType: Action; + version: V6CompatibleVersion; + matches?: AgnosticDataRouteMatch; + basename?: string; + allRoutes?: RouteObject[]; +}): void { + const { location, routes, navigationType, version, matches, basename, allRoutes } = opts; + const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename); const client = getClient(); @@ -231,7 +251,18 @@ export function handleNavigation( } if ((navigationType === 'PUSH' || navigationType === 'POP') && branches) { - const [name, source] = getNormalizedName(routes, location, branches, basename); + let name, + source: TransactionSource = 'url'; + const isInDescendantRoute = locationIsInsideDescendantRoute(location, allRoutes || routes); + + if (isInDescendantRoute) { + name = prefixWithSlash(rebuildRoutePathFromAllRoutes(allRoutes || routes, location)); + source = 'route'; + } + + if (!isInDescendantRoute || !name) { + [name, source] = getNormalizedName(routes, location, branches, basename); + } startBrowserTracingNavigationSpan(client, { name, @@ -286,12 +317,91 @@ function sendIndexPath(pathBuilder: string, pathname: string, basename: string): return [formattedPath, 'route']; } -function pathEndsWithWildcard(path: string, branch: RouteMatch): boolean { - return (path.slice(-2) === '/*' && branch.route.children && branch.route.children.length > 0) || false; +function pathEndsWithWildcard(path: string): boolean { + return path.endsWith('*'); } function pathIsWildcardAndHasChildren(path: string, branch: RouteMatch): boolean { - return (path === '*' && branch.route.children && branch.route.children.length > 0) || false; + return (pathEndsWithWildcard(path) && branch.route.children && branch.route.children.length > 0) || false; +} + +function routeIsDescendant(route: RouteObject): boolean { + return !!(!route.children && route.element && route.path && route.path.endsWith('/*')); +} + +function locationIsInsideDescendantRoute(location: Location, routes: RouteObject[]): boolean { + const matchedRoutes = _matchRoutes(routes, location) as RouteMatch[]; + + if (matchedRoutes) { + for (const match of matchedRoutes) { + if (routeIsDescendant(match.route) && pickSplat(match)) { + return true; + } + } + } + + return false; +} + +function getChildRoutesRecursively(route: RouteObject, allRoutes: RouteObject[] = []): RouteObject[] { + if (route.children && !route.index) { + route.children.forEach(child => { + allRoutes.push(...getChildRoutesRecursively(child, allRoutes)); + }); + } + + allRoutes.push(route); + + return allRoutes; +} + +function pickPath(match: RouteMatch): string { + return trimWildcard(match.route.path || ''); +} + +function pickSplat(match: RouteMatch): string { + return match.params['*'] || ''; +} + +function trimWildcard(path: string): string { + return path[path.length - 1] === '*' ? path.slice(0, -1) : path; +} + +function trimSlash(path: string): string { + return path[path.length - 1] === '/' ? path.slice(0, -1) : path; +} + +function prefixWithSlash(path: string): string { + return path[0] === '/' ? path : `/${path}`; +} + +function rebuildRoutePathFromAllRoutes(allRoutes: RouteObject[], location: Location): string { + const matchedRoutes = _matchRoutes(allRoutes, location) as RouteMatch[]; + + if (!matchedRoutes || matchedRoutes.length === 0) { + return ''; + } + + for (const match of matchedRoutes) { + if (match.route.path && match.route.path !== '*') { + const path = pickPath(match); + const strippedPath = stripBasenameFromPathname(location.pathname, prefixWithSlash(match.pathnameBase)); + + return trimSlash( + trimSlash(path || '') + + prefixWithSlash( + rebuildRoutePathFromAllRoutes( + allRoutes.filter(route => route !== match.route), + { + pathname: strippedPath, + }, + ), + ), + ); + } + } + + return ''; } function getNormalizedName( @@ -321,7 +431,7 @@ function getNormalizedName( pathBuilder += newPath; // If the path matches the current location, return the path - if (basename + branch.pathname === location.pathname) { + if (location.pathname.endsWith(basename + branch.pathname)) { if ( // If the route defined on the element is something like // Product} /> @@ -330,13 +440,13 @@ function getNormalizedName( // eslint-disable-next-line deprecation/deprecation getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) && // We should not count wildcard operators in the url segments calculation - pathBuilder.slice(-2) !== '/*' + !pathEndsWithWildcard(pathBuilder) ) { return [(_stripBasename ? '' : basename) + newPath, 'route']; } // if the last character of the pathbuilder is a wildcard and there are children, remove the wildcard - if (pathEndsWithWildcard(pathBuilder, branch)) { + if (pathIsWildcardAndHasChildren(pathBuilder, branch)) { pathBuilder = pathBuilder.slice(0, -1); } @@ -347,7 +457,11 @@ function getNormalizedName( } } - return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url']; + const fallbackTransactionName = _stripBasename + ? stripBasenameFromPathname(location.pathname, basename) + : location.pathname || '/'; + + return [fallbackTransactionName, 'url']; } function updatePageloadTransaction( @@ -356,13 +470,25 @@ function updatePageloadTransaction( routes: RouteObject[], matches?: AgnosticDataRouteMatch, basename?: string, + allRoutes?: RouteObject[], ): void { const branches = Array.isArray(matches) ? matches : (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]); if (branches) { - const [name, source] = getNormalizedName(routes, location, branches, basename); + let name, + source: TransactionSource = 'url'; + const isInDescendantRoute = locationIsInsideDescendantRoute(location, allRoutes || routes); + + if (isInDescendantRoute) { + name = prefixWithSlash(rebuildRoutePathFromAllRoutes(allRoutes || routes, location)); + source = 'route'; + } + + if (!isInDescendantRoute || !name) { + [name, source] = getNormalizedName(routes, location, branches, basename); + } getCurrentScope().setTransactionName(name); @@ -387,9 +513,11 @@ export function createV6CompatibleWithSentryReactRouterRouting

= (props: P) => { + const isMountRenderPass = React.useRef(true); + const location = _useLocation(); const navigationType = _useNavigationType(); @@ -397,11 +525,21 @@ export function createV6CompatibleWithSentryReactRouterRouting

{ const routes = _createRoutesFromChildren(props.children) as RouteObject[]; - if (isMountRenderPass) { - updatePageloadTransaction(getActiveRootSpan(), location, routes); - isMountRenderPass = false; + if (isMountRenderPass.current) { + routes.forEach(route => { + allRoutes.push(...getChildRoutesRecursively(route)); + }); + + updatePageloadTransaction(getActiveRootSpan(), location, routes, undefined, undefined, allRoutes); + isMountRenderPass.current = false; } else { - handleNavigation(location, routes, navigationType, version); + handleNavigation({ + location, + routes, + navigationType, + version, + allRoutes, + }); } }, // `props.children` is purposely not included in the dependency array, because we do not want to re-run this effect diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx deleted file mode 100644 index 3ae6a69bdf56..000000000000 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ /dev/null @@ -1,676 +0,0 @@ -import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - createTransport, - getCurrentScope, - setCurrentClient, -} from '@sentry/core'; -import { render } from '@testing-library/react'; -import { Request } from 'node-fetch'; -import * as React from 'react'; -import { - Navigate, - RouterProvider, - createMemoryRouter, - createRoutesFromChildren, - matchRoutes, - useLocation, - useNavigationType, -} from 'react-router-6.4'; - -import { BrowserClient, wrapCreateBrowserRouter } from '../src'; -import { reactRouterV6BrowserTracingIntegration } from '../src/reactrouterv6'; -import type { CreateRouterFunction } from '../src/types'; - -beforeAll(() => { - // @ts-expect-error need to override global Request because it's not in the jest environment (even with an - // `@jest-environment jsdom` directive, for some reason) - global.Request = Request; -}); - -const mockStartBrowserTracingPageLoadSpan = jest.fn(); -const mockStartBrowserTracingNavigationSpan = jest.fn(); - -const mockRootSpan = { - updateName: jest.fn(), - setAttribute: jest.fn(), - getSpanJSON() { - return { op: 'pageload' }; - }, -}; - -jest.mock('@sentry/browser', () => { - const actual = jest.requireActual('@sentry/browser'); - return { - ...actual, - startBrowserTracingNavigationSpan: (...args: unknown[]) => { - mockStartBrowserTracingNavigationSpan(...args); - return actual.startBrowserTracingNavigationSpan(...args); - }, - startBrowserTracingPageLoadSpan: (...args: unknown[]) => { - mockStartBrowserTracingPageLoadSpan(...args); - return actual.startBrowserTracingPageLoadSpan(...args); - }, - }; -}); - -jest.mock('@sentry/core', () => { - const actual = jest.requireActual('@sentry/core'); - return { - ...actual, - getRootSpan: () => { - return mockRootSpan; - }, - }; -}); - -describe('reactRouterV6BrowserTracingIntegration (v6.4)', () => { - function createMockBrowserClient(): BrowserClient { - return new BrowserClient({ - integrations: [], - tracesSampleRate: 1, - transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), - stackParser: () => [], - }); - } - - beforeEach(() => { - jest.clearAllMocks(); - getCurrentScope().setClient(undefined); - }); - - describe('wrapCreateBrowserRouter', () => { - it('starts a pageload transaction', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element:

TEST
, - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6', - }, - }); - }); - - it("updates the scope's `transactionName` on a pageload", () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element:
TEST
, - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(getCurrentScope().getScopeData().transactionName).toEqual('/'); - }); - - it('starts a navigation transaction', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'about', - element:
About
, - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/about', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('works with nested routes', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'about', - element:
About
, - children: [ - { - path: 'us', - element:
Us
, - }, - ], - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/about/us', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('works with parameterized paths', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'about', - element:
About
, - children: [ - { - path: ':page', - element:
Page
, - }, - ], - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/about/:page', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('works with paths with multiple parameters', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'stores', - element:
Stores
, - children: [ - { - path: ':storeId', - element:
Store
, - children: [ - { - path: 'products', - element:
Products
, - children: [ - { - path: ':productId', - element:
Product
, - }, - ], - }, - ], - }, - ], - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/stores/:storeId/products/:productId', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('updates pageload transaction to a parameterized route', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: 'about', - element:
About
, - children: [ - { - path: ':page', - element:
page
, - }, - ], - }, - ], - { - initialEntries: ['/about/us'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); - expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about/:page'); - expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - }); - - it('works with `basename` option', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'about', - element:
About
, - children: [ - { - path: 'us', - element:
Us
, - }, - ], - }, - ], - { - initialEntries: ['/app'], - basename: '/app', - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/app/about/us', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('works with parameterized paths and `basename`', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: ':orgId', - children: [ - { - path: 'users', - children: [ - { - path: ':userId', - element:
User
, - }, - ], - }, - ], - }, - ], - { - initialEntries: ['/admin'], - basename: '/admin', - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/admin/:orgId/users/:userId', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('strips `basename` from transaction names of parameterized paths', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - stripBasename: true, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: ':orgId', - children: [ - { - path: 'users', - children: [ - { - path: ':userId', - element:
User
, - }, - ], - }, - ], - }, - ], - { - initialEntries: ['/admin'], - basename: '/admin', - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/:orgId/users/:userId', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it('strips `basename` from transaction names of non-parameterized paths', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - stripBasename: true, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'about', - element:
About
, - children: [ - { - path: 'us', - element:
Us
, - }, - ], - }, - ], - { - initialEntries: ['/app'], - basename: '/app', - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); - expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { - name: '/about/us', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', - }, - }); - }); - - it("updates the scope's `transactionName` on a navigation", () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); - // eslint-disable-next-line deprecation/deprecation - const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); - - const router = sentryCreateBrowserRouter( - [ - { - path: '/', - element: , - }, - { - path: 'about', - element:
About
, - }, - ], - { - initialEntries: ['/'], - }, - ); - - // @ts-expect-error router is fine - render(); - - expect(getCurrentScope().getScopeData().transactionName).toEqual('/about'); - }); - }); -}); diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index b9cf4003c330..815b562f08f7 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -25,7 +25,7 @@ import { BrowserClient } from '../src'; import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, - wrapUseRoutes, + wrapUseRoutesV6, } from '../src/reactrouterv6'; const mockStartBrowserTracingPageLoadSpan = jest.fn(); @@ -491,6 +491,109 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); + it('works with descendant wildcard routes - pageload', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + const DetailsRoutes = () => ( + + Details} /> + + ); + + const ViewsRoutes = () => ( + + Views} /> + } /> + + ); + + const ProjectsRoutes = () => ( + + }> + No Match Page} /> + + ); + + render( + + + }> + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/projects/:projectId/views/:viewId/:detailId'); + expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('works with descendant wildcard routes - navigation', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + const DetailsRoutes = () => ( + + Details} /> + + ); + + const ViewsRoutes = () => ( + + Views} /> + } /> + + ); + + const ProjectsRoutes = () => ( + + }> + No Match Page} /> + + ); + + render( + + + } /> + }> + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/projects/:projectId/views/:viewId/:detailId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + it("updates the scope's `transactionName` on a navigation", () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -521,7 +624,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); - describe('wrapUseRoutes', () => { + describe('wrapUseRoutesV6', () => { it('starts a pageload transaction', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -536,8 +639,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -578,8 +680,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -613,8 +714,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -648,8 +748,7 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -686,8 +785,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -731,8 +830,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -782,8 +881,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -833,8 +932,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -890,8 +989,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -971,8 +1070,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -1037,6 +1136,150 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); + it('works with descendant wildcard routes - pageload', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); + + const DetailsRoutes = () => + wrappedUseRoutes([ + { + path: ':detailId', + element:
Details
, + }, + ]); + + const ViewsRoutes = () => + wrappedUseRoutes([ + { + index: true, + element:
Views
, + }, + { + path: 'views/:viewId/*', + element: , + }, + ]); + + const ProjectsRoutes = () => + wrappedUseRoutes([ + { + path: 'projects/:projectId/*', + element: , + }, + { + path: '*', + element:
No Match Page
, + }, + ]); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/*', + element: , + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingPageLoadSpan).toHaveBeenCalledTimes(1); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/projects/:projectId/views/:viewId/:detailId'); + expect(mockRootSpan.setAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + }); + + it('works with descendant wildcard routes - navigation', () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); + + const DetailsRoutes = () => + wrappedUseRoutes([ + { + path: ':detailId', + element:
Details
, + }, + ]); + + const ViewsRoutes = () => + wrappedUseRoutes([ + { + index: true, + element:
Views
, + }, + { + path: 'views/:viewId/*', + element: , + }, + ]); + + const ProjectsRoutes = () => + wrappedUseRoutes([ + { + path: 'projects/:projectId/*', + element: , + }, + { + path: '*', + element:
No Match Page
, + }, + ]); + + const Routes = () => + wrappedUseRoutes([ + { + index: true, + element: , + }, + { + path: '/*', + element: , + }, + ]); + + render( + + + , + ); + + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenCalledTimes(1); + expect(mockStartBrowserTracingNavigationSpan).toHaveBeenLastCalledWith(expect.any(BrowserClient), { + name: '/projects/:projectId/views/:viewId/:detailId', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6', + }, + }); + }); + it('does not add double slashes to URLS', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -1050,8 +1293,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -1109,8 +1352,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ @@ -1167,8 +1410,8 @@ describe('reactRouterV6BrowserTracingIntegration', () => { matchRoutes, }), ); - // eslint-disable-next-line deprecation/deprecation - const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const wrappedUseRoutes = wrapUseRoutesV6(useRoutes); const Routes = () => wrappedUseRoutes([ diff --git a/packages/remix/test/integration/test/server/utils/helpers.ts b/packages/remix/test/integration/test/server/utils/helpers.ts index 6ab89c7856ac..deec33e06b1e 100644 --- a/packages/remix/test/integration/test/server/utils/helpers.ts +++ b/packages/remix/test/integration/test/server/utils/helpers.ts @@ -54,7 +54,10 @@ class TestEnv { private _axiosConfig: AxiosRequestConfig | undefined = undefined; private _terminator: HttpTerminator; - public constructor(public readonly server: http.Server, public readonly url: string) { + public constructor( + public readonly server: http.Server, + public readonly url: string, + ) { this.server = server; this.url = url; this._terminator = createHttpTerminator({ server: this.server, gracefulTerminationTimeout: 0 }); @@ -236,19 +239,16 @@ class TestEnv { return false; }); - setTimeout( - () => { - nock.removeInterceptor(mock); + setTimeout(() => { + nock.removeInterceptor(mock); - nock.cleanAll(); + nock.cleanAll(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this._closeServer().then(() => { - resolve(reqCount); - }); - }, - options.timeout || 1000, - ); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this._closeServer().then(() => { + resolve(reqCount); + }); + }, options.timeout || 1000); }); } @@ -258,7 +258,10 @@ class TestEnv { } export class RemixTestEnv extends TestEnv { - private constructor(public readonly server: http.Server, public readonly url: string) { + private constructor( + public readonly server: http.Server, + public readonly url: string, + ) { super(server, url); } diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 2a4a5d1505c3..678390d1cc69 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -65,7 +65,7 @@ }, "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { - "@sentry-internal/rrweb": "2.30.0" + "@sentry-internal/rrweb": "2.31.0" }, "dependencies": { "@sentry-internal/replay": "8.44.0", diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index 0f7882c14cb4..7e856661690c 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -69,8 +69,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "8.44.0", - "@sentry-internal/rrweb": "2.30.0", - "@sentry-internal/rrweb-snapshot": "2.30.0", + "@sentry-internal/rrweb": "2.31.0", + "@sentry-internal/rrweb-snapshot": "2.31.0", "fflate": "^0.8.1", "jest-matcher-utils": "^29.0.0", "jsdom-worker": "^0.2.1" diff --git a/packages/replay-worker/src/worker.ts b/packages/replay-worker/src/worker.ts index e9da044b976b..e31356388d35 100644 --- a/packages/replay-worker/src/worker.ts +++ b/packages/replay-worker/src/worker.ts @@ -1,3 +1,3 @@ // This is replaced at build-time with the content from _worker.ts, wrapped as a string. // This is just a placeholder so that types etc. are correct. -export default ('' as string); +export default '' as string; diff --git a/yarn.lock b/yarn.lock index 47d5996cf30a..e2f483ec6322 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4393,47 +4393,59 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@biomejs/biome@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.4.0.tgz#b512e1e7a4f3ec0bc0aceaa99fab8eded2bd95c9" - integrity sha512-/rDlao6ra38nhxo4IYCqWCzfTJcpMk4YHjSVBI9yN/ifdhnzSwirL25xDVH7G9hZdNhpF9g78FaPJhFa9DX0Cw== +"@biomejs/biome@^1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-1.5.2.tgz#fdc194125a904ec69a87cb48b03141b6c070df66" + integrity sha512-LhycxGQBQLmfv6M3e4tMfn/XKcUWyduDYOlCEBrHXJ2mMth2qzYt1JWypkWp+XmU/7Hl2dKvrP4mZ5W44+nWZw== optionalDependencies: - "@biomejs/cli-darwin-arm64" "1.4.0" - "@biomejs/cli-darwin-x64" "1.4.0" - "@biomejs/cli-linux-arm64" "1.4.0" - "@biomejs/cli-linux-x64" "1.4.0" - "@biomejs/cli-win32-arm64" "1.4.0" - "@biomejs/cli-win32-x64" "1.4.0" - -"@biomejs/cli-darwin-arm64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.4.0.tgz#08e9e19ae72fd980be65307844a71cd7ba96f4f2" - integrity sha512-nBrtVRwr4IlTtxLOHwBwLv1sWvggf9/DnT5/ALIANJZOpoING6u8jHWipods69wK8kGa8Ld7iwHm3W5BrJJFFQ== + "@biomejs/cli-darwin-arm64" "1.5.2" + "@biomejs/cli-darwin-x64" "1.5.2" + "@biomejs/cli-linux-arm64" "1.5.2" + "@biomejs/cli-linux-arm64-musl" "1.5.2" + "@biomejs/cli-linux-x64" "1.5.2" + "@biomejs/cli-linux-x64-musl" "1.5.2" + "@biomejs/cli-win32-arm64" "1.5.2" + "@biomejs/cli-win32-x64" "1.5.2" + +"@biomejs/cli-darwin-arm64@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.5.2.tgz#fc744f6ac454ce18b1b36d306c77b2bdb216d6ae" + integrity sha512-3JVl08aHKsPyf0XL9SEj1lssIMmzOMAn2t1zwZKBiy/mcZdb0vuyMSTM5haMQ/90wEmrkYN7zux777PHEGrGiw== -"@biomejs/cli-darwin-x64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.4.0.tgz#ae04f06a4446fa718dfeba863af6250a0b4185e6" - integrity sha512-nny0VgOj3ksUGzU5GblgtQEvrAZFgFe1IJBoYOP978OQdDrg7BpS+GX5udfof87Dl4ZlHPRBU951ceHOxF7BTg== +"@biomejs/cli-darwin-x64@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.5.2.tgz#2439a338166b9acb6d56939bd9e8e6331ee43dcb" + integrity sha512-QAPW9rZb/AgucUx+ogMg+9eJNipQDqvabktC5Tx4Aqb/mFzS6eDqNP7O0SbGz3DtC5Y2LATEj6o6zKIQ4ZT+3w== -"@biomejs/cli-linux-arm64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.4.0.tgz#40fbd94cff2c8437d18136d25801ead441ac6739" - integrity sha512-gyLkT/Yh9xfW1T9yjQs/2txkCeG0e+LRs0adLugMwN0ptcNTRyusBvUoiHnpB+9rS6hWu9ZCedGMNmKQ8v2GSw== +"@biomejs/cli-linux-arm64-musl@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.2.tgz#fe5cafb9ff34ebfed7a5abe28a71cfbddd4de70f" + integrity sha512-Z29SjaOyO4QfajplNXSjLx17S79oPN42D094zjE24z7C7p3NxvLhKLygtSP9emgaXkcoESe2chOzF4IrGy/rlg== -"@biomejs/cli-linux-x64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.4.0.tgz#813d191b020a90aa829a5fc37dfeea393696a0f1" - integrity sha512-LIxTuU2zSbIHM9XDYjQphJ5UU8h2eS7yR8uIvGYSba7Qt9AKqfbenyVJTsVnoj1CXxxgKNVSc/wVmlOlGz5DBQ== +"@biomejs/cli-linux-arm64@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.5.2.tgz#2fd9305441d9df0aca5dfa9e56004d951aea0ec9" + integrity sha512-fVLrUgIlo05rO4cNu+Py5EwwmXnXhWH+8KrNlWkr2weMYjq85SihUsuWWKpmqU+bUVR+m5gwfcIXZVWYVCJMHw== -"@biomejs/cli-win32-arm64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.4.0.tgz#a6edb984d48d9a9db5971e13c3047ab19fd592c2" - integrity sha512-U2jT1/0wZLJIRqnU8qHAfi/A/+yUwlL3sYJgqs+wO0BbR22WGQZlj03u5FdpEoyLXdsLv1pbeIcjNp+V0NYXWA== +"@biomejs/cli-linux-x64-musl@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.2.tgz#80cc7eb91ea10aca0a17e6296fa468b0b3332793" + integrity sha512-ZolquPEjWYUmGeERS8svHOOT7OXEeoriPnV8qptgWJmYF9EO9HUGRn1UtCvdVziDYK+u1A7PxjOdkY1B00ty5A== -"@biomejs/cli-win32-x64@1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.4.0.tgz#0bb1292c5e279198912b6ec35649124ba8349b72" - integrity sha512-gN6DgyyBxIwoCovAUFJHFWVallb0cLosayDRtNyxU3MDv/atZxSXOWQezfVKBIbgmFPxYWJObd+awvbPYXwwww== +"@biomejs/cli-linux-x64@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-1.5.2.tgz#9247165d0514a6f0fa17f9c8cd49c7d9769a9641" + integrity sha512-ixqJtUHtF0ho1+1DTZQLAEwHGSqvmvHhAAFXZQoaSdABn+IcITYExlFVA3bGvASy/xtPjRhTx42hVwPtLwMHwg== + +"@biomejs/cli-win32-arm64@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.5.2.tgz#86d709f835547537f87fd07c35b6ef3b97dca54f" + integrity sha512-DN4cXSAoFTdjOoh7f+JITj1uQgQSXt+1pVea9bFrpbgip+ZwkONqQq+jUcmFMMehbp9LuiVtNXFz/ReHn6FY7A== + +"@biomejs/cli-win32-x64@1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-1.5.2.tgz#607f0e4c01c22e573785bd69be2d7be3415838f8" + integrity sha512-YvWWXZmk936FdrXqc2jcP6rfsXsNBIs9MKBQQoVXIihwNNRiAaBD9Iwa/ouU1b7Zxq2zETgeuRewVJickFuVOw== "@cloudflare/kv-asset-handler@0.3.4", "@cloudflare/kv-asset-handler@^0.3.4": version "0.3.4" @@ -8189,10 +8201,10 @@ history "^5.3.0" react-router-dom "^6.2.2" -"@remix-run/router@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.2.tgz#1c17eadb2fa77f80a796ad5ea9bf108e6993ef06" - integrity sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ== +"@remix-run/router@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.21.0.tgz#c65ae4262bdcfe415dbd4f64ec87676e4a56e2b5" + integrity sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA== "@remix-run/router@1.x": version "1.15.0" @@ -8633,34 +8645,34 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry-internal/rrdom@2.30.0": - version "2.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.30.0.tgz#b0d0455be62db08a196d22c3f99e063489634223" - integrity sha512-u5f38j3y7esGSoJfblgQETX2sWC2+jM3nkzhqPP0nOEKoIb0GPA+m1fa2D949BXrk20e98qEUPzW32dpF4ka/w== +"@sentry-internal/rrdom@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.31.0.tgz#548773964167ec104d3cbb9d7a4b25103c091e06" + integrity sha512-6sCgyKZy0Jpkb0wQ2XYLNcJjCETfbjHJ5jroAm2mU1imoSBlAldtNTdMc0wk8MaX/0q5qkMlr78SiYFf7xo12Q== dependencies: - "@sentry-internal/rrweb-snapshot" "2.30.0" + "@sentry-internal/rrweb-snapshot" "2.31.0" -"@sentry-internal/rrweb-snapshot@2.30.0": - version "2.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.30.0.tgz#d4e1974e32e068db0bd3e3cfd5f0d700f5c4d414" - integrity sha512-rR6KRcE0UZfrh1taBO1KLVzfDaQ2iWW879LBMa94HEH/xUUSG3vRF7t55rmKpxIam1v2Ib6iiCMMTAZoZxzE0Q== +"@sentry-internal/rrweb-snapshot@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.31.0.tgz#7a86d02429a490f6367d7aead0548fad7e0c9487" + integrity sha512-uGyJPfmOaiSZOZyd5HFiI8aCd/pPOtrZB89BJBgdB63++wD9Fry8OoWvRORzTo+N+Squummkq3Iucjm/yGdbAw== -"@sentry-internal/rrweb-types@2.30.0": - version "2.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.30.0.tgz#50ab65034fab3d8243b601ff9925fe45ad141f48" - integrity sha512-Wb6RM5SnnWdCpHB6nxEGFV4bqQwMMDOGryMj8QyZf7fK6lkxtEBXOWiEhxUgAUPjMBqQDZm/2DzKO+bW4NHLZg== +"@sentry-internal/rrweb-types@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.31.0.tgz#daf766526efff760eb6020ddf22c6432db9ff6a6" + integrity sha512-rWNCU6KaYopmd+353KBbRuNftpqfI97GdNFeCmB1wwwV/S2CW/lVcEn1uzMwzs8blm3YL2i/O+ykONxecQj+BQ== dependencies: - "@sentry-internal/rrweb-snapshot" "2.30.0" + "@sentry-internal/rrweb-snapshot" "2.31.0" "@types/css-font-loading-module" "0.0.7" -"@sentry-internal/rrweb@2.30.0": - version "2.30.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.30.0.tgz#9af028b80a0081c75ff410817fe2fcda010f9cf6" - integrity sha512-ZOf4RmxX29LgQDW5sy9D/JfwmQbgMzF6DfA00rlFTtQYht56gbgtmWfqeWMDxG9tas71BnMTOz6eF28t7MoykQ== +"@sentry-internal/rrweb@2.31.0": + version "2.31.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.31.0.tgz#69c6f6a4304c4d7446c3a1cc1c9b044d5dc4f040" + integrity sha512-u1uobvl5qnjtdhL2lrzbpwROZagc5xT1P2lANXxrKpLOUpp2+jMSjq0Zt2TElj+2aHlqh4lkDsj/OIa/FBHnnQ== dependencies: - "@sentry-internal/rrdom" "2.30.0" - "@sentry-internal/rrweb-snapshot" "2.30.0" - "@sentry-internal/rrweb-types" "2.30.0" + "@sentry-internal/rrdom" "2.31.0" + "@sentry-internal/rrweb-snapshot" "2.31.0" + "@sentry-internal/rrweb-types" "2.31.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" @@ -28781,19 +28793,12 @@ react-is@^18.0.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -"react-router-6.4@npm:react-router@6.4.2": - version "6.4.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.4.2.tgz#300628ee9ed81b8ef1597b5cb98b474efe9779b8" - integrity sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw== +"react-router-6@npm:react-router@6.28.0": + version "6.28.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.28.0.tgz#29247c86d7ba901d7e5a13aa79a96723c3e59d0d" + integrity sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg== dependencies: - "@remix-run/router" "1.0.2" - -"react-router-6@npm:react-router@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" + "@remix-run/router" "1.21.0" react-router-dom@^6.2.2: version "6.3.0"