Skip to content

Commit ea60f3a

Browse files
committed
Support lazy-loaded descendant routes.
1 parent 1e023fb commit ea60f3a

File tree

6 files changed

+174
-10
lines changed

6 files changed

+174
-10
lines changed

dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Sentry from '@sentry/react';
2-
import React from 'react';
2+
import React, { lazy, Suspense } from 'react';
33
import ReactDOM from 'react-dom/client';
44
import {
55
RouterProvider,
@@ -42,13 +42,22 @@ Sentry.init({
4242
});
4343

4444
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
45+
const LazyLoadedUser = lazy(() => import('./pages/LazyLoadedUser'));
4546

4647
const router = sentryCreateBrowserRouter(
4748
[
4849
{
4950
path: '/',
5051
element: <Index />,
5152
},
53+
{
54+
path: '/lazy-loaded-user/*',
55+
element: (
56+
<Suspense fallback={<div>Loading...</div>}>
57+
<LazyLoadedUser />
58+
</Suspense>
59+
),
60+
},
5261
{
5362
path: '/user/:id',
5463
element: <User />,

dev-packages/e2e-tests/test-applications/react-create-browser-router/src/pages/Index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const Index = () => {
1616
<Link to="/user/5" id="navigation">
1717
navigate
1818
</Link>
19+
<Link to="/lazy-loaded-user/5/foo" id="lazy-navigation">
20+
lazy navigate
21+
</Link>
1922
</>
2023
);
2124
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
2+
import * as React from 'react';
3+
import * as Sentry from '@sentry/react';
4+
import { Route, Routes } from 'react-router-dom';
5+
6+
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
7+
8+
const InnerRoute = () => (
9+
<SentryRoutes>
10+
<Route path=":innerId" element={<p id="content">I am a lazy loaded user</p>} />
11+
</SentryRoutes>
12+
);
13+
14+
export default InnerRoute;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
2+
import * as React from 'react';
3+
import * as Sentry from '@sentry/react';
4+
import { Route, Routes } from 'react-router-dom';
5+
6+
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
7+
const InnerRoute = React.lazy(() => import('./LazyLoadedInnerRoute'));
8+
9+
const LazyLoadedUser = () => {
10+
return (
11+
<SentryRoutes>
12+
<Route
13+
path=":id/*"
14+
element={
15+
<React.Suspense fallback={<p>Loading...</p>}>
16+
<InnerRoute />
17+
</React.Suspense>
18+
}
19+
/>
20+
</SentryRoutes>
21+
);
22+
};
23+
24+
export default LazyLoadedUser;

dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts

+98
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,101 @@ test('Captures a navigation transaction', async ({ page }) => {
7676

7777
expect(transactionEvent.spans).toEqual([]);
7878
});
79+
80+
test('Captures a lazy pageload transaction', async ({ page }) => {
81+
const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
82+
return event.contexts?.trace?.op === 'pageload';
83+
});
84+
85+
await page.goto('/lazy-loaded-user/5/foo');
86+
87+
const transactionEvent = await transactionEventPromise;
88+
expect(transactionEvent.contexts?.trace).toEqual({
89+
data: expect.objectContaining({
90+
'sentry.idle_span_finish_reason': 'idleTimeout',
91+
'sentry.op': 'pageload',
92+
'sentry.origin': 'auto.pageload.react.reactrouter_v6',
93+
'sentry.sample_rate': 1,
94+
'sentry.source': 'route',
95+
}),
96+
op: 'pageload',
97+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
98+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
99+
origin: 'auto.pageload.react.reactrouter_v6',
100+
});
101+
102+
expect(transactionEvent).toEqual(
103+
expect.objectContaining({
104+
transaction: '/lazy-loaded-user/:id/:innerId',
105+
type: 'transaction',
106+
transaction_info: {
107+
source: 'route',
108+
},
109+
}),
110+
);
111+
112+
expect(await page.innerText("id=content")).toContain('I am a lazy loaded user');
113+
114+
expect(transactionEvent.spans).toEqual(expect.arrayContaining([
115+
// This one is the outer lazy route
116+
expect.objectContaining({
117+
op: 'resource.script',
118+
origin: 'auto.resource.browser.metrics',
119+
}),
120+
// This one is the inner lazy route
121+
expect.objectContaining({
122+
op: 'resource.script',
123+
origin: 'auto.resource.browser.metrics',
124+
}),
125+
]));
126+
});
127+
128+
test('Captures a lazy navigation transaction', async ({ page }) => {
129+
const transactionEventPromise = waitForTransaction('react-create-browser-router', event => {
130+
return event.contexts?.trace?.op === 'navigation';
131+
});
132+
133+
await page.goto('/');
134+
const linkElement = page.locator('id=lazy-navigation');
135+
await linkElement.click();
136+
137+
const transactionEvent = await transactionEventPromise;
138+
expect(transactionEvent.contexts?.trace).toEqual({
139+
data: expect.objectContaining({
140+
'sentry.idle_span_finish_reason': 'idleTimeout',
141+
'sentry.op': 'navigation',
142+
'sentry.origin': 'auto.navigation.react.reactrouter_v6',
143+
'sentry.sample_rate': 1,
144+
'sentry.source': 'route',
145+
}),
146+
op: 'navigation',
147+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
148+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
149+
origin: 'auto.navigation.react.reactrouter_v6',
150+
});
151+
152+
expect(transactionEvent).toEqual(
153+
expect.objectContaining({
154+
transaction: '/lazy-loaded-user/:id/:innerId',
155+
type: 'transaction',
156+
transaction_info: {
157+
source: 'route',
158+
},
159+
}),
160+
);
161+
162+
expect(await page.innerText("id=content")).toContain('I am a lazy loaded user');
163+
164+
expect(transactionEvent.spans).toEqual(expect.arrayContaining([
165+
// This one is the outer lazy route
166+
expect.objectContaining({
167+
op: 'resource.script',
168+
origin: 'auto.resource.browser.metrics',
169+
}),
170+
// This one is the inner lazy route
171+
expect.objectContaining({
172+
op: 'resource.script',
173+
origin: 'auto.resource.browser.metrics',
174+
}),
175+
]));
176+
});

packages/react/src/reactrouterv6-compat-utils.tsx

+25-9
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export interface ReactRouterOptions {
6161

6262
type V6CompatibleVersion = '6' | '7';
6363

64+
// Keeping as a global variable for cross-usage in multiple functions
65+
const allRoutes = new Set<RouteObject>();
66+
6467
/**
6568
* Creates a wrapCreateBrowserRouter function that can be used with all React Router v6 compatible versions.
6669
*/
@@ -81,6 +84,10 @@ export function createV6CompatibleWrapCreateBrowserRouter<
8184
}
8285

8386
return function (routes: RouteObject[], opts?: Record<string, unknown> & { basename?: string }): TRouter {
87+
routes.forEach(route => {
88+
allRoutes.add(route);
89+
});
90+
8491
const router = createRouterFunction(routes, opts);
8592
const basename = opts?.basename;
8693

@@ -90,7 +97,14 @@ export function createV6CompatibleWrapCreateBrowserRouter<
9097
// This is the earliest convenient time to update the transaction name.
9198
// Callbacks to `router.subscribe` are not called for the initial load.
9299
if (router.state.historyAction === 'POP' && activeRootSpan) {
93-
updatePageloadTransaction(activeRootSpan, router.state.location, routes, undefined, basename);
100+
updatePageloadTransaction(
101+
activeRootSpan,
102+
router.state.location,
103+
routes,
104+
undefined,
105+
basename,
106+
Array.from(allRoutes),
107+
);
94108
}
95109

96110
router.subscribe((state: RouterState) => {
@@ -104,6 +118,7 @@ export function createV6CompatibleWrapCreateBrowserRouter<
104118
navigationType: state.historyAction,
105119
version,
106120
basename,
121+
allRoutes: Array.from(allRoutes),
107122
});
108123
});
109124
} else {
@@ -113,6 +128,7 @@ export function createV6CompatibleWrapCreateBrowserRouter<
113128
navigationType: state.historyAction,
114129
version,
115130
basename,
131+
allRoutes: Array.from(allRoutes),
116132
});
117133
}
118134
}
@@ -149,6 +165,10 @@ export function createV6CompatibleWrapCreateMemoryRouter<
149165
initialIndex?: number;
150166
},
151167
): TRouter {
168+
routes.forEach(route => {
169+
allRoutes.add(route);
170+
});
171+
152172
const router = createRouterFunction(routes, opts);
153173
const basename = opts?.basename;
154174

@@ -174,7 +194,7 @@ export function createV6CompatibleWrapCreateMemoryRouter<
174194
: router.state.location;
175195

176196
if (router.state.historyAction === 'POP' && activeRootSpan) {
177-
updatePageloadTransaction(activeRootSpan, location, routes, undefined, basename);
197+
updatePageloadTransaction(activeRootSpan, location, routes, undefined, basename, Array.from(allRoutes));
178198
}
179199

180200
router.subscribe((state: RouterState) => {
@@ -186,6 +206,7 @@ export function createV6CompatibleWrapCreateMemoryRouter<
186206
navigationType: state.historyAction,
187207
version,
188208
basename,
209+
allRoutes: Array.from(allRoutes),
189210
});
190211
}
191212
});
@@ -260,8 +281,6 @@ export function createV6CompatibleWrapUseRoutes(origUseRoutes: UseRoutes, versio
260281
return origUseRoutes;
261282
}
262283

