Skip to content

Commit 06e535e

Browse files
committed
Add support for parameterized pageload transactions
1 parent d8216c1 commit 06e535e

File tree

4 files changed

+400
-30
lines changed

4 files changed

+400
-30
lines changed

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/src/index.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,27 +46,22 @@ const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV7(createBrowser
4646
const router = sentryCreateBrowserRouter(
4747
[
4848
{
49+
path: '/',
4950
element: <Index />,
50-
children: [
51-
{
52-
path: '/',
53-
element: <Index />,
54-
},
55-
{
56-
path: '/lazy',
57-
handle: {
58-
lazyChildren: () => import('./pages/InnerLazyRoutes').then(module => module.someMoreNestedRoutes),
59-
},
60-
},
61-
{
62-
path: '/static',
63-
element: <>Hello World</>,
64-
},
65-
{
66-
path: '*',
67-
element: <Navigate to="/\" replace />,
68-
},
69-
],
51+
},
52+
{
53+
path: '/lazy',
54+
handle: {
55+
lazyChildren: () => import('./pages/InnerLazyRoutes').then(module => module.someMoreNestedRoutes),
56+
},
57+
},
58+
{
59+
path: '/static',
60+
element: <>Hello World</>,
61+
},
62+
{
63+
path: '*',
64+
element: <Navigate to="/\" replace />,
7065
},
7166
],
7267
{

dev-packages/e2e-tests/test-applications/react-router-7-lazy-routes/tests/transactions.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
33

4-
test('sends a navigation transaction for lazy route', async ({ page }) => {
4+
5+
test('Creates a pageload transaction with parameterized route', async ({ page }) => {
6+
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
7+
console.debug('transactionEvent', transactionEvent);
8+
9+
return (
10+
!!transactionEvent?.transaction &&
11+
transactionEvent.contexts?.trace?.op === 'pageload' &&
12+
transactionEvent.transaction === '/lazy/inner/:id/:anotherId/:someAnotherId'
13+
);
14+
});
15+
16+
await page.goto('/lazy/inner/1/2/3');
17+
const event = await transactionPromise;
18+
19+
20+
const lazyRouteContent = page.locator('id=innermost-lazy-route');
21+
22+
await expect(lazyRouteContent).toBeVisible();
23+
24+
// Validate the transaction event
25+
expect(event.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId');
26+
expect(event.type).toBe('transaction');
27+
expect(event.contexts?.trace?.op).toBe('pageload');
28+
});
29+
30+
test('Creates a navigation transaction inside a lazy route', async ({ page }) => {
531
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
6-
console.debug('Lazy route transaction event', transactionEvent);
732
return (
833
!!transactionEvent?.transaction &&
934
transactionEvent.contexts?.trace?.op === 'navigation' &&
@@ -12,10 +37,21 @@ test('sends a navigation transaction for lazy route', async ({ page }) => {
1237
});
1338

1439
await page.goto('/');
15-
await page.locator('id=navigation').click();
1640

41+
// Check if the navigation link exists
42+
const navigationLink = page.locator('id=navigation');
43+
await expect(navigationLink).toBeVisible();
44+
45+
// Click the navigation link to navigate to the lazy route
46+
await navigationLink.click();
1747
const event = await transactionPromise;
1848

49+
// Check if the lazy route content is rendered
50+
const lazyRouteContent = page.locator('id=innermost-lazy-route');
51+
52+
await expect(lazyRouteContent).toBeVisible();
53+
54+
// Validate the transaction event
1955
expect(event.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId');
2056
expect(event.type).toBe('transaction');
2157
expect(event.contexts?.trace?.op).toBe('navigation');

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

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@sentry/browser';
1111
import type { Client, Integration, Span, TransactionSource } from '@sentry/core';
1212
import {
13+
addNonEnumerableProperty,
1314
debug,
1415
getActiveSpan,
1516
getClient,
@@ -63,6 +64,15 @@ type V6CompatibleVersion = '6' | '7';
6364
// Keeping as a global variable for cross-usage in multiple functions
6465
const allRoutes = new Set<RouteObject>();
6566

67+
/**
68+
* Adds resolved routes as children to the parent route.
69+
*/
70+
function addResolvedRoutesToParent(resolvedRoutes: RouteObject[], parentRoute: RouteObject): void {
71+
parentRoute.children = Array.isArray(parentRoute.children)
72+
? [...parentRoute.children, ...resolvedRoutes]
73+
: resolvedRoutes;
74+
}
75+
6676
/**
6777
* Handles the result of an async handler function call.
6878
*/
@@ -76,25 +86,47 @@ function handleAsyncHandlerResult(result: unknown, route: RouteObject, handlerKe
7686
(result as Promise<unknown>)
7787
.then((resolvedRoutes: unknown) => {
7888
if (Array.isArray(resolvedRoutes)) {
79-
processResolvedRoutes(resolvedRoutes);
89+
processResolvedRoutes(resolvedRoutes, route);
8090
}
8191
})
8292
.catch((e: unknown) => {
8393
DEBUG_BUILD && debug.warn(`Error resolving async handler '${handlerKey}' for route`, route, e);
8494
});
8595
} else if (Array.isArray(result)) {
86-
processResolvedRoutes(result);
96+
processResolvedRoutes(result, route);
8797
}
8898
}
8999

90100
/**
91101
* Processes resolved routes by adding them to allRoutes and checking for nested async handlers.
92102
*/
93-
function processResolvedRoutes(resolvedRoutes: RouteObject[]): void {
103+
function processResolvedRoutes(resolvedRoutes: RouteObject[], parentRoute?: RouteObject): void {
94104
resolvedRoutes.forEach(child => {
95105
allRoutes.add(child);
96106
checkRouteForAsyncHandler(child);
97107
});
108+
109+
if (parentRoute) {
110+
// If a parent route is provided, add the resolved routes as children to the parent route
111+
addResolvedRoutesToParent(resolvedRoutes, parentRoute);
112+
}
113+
114+
// After processing lazy routes, check if we need to update an active pageload transaction
115+
const activeRootSpan = getActiveRootSpan();
116+
if (activeRootSpan && spanToJSON(activeRootSpan).op === 'pageload') {
117+
const location = WINDOW.location;
118+
if (location) {
119+
// Re-run the pageload transaction update with the newly loaded routes
120+
updatePageloadTransaction(
121+
activeRootSpan,
122+
{ pathname: location.pathname },
123+
Array.from(allRoutes),
124+
undefined,
125+
undefined,
126+
Array.from(allRoutes),
127+
);
128+
}
129+
}
98130
}
99131

100132
/**
@@ -105,13 +137,17 @@ function createAsyncHandlerProxy(
105137
route: RouteObject,
106138
handlerKey: string,
107139
): (...args: unknown[]) => unknown {
108-
return new Proxy(originalFunction, {
140+
const proxy = new Proxy(originalFunction, {
109141
apply(target: (...args: unknown[]) => unknown, thisArg, argArray) {
110142
const result = target.apply(thisArg, argArray);
111143
handleAsyncHandlerResult(result, route, handlerKey);
112144
return result;
113145
},
114146
});
147+
148+
addNonEnumerableProperty(proxy, '__sentry_proxied__', true);
149+
150+
return proxy;
115151
}
116152

117153
/**
@@ -122,7 +158,7 @@ export function checkRouteForAsyncHandler(route: RouteObject): void {
122158
if (route.handle && typeof route.handle === 'object') {
123159
for (const key of Object.keys(route.handle)) {
124160
const maybeFn = route.handle[key];
125-
if (typeof maybeFn === 'function') {
161+
if (typeof maybeFn === 'function' && !(maybeFn as { __sentry_proxied__?: boolean }).__sentry_proxied__) {
126162
route.handle[key] = createAsyncHandlerProxy(maybeFn, route, key);
127163
}
128164
}
@@ -620,6 +656,7 @@ function getNormalizedName(
620656
}
621657

622658
let pathBuilder = '';
659+
623660
if (branches) {
624661
for (const branch of branches) {
625662
const route = branch.route;

0 commit comments

Comments
 (0)