diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore
new file mode 100644
index 000000000000..84634c973eeb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.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-create-browser-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/.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-create-browser-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
new file mode 100644
index 000000000000..a4e7dae6d1e2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "react-create-browser-router-test",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@sentry/react": "latest || *",
+ "@types/node": "^18.19.1",
+ "@types/react": "18.0.0",
+ "@types/react-dom": "18.0.0",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "typescript": "~5.0.0"
+ },
+ "scripts": {
+ "build": "react-scripts build",
+ "start": "serve -s build",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && 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"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/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-create-browser-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html
new file mode 100644
index 000000000000..39da76522bea
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts
new file mode 100644
index 000000000000..ffa61ca49acc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/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-create-browser-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx
new file mode 100644
index 000000000000..88f8cfa502ec
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx
@@ -0,0 +1,66 @@
+import * as Sentry from '@sentry/react';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import {
+ RouterProvider,
+ createBrowserRouter,
+ createRoutesFromChildren,
+ matchRoutes,
+ useLocation,
+ useNavigationType,
+} from 'react-router-dom';
+import Index from './pages/Index';
+import User from './pages/User';
+
+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,
+ }),
+ replay,
+ ],
+ // We recommend adjusting this value in production, or using tracesSampler
+ // for finer control
+ tracesSampleRate: 1.0,
+ release: 'e2e-test',
+
+ tunnel: 'http://localhost:3031',
+
+ // Always capture replays, so we can test this properly
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ debug: !!process.env.DEBUG,
+});
+
+const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
+
+const router = sentryCreateBrowserRouter(
+ [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/user/:id',
+ element: ,
+ },
+ ],
+ {
+ // We're testing whether this option is avoided in the integration
+ // We expect this to be ignored
+ initialEntries: ['/user/1'],
+ },
+);
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+
+root.render();
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx
new file mode 100644
index 000000000000..d6b71a1d1279
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx
@@ -0,0 +1,23 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+
+const Index = () => {
+ return (
+ <>
+ {
+ throw new Error('I am an error!');
+ }}
+ />
+
+ navigate
+
+ >
+ );
+};
+
+export default Index;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx
new file mode 100644
index 000000000000..62f0c2d17533
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx
@@ -0,0 +1,8 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+
+const User = () => {
+ return I am a blank page :)
;
+};
+
+export default User;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts
new file mode 100644
index 000000000000..6431bc5fc6b2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs
new file mode 100644
index 000000000000..be93e129284f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'react-create-browser-router',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts
new file mode 100644
index 000000000000..4a11f07410ab
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts
@@ -0,0 +1,30 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Captures exception correctly', async ({ page }) => {
+ const errorEventPromise = waitForError('react-create-browser-router', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+
+ expect(errorEvent.request).toEqual({
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/',
+ });
+
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts
new file mode 100644
index 000000000000..5ecd098daf94
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts
@@ -0,0 +1,78 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Captures a pageload transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
+ return event.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.contexts?.trace).toEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ deviceMemory: expect.any(String),
+ effectiveConnectionType: expect.any(String),
+ hardwareConcurrency: expect.any(String),
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'pageload',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.pageload.react.reactrouter_v6',
+ }),
+ );
+});
+
+test('Captures a navigation transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
+ return event.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto('/');
+ const linkElement = page.locator('id=navigation');
+ await linkElement.click();
+
+ const transactionEvent = await transactionEventPromise;
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'navigation',
+ 'sentry.origin': 'auto.navigation.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'navigation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.navigation.react.reactrouter_v6',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/user/:id',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.spans).toEqual([]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json
new file mode 100644
index 000000000000..4cc95dc2689a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/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/e2e-tests/test-applications/react-create-memory-router/.gitignore b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore
new file mode 100644
index 000000000000..84634c973eeb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.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-create-memory-router/.npmrc b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/.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-create-memory-router/package.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
new file mode 100644
index 000000000000..dc6c9b4340f0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "react-create-memory-router-test",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@sentry/react": "latest || *",
+ "@types/node": "^18.19.1",
+ "@types/react": "18.0.0",
+ "@types/react-dom": "18.0.0",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-router-dom": "^6.4.1",
+ "react-scripts": "5.0.1",
+ "typescript": "~5.0.0"
+ },
+ "scripts": {
+ "build": "react-scripts build",
+ "start": "serve -s build",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && 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"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/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-create-memory-router/public/index.html b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html
new file mode 100644
index 000000000000..39da76522bea
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts
new file mode 100644
index 000000000000..ffa61ca49acc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/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-create-memory-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx
new file mode 100644
index 000000000000..f71572f9dc1f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx
@@ -0,0 +1,65 @@
+import * as Sentry from '@sentry/react';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import {
+ RouterProvider,
+ createMemoryRouter,
+ createRoutesFromChildren,
+ matchRoutes,
+ useLocation,
+ useNavigationType,
+} from 'react-router-dom';
+import Index from './pages/Index';
+import User from './pages/User';
+
+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,
+ }),
+ replay,
+ ],
+ // We recommend adjusting this value in production, or using tracesSampler
+ // for finer control
+ tracesSampleRate: 1.0,
+ release: 'e2e-test',
+
+ tunnel: 'http://localhost:3031',
+
+ // Always capture replays, so we can test this properly
+ replaysSessionSampleRate: 1.0,
+ replaysOnErrorSampleRate: 0.0,
+
+ debug: !!process.env.DEBUG,
+});
+
+const sentryCreateMemoryRouter = Sentry.wrapCreateMemoryRouterV6(createMemoryRouter);
+
+const router = sentryCreateMemoryRouter(
+ [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/user/:id',
+ element: ,
+ },
+ ],
+ {
+ initialEntries: ['/', '/user/1', '/user/2'],
+ initialIndex: 2,
+ },
+);
+
+const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
+
+root.render();
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx
new file mode 100644
index 000000000000..b025f721e100
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx
@@ -0,0 +1,19 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+
+const Index = () => {
+ return (
+ <>
+ {
+ throw new Error('I am an error!');
+ }}
+ />
+ >
+ );
+};
+
+export default Index;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx
new file mode 100644
index 000000000000..e54d6c604e2d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx
@@ -0,0 +1,19 @@
+// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
+import * as React from 'react';
+import { Link } from 'react-router-dom';
+
+const User = () => {
+ return (
+
+
+ Home
+
+
+ navigate
+
+
I am a blank page :)
;
+
+ );
+};
+
+export default User;
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts
new file mode 100644
index 000000000000..6431bc5fc6b2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs
new file mode 100644
index 000000000000..9c451610f4c7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'react-create-memory-router',
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts
new file mode 100644
index 000000000000..9406ca63e30c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts
@@ -0,0 +1,34 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Captures exception correctly', async ({ page }) => {
+ const errorEventPromise = waitForError('react-create-memory-router', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+
+ // We're on the user page, navigate back to the home page
+ const homeButton = page.locator('id=home-button');
+ await homeButton.click();
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+
+ expect(errorEvent.request).toEqual({
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/',
+ });
+
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts
new file mode 100644
index 000000000000..7c75c395c3af
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts
@@ -0,0 +1,75 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Captures a pageload transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-memory-router', event => {
+ return event.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/user/:id',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.contexts?.trace).toEqual(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'pageload',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.pageload.react.reactrouter_v6',
+ }),
+ );
+});
+
+test('Captures a navigation transaction', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('react-create-memory-router', event => {
+ return event.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto('/');
+ const linkElement = page.locator('id=navigation-button');
+ await linkElement.click();
+
+ const transactionEvent = await transactionEventPromise;
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'navigation',
+ 'sentry.origin': 'auto.navigation.react.reactrouter_v6',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ }),
+ op: 'navigation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.navigation.react.reactrouter_v6',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ transaction: '/user/:id',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+
+ expect(transactionEvent.spans).toEqual([]);
+});
diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json
new file mode 100644
index 000000000000..4cc95dc2689a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/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/packages/react/src/index.ts b/packages/react/src/index.ts
index 9481e37789ec..ae441a66837b 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -18,10 +18,12 @@ export {
withSentryReactRouterV6Routing,
wrapUseRoutesV6,
wrapCreateBrowserRouterV6,
+ wrapCreateMemoryRouterV6,
} from './reactrouterv6';
export {
reactRouterV7BrowserTracingIntegration,
withSentryReactRouterV7Routing,
wrapCreateBrowserRouterV7,
+ wrapCreateMemoryRouterV7,
wrapUseRoutesV7,
} from './reactrouterv7';
diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx
index 06de135dda40..9ca96a03b0de 100644
--- a/packages/react/src/reactrouterv6-compat-utils.tsx
+++ b/packages/react/src/reactrouterv6-compat-utils.tsx
@@ -80,10 +80,7 @@ export function createV6CompatibleWrapCreateBrowserRouter<
return createRouterFunction;
}
- // `opts` for createBrowserHistory and createMemoryHistory are different, but also not relevant for us at the moment.
- // `basename` is the only option that is relevant for us, and it is the same for all.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter {
+ return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter {
const router = createRouterFunction(routes, opts);
const basename = opts?.basename;
@@ -113,6 +110,78 @@ export function createV6CompatibleWrapCreateBrowserRouter<
};
}
+/**
+ * Creates a wrapCreateMemoryRouter function that can be used with all React Router v6 compatible versions.
+ */
+export function createV6CompatibleWrapCreateMemoryRouter<
+ TState extends RouterState = RouterState,
+ TRouter extends Router = Router,
+>(
+ createRouterFunction: CreateRouterFunction,
+ version: V6CompatibleVersion,
+): CreateRouterFunction {
+ if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) {
+ DEBUG_BUILD &&
+ logger.warn(
+ `reactRouterV${version}Instrumentation was unable to wrap the \`createMemoryRouter\` function because of one or more missing parameters.`,
+ );
+
+ return createRouterFunction;
+ }
+
+ return function (
+ routes: RouteObject[],
+ opts?: Record & {
+ basename?: string;
+ initialEntries?: (string | { pathname: string })[];
+ initialIndex?: number;
+ },
+ ): TRouter {
+ const router = createRouterFunction(routes, opts);
+ const basename = opts?.basename;
+
+ const activeRootSpan = getActiveRootSpan();
+ let initialEntry = undefined;
+
+ const initialEntries = opts?.initialEntries;
+ const initialIndex = opts?.initialIndex;
+
+ const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1;
+ const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex];
+
+ initialEntry = hasOnlyOneInitialEntry
+ ? initialEntries[0]
+ : hasIndexedEntry
+ ? initialEntries[initialIndex]
+ : undefined;
+
+ const location = initialEntry
+ ? typeof initialEntry === 'string'
+ ? { pathname: initialEntry }
+ : initialEntry
+ : router.state.location;
+
+ if (router.state.historyAction === 'POP' && activeRootSpan) {
+ updatePageloadTransaction(activeRootSpan, location, routes, undefined, basename);
+ }
+
+ router.subscribe((state: RouterState) => {
+ const location = state.location;
+ if (state.historyAction === 'PUSH' || state.historyAction === 'POP') {
+ handleNavigation({
+ location,
+ routes,
+ navigationType: state.historyAction,
+ version,
+ basename,
+ });
+ }
+ });
+
+ return router;
+ };
+}
+
/**
* Creates a browser tracing integration that can be used with all React Router v6 compatible versions.
*/
diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx
index 6faaa2e6f65a..a32e2bb02bf1 100644
--- a/packages/react/src/reactrouterv6.tsx
+++ b/packages/react/src/reactrouterv6.tsx
@@ -5,6 +5,7 @@ import {
createReactRouterV6CompatibleTracingIntegration,
createV6CompatibleWithSentryReactRouterRouting,
createV6CompatibleWrapCreateBrowserRouter,
+ createV6CompatibleWrapCreateMemoryRouter,
createV6CompatibleWrapUseRoutes,
} from './reactrouterv6-compat-utils';
import type { CreateRouterFunction, Router, RouterState, UseRoutes } from './types';
@@ -38,6 +39,19 @@ export function wrapCreateBrowserRouterV6<
return createV6CompatibleWrapCreateBrowserRouter(createRouterFunction, '6');
}
+/**
+ * A wrapper function that adds Sentry routing instrumentation to a React Router v6 createMemoryRouter function.
+ * This is used to automatically capture route changes as transactions when using the createMemoryRouter API.
+ * The difference between createBrowserRouter and createMemoryRouter is that with createMemoryRouter,
+ * optional `initialEntries` are also taken into account.
+ */
+export function wrapCreateMemoryRouterV6<
+ TState extends RouterState = RouterState,
+ TRouter extends Router = Router,
+>(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction {
+ return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '6');
+}
+
/**
* A higher-order component that adds Sentry routing instrumentation to a React Router v6 Route component.
* This is used to automatically capture route changes as transactions.
diff --git a/packages/react/src/reactrouterv7.tsx b/packages/react/src/reactrouterv7.tsx
index df2badd35e44..5a80482cd2c3 100644
--- a/packages/react/src/reactrouterv7.tsx
+++ b/packages/react/src/reactrouterv7.tsx
@@ -6,6 +6,7 @@ import {
createReactRouterV6CompatibleTracingIntegration,
createV6CompatibleWithSentryReactRouterRouting,
createV6CompatibleWrapCreateBrowserRouter,
+ createV6CompatibleWrapCreateMemoryRouter,
createV6CompatibleWrapUseRoutes,
} from './reactrouterv6-compat-utils';
import type { CreateRouterFunction, Router, RouterState, UseRoutes } from './types';
@@ -40,6 +41,19 @@ export function wrapCreateBrowserRouterV7<
return createV6CompatibleWrapCreateBrowserRouter(createRouterFunction, '7');
}
+/**
+ * A wrapper function that adds Sentry routing instrumentation to a React Router v7 createMemoryRouter function.
+ * This is used to automatically capture route changes as transactions when using the createMemoryRouter API.
+ * The difference between createBrowserRouter and createMemoryRouter is that with createMemoryRouter,
+ * optional `initialEntries` are also taken into account.
+ */
+export function wrapCreateMemoryRouterV7<
+ TState extends RouterState = RouterState,
+ TRouter extends Router = Router,
+>(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction {
+ return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '7');
+}
+
/**
* A wrapper function that adds Sentry routing instrumentation to a React Router v7 useRoutes hook.
* This is used to automatically capture route changes as transactions when using the useRoutes hook.
diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx
index 3b9e9e42a4c7..879abf15d2d0 100644
--- a/packages/react/test/reactrouterv6.test.tsx
+++ b/packages/react/test/reactrouterv6.test.tsx
@@ -13,7 +13,9 @@ import {
Navigate,
Outlet,
Route,
+ RouterProvider,
Routes,
+ createMemoryRouter,
createRoutesFromChildren,
matchRoutes,
useLocation,
@@ -21,10 +23,13 @@ import {
useRoutes,
} from 'react-router-6';
+import type { RouteObject } from 'react-router-6';
+
import { BrowserClient } from '../src';
import {
reactRouterV6BrowserTracingIntegration,
withSentryReactRouterV6Routing,
+ wrapCreateMemoryRouterV6,
wrapUseRoutesV6,
} from '../src/reactrouterv6';
@@ -79,6 +84,99 @@ describe('reactRouterV6BrowserTracingIntegration', () => {
getCurrentScope().setClient(undefined);
});
+ it('wrapCreateMemoryRouterV6 starts and updates a pageload transaction - single initialEntry', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+
+ const routes: RouteObject[] = [
+ {
+ path: '/',
+ element: Home
,
+ },
+ {
+ path: '/about',
+ element: About
,
+ },
+ ];
+
+ const wrappedCreateMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter);
+
+ const router = wrappedCreateMemoryRouter(routes, {
+ initialEntries: ['/about'],
+ });
+
+ 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',
+ },
+ });
+
+ expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about');
+ });
+
+ it('wrapCreateMemoryRouterV6 starts and updates a pageload transaction - multiple initialEntries', () => {
+ const client = createMockBrowserClient();
+ setCurrentClient(client);
+
+ client.addIntegration(
+ reactRouterV6BrowserTracingIntegration({
+ useEffect: React.useEffect,
+ useLocation,
+ useNavigationType,
+ createRoutesFromChildren,
+ matchRoutes,
+ }),
+ );
+
+ const routes: RouteObject[] = [
+ {
+ path: '/',
+ element: Home
,
+ },
+ {
+ path: '/about',
+ element: About
,
+ },
+ ];
+
+ const wrappedCreateMemoryRouter = wrapCreateMemoryRouterV6(createMemoryRouter);
+
+ const router = wrappedCreateMemoryRouter(routes, {
+ initialEntries: ['/', '/about'],
+ initialIndex: 1,
+ });
+
+ 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',
+ },
+ });
+
+ expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about');
+ });
+
describe('withSentryReactRouterV6Routing', () => {
it('starts a pageload transaction', () => {
const client = createMockBrowserClient();