From 1cf960b34a83c4d4f9ee48bb2ab06be412e0b3d3 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 30 Dec 2024 13:37:20 +0300 Subject: [PATCH 1/7] feat(react): Use `initialEntries` in wrapped routers. --- .../react/src/reactrouterv6-compat-utils.tsx | 32 +++++-- packages/react/test/reactrouterv6.test.tsx | 89 +++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx index 17ce3753bdbd..80b8062fe06c 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -83,17 +83,39 @@ export function createV6CompatibleWrapCreateBrowserRouter< // `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; + initialEntries?: (string | { pathname: string })[]; + initialIndex?: number; + }, + ): TRouter { const router = createRouterFunction(routes, opts); const basename = opts?.basename; const activeRootSpan = getActiveRootSpan(); - // The initial load ends when `createBrowserRouter` is called. - // This is the earliest convenient time to update the transaction name. - // Callbacks to `router.subscribe` are not called for the initial load. + const initialEntries = opts && opts.initialEntries; + const initialIndex = opts && opts.initialIndex; + + const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; + const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; + + const 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, router.state.location, routes, undefined, basename); + updatePageloadTransaction(activeRootSpan, location, routes, undefined, basename); } router.subscribe((state: RouterState) => { diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 3b9e9e42a4c7..466f45a2a813 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, + wrapCreateBrowserRouterV6, wrapUseRoutesV6, } from '../src/reactrouterv6'; @@ -79,6 +84,90 @@ describe('reactRouterV6BrowserTracingIntegration', () => { getCurrentScope().setClient(undefined); }); + describe('wrapCreateBrowserRouterV6 - createMemoryRouter', () => { + it('starts a pageload transaction', () => { + 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 = wrapCreateBrowserRouterV6(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', + }, + }); + }); + + it('updates the transaction name on a pageload', () => { + 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 = wrapCreateBrowserRouterV6(createMemoryRouter); + + const router = wrappedCreateMemoryRouter(routes, { + initialEntries: ['/', '/about'], + initialIndex: 2, + }); + + render(); + + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about'); + }); + }); + describe('withSentryReactRouterV6Routing', () => { it('starts a pageload transaction', () => { const client = createMockBrowserClient(); From 39828b5aece6397ef8c9cb98b756cc363f544c5e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 9 Jan 2025 19:17:48 +0300 Subject: [PATCH 2/7] Export `wrapCreateMemoryRouter` --- .../react-create-browser-router/.gitignore | 29 ++++ .../react-create-browser-router/.npmrc | 2 + .../react-create-browser-router/package.json | 40 +++++ .../playwright.config.mjs | 7 + .../public/index.html | 24 +++ .../src/globals.d.ts | 5 + .../react-create-browser-router/src/index.tsx | 66 ++++++++ .../src/pages/Index.tsx | 23 +++ .../src/pages/User.tsx | 8 + .../src/react-app-env.d.ts | 1 + .../start-event-proxy.mjs | 6 + .../tests/errors.test.ts | 30 ++++ .../tests/transactions.test.ts | 142 ++++++++++++++++ .../react-create-browser-router/tsconfig.json | 20 +++ .../react-create-memory-router/.gitignore | 29 ++++ .../react-create-memory-router/.npmrc | 2 + .../react-create-memory-router/package.json | 40 +++++ .../playwright.config.mjs | 7 + .../public/index.html | 24 +++ .../src/globals.d.ts | 5 + .../react-create-memory-router/src/index.tsx | 65 +++++++ .../src/pages/Index.tsx | 20 +++ .../src/pages/User.tsx | 19 +++ .../src/react-app-env.d.ts | 1 + .../start-event-proxy.mjs | 6 + .../tests/errors.test.ts | 34 ++++ .../tests/transactions.test.ts | 141 ++++++++++++++++ .../react-create-memory-router/tsconfig.json | 20 +++ packages/react/src/index.ts | 2 + .../react/src/reactrouterv6-compat-utils.tsx | 24 +-- packages/react/src/reactrouterv6.tsx | 13 ++ packages/react/src/reactrouterv7.tsx | 13 ++ packages/react/test/reactrouterv6.test.tsx | 158 ++++++++++-------- 33 files changed, 942 insertions(+), 84 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/public/index.html create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/src/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/User.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/src/react-app-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-browser-router/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/public/index.html create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/src/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/User.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/src/react-app-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-create-memory-router/tsconfig.json 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..d3e3a4c2a64c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts @@ -0,0 +1,142 @@ +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.contexts?.trace).toEqual({ + data: { + deviceMemory: expect.any(String), + effectiveConnectionType: expect.any(String), + hardwareConcurrency: expect.any(String), + 'lcp.element': 'body > div#root > input#exception-button[type="button"]', + 'lcp.id': 'exception-button', + 'lcp.size': 1650, + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + 'performance.timeOrigin': expect.any(Number), + 'performance.activationStart': expect.any(Number), + 'lcp.renderTime': expect.any(Number), + 'lcp.loadTime': expect.any(Number), + }, + op: 'pageload', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.pageload.react.reactrouter_v6', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.domContentLoadedEvent', + }, + description: page.url(), + op: 'browser.domContentLoadedEvent', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.connect', + }, + description: page.url(), + op: 'browser.connect', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.request', + }, + description: page.url(), + op: 'browser.request', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.response', + }, + description: page.url(), + op: 'browser.response', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); +}); + +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({ + deviceMemory: expect.any(String), + effectiveConnectionType: expect.any(String), + hardwareConcurrency: expect.any(String), + '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..1fc40bac9483 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/pages/Index.tsx @@ -0,0 +1,20 @@ +// 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!'); + }} + /> + + ); +}; + +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..0d0d50cef5cc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts @@ -0,0 +1,141 @@ +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.contexts?.trace).toEqual({ + data: { + deviceMemory: expect.any(String), + effectiveConnectionType: expect.any(String), + hardwareConcurrency: expect.any(String), + 'lcp.element': 'body > div#root > div', + 'lcp.size': 8084, + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.react.reactrouter_v6', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + 'performance.timeOrigin': expect.any(Number), + 'performance.activationStart': expect.any(Number), + 'lcp.renderTime': expect.any(Number), + 'lcp.loadTime': expect.any(Number), + }, + op: 'pageload', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.pageload.react.reactrouter_v6', + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: '/user/:id', + type: 'transaction', + transaction_info: { + source: 'route', + }, + }), + ); + + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.domContentLoadedEvent', + }, + description: page.url(), + op: 'browser.domContentLoadedEvent', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.connect', + }, + description: page.url(), + op: 'browser.connect', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.request', + }, + description: page.url(), + op: 'browser.request', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); + expect(transactionEvent.spans).toContainEqual({ + data: { + 'sentry.origin': 'auto.ui.browser.metrics', + 'sentry.op': 'browser.response', + }, + description: page.url(), + op: 'browser.response', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.ui.browser.metrics', + }); +}); + +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({ + deviceMemory: expect.any(String), + effectiveConnectionType: expect.any(String), + hardwareConcurrency: expect.any(String), + '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 80b8062fe06c..fc24b8ad4d3b 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -70,6 +70,7 @@ export function createV6CompatibleWrapCreateBrowserRouter< >( createRouterFunction: CreateRouterFunction, version: V6CompatibleVersion, + isMemoryRouter = false, ): CreateRouterFunction { if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { DEBUG_BUILD && @@ -82,9 +83,9 @@ export function createV6CompatibleWrapCreateBrowserRouter< // `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[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any opts?: Record & { basename?: string; initialEntries?: (string | { pathname: string })[]; @@ -95,18 +96,21 @@ export function createV6CompatibleWrapCreateBrowserRouter< const basename = opts?.basename; const activeRootSpan = getActiveRootSpan(); + let initialEntry = undefined; - const initialEntries = opts && opts.initialEntries; - const initialIndex = opts && opts.initialIndex; + if (isMemoryRouter) { + const initialEntries = opts && opts.initialEntries; + const initialIndex = opts && opts.initialIndex; - const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; - const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; + const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; + const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; - const initialEntry = hasOnlyOneInitialEntry - ? initialEntries[0] - : hasIndexedEntry - ? initialEntries[initialIndex] - : undefined; + initialEntry = hasOnlyOneInitialEntry + ? initialEntries[0] + : hasIndexedEntry + ? initialEntries[initialIndex] + : undefined; + } const location = initialEntry ? typeof initialEntry === 'string' diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 6faaa2e6f65a..6e26c18bfd06 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -38,6 +38,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 createV6CompatibleWrapCreateBrowserRouter(createMemoryRouterFunction, '6', true); +} + /** * 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..c20e3eb54917 100644 --- a/packages/react/src/reactrouterv7.tsx +++ b/packages/react/src/reactrouterv7.tsx @@ -40,6 +40,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 createV6CompatibleWrapCreateBrowserRouter(createMemoryRouterFunction, '7', true); +} + /** * 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 466f45a2a813..6a88bfd18163 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -30,6 +30,7 @@ import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, wrapCreateBrowserRouterV6, + wrapCreateMemoryRouterV6, wrapUseRoutesV6, } from '../src/reactrouterv6'; @@ -84,88 +85,97 @@ describe('reactRouterV6BrowserTracingIntegration', () => { getCurrentScope().setClient(undefined); }); - describe('wrapCreateBrowserRouterV6 - createMemoryRouter', () => { - it('starts a pageload transaction', () => { - 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 = wrapCreateBrowserRouterV6(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', - }, - }); + 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'], }); - it('updates the transaction name on a pageload', () => { - const client = createMockBrowserClient(); - setCurrentClient(client); - - client.addIntegration( - reactRouterV6BrowserTracingIntegration({ - useEffect: React.useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ); + render(); - const routes: RouteObject[] = [ - { - path: '/', - element:
Home
, - }, - { - path: '/about', - element:
About
, - }, - ]; + 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', + }, + }); - const wrappedCreateMemoryRouter = wrapCreateBrowserRouterV6(createMemoryRouter); + expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about'); + }); - const router = wrappedCreateMemoryRouter(routes, { - initialEntries: ['/', '/about'], - initialIndex: 2, - }); + 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(); + render(); - expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/about'); + 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', () => { From 14a8e0ac0166c7adf40cfa2136810ea7f313a994 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 9 Jan 2025 19:37:24 +0300 Subject: [PATCH 3/7] Fix formatting --- .../react-create-memory-router/src/pages/Index.tsx | 1 - packages/react/test/reactrouterv6.test.tsx | 1 - 2 files changed, 2 deletions(-) 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 index 1fc40bac9483..b025f721e100 100644 --- 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 @@ -1,6 +1,5 @@ // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; -import { Link } from 'react-router-dom'; const Index = () => { return ( diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 6a88bfd18163..879abf15d2d0 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -29,7 +29,6 @@ import { BrowserClient } from '../src'; import { reactRouterV6BrowserTracingIntegration, withSentryReactRouterV6Routing, - wrapCreateBrowserRouterV6, wrapCreateMemoryRouterV6, wrapUseRoutesV6, } from '../src/reactrouterv6'; From ea95f261485a13a8957a018dbdc1454102458c83 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 9 Jan 2025 19:47:49 +0300 Subject: [PATCH 4/7] Accept any `lcp.size` in test. --- .../react-create-memory-router/tests/transactions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 0d0d50cef5cc..828910a8b56a 100644 --- 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 @@ -15,7 +15,7 @@ test('Captures a pageload transaction', async ({ page }) => { effectiveConnectionType: expect.any(String), hardwareConcurrency: expect.any(String), 'lcp.element': 'body > div#root > div', - 'lcp.size': 8084, + 'lcp.size': expect.any(Number), 'sentry.idle_span_finish_reason': 'idleTimeout', 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.react.reactrouter_v6', From 149869f41a71fcb46b555a7048c10fc189101d41 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 10 Jan 2025 11:14:43 +0000 Subject: [PATCH 5/7] separate generator generator functions --- .../react/src/reactrouterv6-compat-utils.tsx | 75 +++++++++++++++---- packages/react/src/reactrouterv6.tsx | 3 +- packages/react/src/reactrouterv7.tsx | 3 +- 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx index fc24b8ad4d3b..bd3e2400a3bf 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -70,7 +70,6 @@ export function createV6CompatibleWrapCreateBrowserRouter< >( createRouterFunction: CreateRouterFunction, version: V6CompatibleVersion, - isMemoryRouter = false, ): CreateRouterFunction { if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes) { DEBUG_BUILD && @@ -81,12 +80,58 @@ 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. + return function (routes: RouteObject[], opts?: Record & { basename?: string }): TRouter { + const router = createRouterFunction(routes, opts); + const basename = opts?.basename; + + const activeRootSpan = getActiveRootSpan(); + + // The initial load ends when `createBrowserRouter` is called. + // This is the earliest convenient time to update the transaction name. + // Callbacks to `router.subscribe` are not called for the initial load. + if (router.state.historyAction === 'POP' && activeRootSpan) { + updatePageloadTransaction(activeRootSpan, router.state.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 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[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - opts?: Record & { + opts?: Record & { basename?: string; initialEntries?: (string | { pathname: string })[]; initialIndex?: number; @@ -98,19 +143,17 @@ export function createV6CompatibleWrapCreateBrowserRouter< const activeRootSpan = getActiveRootSpan(); let initialEntry = undefined; - if (isMemoryRouter) { - const initialEntries = opts && opts.initialEntries; - const initialIndex = opts && opts.initialIndex; + const initialEntries = opts && opts.initialEntries; + const initialIndex = opts && opts.initialIndex; - const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; - const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; + const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; + const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex]; - initialEntry = hasOnlyOneInitialEntry - ? initialEntries[0] - : hasIndexedEntry - ? initialEntries[initialIndex] - : undefined; - } + initialEntry = hasOnlyOneInitialEntry + ? initialEntries[0] + : hasIndexedEntry + ? initialEntries[initialIndex] + : undefined; const location = initialEntry ? typeof initialEntry === 'string' diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 6e26c18bfd06..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'; @@ -48,7 +49,7 @@ export function wrapCreateMemoryRouterV6< TState extends RouterState = RouterState, TRouter extends Router = Router, >(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction { - return createV6CompatibleWrapCreateBrowserRouter(createMemoryRouterFunction, '6', true); + return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '6'); } /** diff --git a/packages/react/src/reactrouterv7.tsx b/packages/react/src/reactrouterv7.tsx index c20e3eb54917..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'; @@ -50,7 +51,7 @@ export function wrapCreateMemoryRouterV7< TState extends RouterState = RouterState, TRouter extends Router = Router, >(createMemoryRouterFunction: CreateRouterFunction): CreateRouterFunction { - return createV6CompatibleWrapCreateBrowserRouter(createMemoryRouterFunction, '7', true); + return createV6CompatibleWrapCreateMemoryRouter(createMemoryRouterFunction, '7'); } /** From 11fe7659ed37547e11d5f2fb4ff1342385307a68 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 10 Jan 2025 11:22:59 +0000 Subject: [PATCH 6/7] limit test assertions to relevant attributes --- .../tests/transactions.test.ts | 100 ++++-------------- .../tests/transactions.test.ts | 96 +++-------------- 2 files changed, 33 insertions(+), 163 deletions(-) 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 index d3e3a4c2a64c..5ecd098daf94 100644 --- 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 @@ -9,29 +9,6 @@ test('Captures a pageload transaction', async ({ page }) => { await page.goto('/'); const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: { - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), - 'lcp.element': 'body > div#root > input#exception-button[type="button"]', - 'lcp.id': 'exception-button', - 'lcp.size': 1650, - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.source': 'route', - 'performance.timeOrigin': expect.any(Number), - 'performance.activationStart': expect.any(Number), - 'lcp.renderTime': expect.any(Number), - 'lcp.loadTime': expect.any(Number), - }, - op: 'pageload', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.pageload.react.reactrouter_v6', - }); expect(transactionEvent).toEqual( expect.objectContaining({ @@ -43,62 +20,24 @@ test('Captures a pageload transaction', async ({ page }) => { }), ); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.domContentLoadedEvent', - }, - description: page.url(), - op: 'browser.domContentLoadedEvent', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.connect', - }, - description: page.url(), - op: 'browser.connect', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.request', - }, - description: page.url(), - op: 'browser.request', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.response', - }, - description: page.url(), - op: 'browser.response', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); + 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 }) => { @@ -113,9 +52,6 @@ test('Captures a navigation transaction', async ({ page }) => { const transactionEvent = await transactionEventPromise; expect(transactionEvent.contexts?.trace).toEqual({ data: expect.objectContaining({ - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), 'sentry.idle_span_finish_reason': 'idleTimeout', 'sentry.op': 'navigation', 'sentry.origin': 'auto.navigation.react.reactrouter_v6', 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 index 828910a8b56a..7c75c395c3af 100644 --- 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 @@ -9,28 +9,6 @@ test('Captures a pageload transaction', async ({ page }) => { await page.goto('/'); const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: { - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), - 'lcp.element': 'body > div#root > div', - 'lcp.size': expect.any(Number), - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.source': 'route', - 'performance.timeOrigin': expect.any(Number), - 'performance.activationStart': expect.any(Number), - 'lcp.renderTime': expect.any(Number), - 'lcp.loadTime': expect.any(Number), - }, - op: 'pageload', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.pageload.react.reactrouter_v6', - }); expect(transactionEvent).toEqual( expect.objectContaining({ @@ -42,62 +20,21 @@ test('Captures a pageload transaction', async ({ page }) => { }), ); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.domContentLoadedEvent', - }, - description: page.url(), - op: 'browser.domContentLoadedEvent', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.connect', - }, - description: page.url(), - op: 'browser.connect', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.request', - }, - description: page.url(), - op: 'browser.request', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.response', - }, - description: page.url(), - op: 'browser.response', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - }); + 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 }) => { @@ -112,9 +49,6 @@ test('Captures a navigation transaction', async ({ page }) => { const transactionEvent = await transactionEventPromise; expect(transactionEvent.contexts?.trace).toEqual({ data: expect.objectContaining({ - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), 'sentry.idle_span_finish_reason': 'idleTimeout', 'sentry.op': 'navigation', 'sentry.origin': 'auto.navigation.react.reactrouter_v6', From 902ceff31d475d25be914a40ff696ce71cd76029 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 10 Jan 2025 11:24:50 +0000 Subject: [PATCH 7/7] lint --- packages/react/src/reactrouterv6-compat-utils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/reactrouterv6-compat-utils.tsx b/packages/react/src/reactrouterv6-compat-utils.tsx index ac6e67693e06..9ca96a03b0de 100644 --- a/packages/react/src/reactrouterv6-compat-utils.tsx +++ b/packages/react/src/reactrouterv6-compat-utils.tsx @@ -143,8 +143,8 @@ export function createV6CompatibleWrapCreateMemoryRouter< const activeRootSpan = getActiveRootSpan(); let initialEntry = undefined; - const initialEntries = opts && opts.initialEntries; - const initialIndex = opts && opts.initialIndex; + const initialEntries = opts?.initialEntries; + const initialIndex = opts?.initialIndex; const hasOnlyOneInitialEntry = initialEntries && initialEntries.length === 1; const hasIndexedEntry = initialIndex !== undefined && initialEntries && initialEntries[initialIndex];