From 05262ff1c42b723e12f43195df0791fdee0b194e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 12 Mar 2024 12:46:02 +0100 Subject: [PATCH 1/2] ReactRouter 4 and 5 --- packages/react/src/reactrouter.tsx | 11 +++-- packages/react/test/reactrouterv4.test.tsx | 53 ++++++++++++++++++++++ packages/react/test/reactrouterv5.test.tsx | 53 ++++++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index e1a97443d756..3fe40df8ece8 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -9,6 +9,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, + getCurrentScope, getRootSpan, spanToJSON, } from '@sentry/core'; @@ -226,9 +227,13 @@ export function withSentryRouting

, R extends React const activeRootSpan = getActiveRootSpan(); const WrappedRoute: React.FC

= (props: P) => { - if (activeRootSpan && props && props.computedMatch && props.computedMatch.isExact) { - activeRootSpan.updateName(props.computedMatch.path); - activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + if (props && props.computedMatch && props.computedMatch.isExact) { + getCurrentScope().setTransactionName(props.computedMatch.path); + + if (activeRootSpan) { + activeRootSpan.updateName(props.computedMatch.path); + activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + } } // @ts-expect-error Setting more specific React Component typing for `R` generic above diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 5e6a142c11cd..0f331ba26abe 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -86,6 +86,18 @@ describe('browserTracingReactRouterV4', () => { }); }); + it("updates the scope's `transactionName` on pageload", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history })); + + client.init(); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + }); + it('starts a navigation transaction', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -341,4 +353,45 @@ describe('browserTracingReactRouterV4', () => { }, }); }); + + it("updates the scope's `transactionName` on a route change", () => { + const routes: RouteConfig[] = [ + { + path: '/organizations/:orgid/v1/:teamid', + }, + { path: '/organizations/:orgid' }, + { path: '/' }, + ]; + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV4BrowserTracingIntegration({ history, routes, matchPath })); + + client.init(); + + const SentryRoute = withSentryRouting(Route); + + render( + + +

Team
} /> +
OrgId
} /> +
Home
} /> + + , + ); + + act(() => { + history.push('/organizations/1234/v1/758'); + }); + + expect(getCurrentScope().getScopeData().transactionName).toEqual('/organizations/:orgid/v1/:teamid'); + + act(() => { + history.push('/organizations/1234'); + }); + + expect(getCurrentScope().getScopeData().transactionName).toEqual('/organizations/:orgid'); + }); }); diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 7d4939cce522..901745f86b23 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -86,6 +86,18 @@ describe('browserTracingReactRouterV5', () => { }); }); + it("updates the scope's `transactionName` on pageload", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history })); + + client.init(); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + }); + it('starts a navigation transaction', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -341,4 +353,45 @@ describe('browserTracingReactRouterV5', () => { }, }); }); + + it("updates the scope's `transactionName` on a route change", () => { + const routes: RouteConfig[] = [ + { + path: '/organizations/:orgid/v1/:teamid', + }, + { path: '/organizations/:orgid' }, + { path: '/' }, + ]; + const client = createMockBrowserClient(); + setCurrentClient(client); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV5BrowserTracingIntegration({ history, routes, matchPath })); + + client.init(); + + const SentryRoute = withSentryRouting(Route); + + render( + + +
Team
} /> +
OrgId
} /> +
Home
} /> +
+
, + ); + + act(() => { + history.push('/organizations/1234/v1/758'); + }); + + expect(getCurrentScope().getScopeData().transactionName).toBe('/organizations/:orgid/v1/:teamid'); + + act(() => { + history.push('/organizations/1234'); + }); + + expect(getCurrentScope().getScopeData().transactionName).toBe('/organizations/:orgid'); + }); }); From 768087572b1a8d91d0abf35abf8de56cfb189e47 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 12 Mar 2024 14:01:00 +0100 Subject: [PATCH 2/2] ReactRouter 3 and 6 --- packages/react/src/reactrouterv6.tsx | 12 +- packages/react/test/reactrouterv3.test.tsx | 32 ++++- packages/react/test/reactrouterv6.4.test.tsx | 70 +++++++++++ packages/react/test/reactrouterv6.test.tsx | 124 +++++++++++++++++++ 4 files changed, 234 insertions(+), 4 deletions(-) diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index cdf798cb84c7..ab67ba44bb5a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -13,6 +13,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getClient, + getCurrentScope, getRootSpan, spanToJSON, } from '@sentry/core'; @@ -198,10 +199,15 @@ function updatePageloadTransaction( ? matches : (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]); - if (activeRootSpan && branches) { + if (branches) { const [name, source] = getNormalizedName(routes, location, branches, basename); - activeRootSpan.updateName(name); - activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + + getCurrentScope().setTransactionName(name); + + if (activeRootSpan) { + activeRootSpan.updateName(name); + activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + } } } diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index 413c9566e71a..c207ead3aab3 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -29,7 +29,6 @@ const mockStartBrowserTracingPageLoadSpan = jest.fn(); const mockStartBrowserTracingNavigationSpan = jest.fn(); const mockRootSpan = { - updateName: jest.fn(), setAttribute: jest.fn(), getSpanJSON() { return { op: 'pageload' }; @@ -115,6 +114,18 @@ describe('browserTracingReactRouterV3', () => { }); }); + it("updates the scope's `transactionName` on pageload", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration(reactRouterV3BrowserTracingIntegration({ history, routes: instrumentationRoutes, match })); + + client.init(); + render({routes}); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + }); + it('starts a navigation transaction', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -192,4 +203,23 @@ describe('browserTracingReactRouterV3', () => { }, }); }); + + it("updates the scope's `transactionName` on a navigation", () => { + const client = createMockBrowserClient(); + + const history = createMemoryHistory(); + client.addIntegration(reactRouterV3BrowserTracingIntegration({ history, routes: instrumentationRoutes, match })); + + client.init(); + const { container } = render({routes}); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + + act(() => { + history.push('/users/123'); + }); + expect(container.innerHTML).toContain('123'); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/users/:userid'); + }); }); diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index 34fe85b6bfc9..b2410e3bfad9 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -122,6 +122,39 @@ describe('reactRouterV6BrowserTracingIntegration (v6.4)', () => { }); }); + it("updates the scope's `transactionName` on a pageload", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element:
TEST
, + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + }); + it('starts a navigation transaction', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -590,5 +623,42 @@ describe('reactRouterV6BrowserTracingIntegration (v6.4)', () => { }, }); }); + + it("updates the scope's `transactionName` on a navigation", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); + + const router = sentryCreateBrowserRouter( + [ + { + path: '/', + element: , + }, + { + path: 'about', + element:
About
, + }, + ], + { + initialEntries: ['/'], + }, + ); + + // @ts-expect-error router is fine + render(); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/about'); + }); }); }); diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index c86f55ccbd73..131161856214 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -114,6 +114,32 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); + it("updates the scope's `transactionName` on a pageload", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + Home} /> + + , + ); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + }); + it('skips pageload transaction with `instrumentPageLoad: false`', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -364,6 +390,35 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }, }); }); + + it("updates the scope's `transactionName` on a navigation", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const SentryRoutes = withSentryReactRouterV6Routing(Routes); + + render( + + + About}> + page} /> + + } /> + + , + ); + + expect(getCurrentScope().getScopeData()?.transactionName).toBe('/about/:page'); + }); }); describe('wrapUseRoutes', () => { @@ -408,6 +463,39 @@ describe('reactRouterV6BrowserTracingIntegration', () => { }); }); + it("updates the scope's `transactionName` on a pageload", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element:
Home
, + }, + ]); + + render( + + + , + ); + + expect(getCurrentScope().getScopeData()?.transactionName).toEqual('/'); + }); + it('skips pageload transaction with `instrumentPageLoad: false`', () => { const client = createMockBrowserClient(); setCurrentClient(client); @@ -875,5 +963,41 @@ describe('reactRouterV6BrowserTracingIntegration', () => { expect(mockRootSpan.updateName).toHaveBeenLastCalledWith('/tests/:testId/*'); expect(mockRootSpan.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); + + it("updates the scope's `transactionName` on a navigation", () => { + const client = createMockBrowserClient(); + setCurrentClient(client); + + client.addIntegration( + reactRouterV6BrowserTracingIntegration({ + useEffect: React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ); + const wrappedUseRoutes = wrapUseRoutes(useRoutes); + + const Routes = () => + wrappedUseRoutes([ + { + path: '/', + element: , + }, + { + path: '/about', + element:
About
, + }, + ]); + + render( + + + , + ); + + expect(getCurrentScope().getScopeData()?.transactionName).toBe('/about'); + }); }); });