263-
const allRoutes: Set<RouteObject> = new Set();
264-
265284
const SentryRoutes: React.FC<{
266285
children?: React.ReactNode;
267286
routes: RouteObject[];
@@ -331,7 +350,6 @@ export function handleNavigation(opts: {
331350
allRoutes?: RouteObject[];
332351
}): void {
333352
const { location, routes, navigationType, version, matches, basename, allRoutes } = opts;
334-
335353
const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename);
336354

337355
const client = getClient();
@@ -565,7 +583,7 @@ function updatePageloadTransaction(
565583
): void {
566584
const branches = Array.isArray(matches)
567585
? matches
568-
: (_matchRoutes(routes, location, basename) as unknown as RouteMatch[]);
586+
: (_matchRoutes(allRoutes || routes, location, basename) as unknown as RouteMatch[]);
569587

570588
if (branches) {
571589
let name,
@@ -581,7 +599,7 @@ function updatePageloadTransaction(
581599
[name, source] = getNormalizedName(routes, location, branches, basename);
582600
}
583601

584-
getCurrentScope().setTransactionName(name);
602+
getCurrentScope().setTransactionName(name || '/');
585603

586604
if (activeRootSpan) {
587605
activeRootSpan.updateName(name);
@@ -604,8 +622,6 @@ export function createV6CompatibleWithSentryReactRouterRouting<P extends Record<
604622
return Routes;
605623
}
606624

607-
const allRoutes: Set<RouteObject> = new Set();
608-
609625
const SentryRoutes: React.FC<P> = (props: P) => {
610626
const isMountRenderPass = React.useRef(true);
611627

0 commit comments

Comments
 (0)