FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(react-router): Register a route provider backed by resolved routes · getsentry/sentry-javascript@2d6425b · GitHub

Commit 2d6425b

Browse files
committed
feat(react-router): Register a route provider backed by resolved routes
The Data Router exposes matches only through router state, and the package has no runtime dependency on `react-router` to call `matchRoutes` with, so the provider answers from routes already resolved rather than by matching. Splits `getMatchedRoute` out of `getParameterizedRoute` so the provider can tell a matched route from the raw-pathname fallback, and records from both the hydrated router subscription and the instrumentation API, since each resolves routes the other never sees.
1 parent d8d190e commit 2d6425b

5 files changed

Lines changed: 42 additions & 5 deletions

File tree

‎packages/react-router/src/client/createClientInstrumentation.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { startSpan } from '@sentry/core/browser';
1919
import type { ClientInstrumentation } from 'react-router';
2020
import { DEBUG_BUILD } from '../common/debug-build';
21+
import { routeProvider } from './routeCache';
2122
import { captureInstrumentationError, getPathFromRequest, getPattern, normalizeRoutePath } from '../common/utils';
2223
import {
2324
resolveNavigateAbsoluteUrl,
@@ -252,7 +253,7 @@ export function createSentryClientInstrumentation(
252253
const routePattern = pattern || urlPath;
253254
// Parameterize the active navigation root span. (Route hooks don't fire on initial
254255
// pageload, so this only affects navigations.)
255-
updateRootSpanRoute(routePattern, !!pattern);
256+
updateRootSpanRoute(routePattern, !!pattern, urlPath);
256257

257258
await startSpan(
258259
{
@@ -279,7 +280,7 @@ export function createSentryClientInstrumentation(
279280
const urlPath = getPathFromRequest(info.request);
280281
const pattern = normalizeRoutePath(getPattern(info));
281282
const routePattern = pattern || urlPath;
282-
updateRootSpanRoute(routePattern, !!pattern);
283+
updateRootSpanRoute(routePattern, !!pattern, urlPath);
283284

284285
await startSpan(
285286
{
@@ -365,13 +366,19 @@ export function createSentryClientInstrumentation(
365366

366367
/**
367368
* Updates the active navigation/pageload root span name with the parameterized route, so the
368-
* transaction reflects the parameterized route pattern (e.g. `/users/:id`).
369+
* transaction reflects the parameterized route pattern (e.g. `/users/:id`), and records the route
370+
* against `urlPath` for the route provider.
369371
*/
370-
function updateRootSpanRoute(routeName: string, hasPattern: boolean): void {
372+
function updateRootSpanRoute(routeName: string, hasPattern: boolean, urlPath: string): void {
371373
if (!hasPattern) {
372374
return;
373375
}
374376

377+
// The instrumentation API resolves routes the hydrated router subscription never sees, so feed the
378+
// provider from here too. Keyed on the request path rather than `location`, because route hooks
379+
// run during the navigation, before the URL commits.
380+
routeProvider.record(urlPath, routeName);
381+
375382
const activeSpan = getActiveSpan();
376383
const rootSpan = activeSpan && getRootSpan(activeSpan);
377384
if (!rootSpan) {

‎packages/react-router/src/client/hydratedRouter.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import {
1515
import type { DataRouter } from 'react-router';
1616
import { DEBUG_BUILD } from '../common/debug-build';
1717
import { isClientInstrumentationApiUsed } from './createClientInstrumentation';
18+
import { routeProvider } from './routeCache';
1819
import {
1920
finalizeNavigationSpanFromRouterState,
21+
getMatchedRoute,
2022
getParameterizedRoute,
2123
normalizePathname,
2224
resolveNavigateAbsoluteUrl,
@@ -46,6 +48,8 @@ export function instrumentHydratedRouter(): void {
4648

4749
if (router) {
4850
// The first time we hit the router, we try to update the pageload transaction
51+
routeProvider.record(router.state.location.pathname, getMatchedRoute(router.state));
52+
4953
const pageloadSpan = getActiveRootSpan();
5054

5155
if (pageloadSpan) {
@@ -121,6 +125,8 @@ export function instrumentHydratedRouter(): void {
121125
// whose route info only became available after `trySubscribe`, e.g. lazy routes) with the
122126
// parameterized route.
123127
router.subscribe(newState => {
128+
routeProvider.record(newState.location.pathname, getMatchedRoute(newState));
129+
124130
const rootSpan = getActiveRootSpan();
125131

126132
if (!rootSpan) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createCachedRouteProvider } from '@sentry/core';
2+
3+
// The Data Router exposes its matches only through router state, and the package has no runtime
4+
// dependency on `react-router` to call `matchRoutes` with. The provider answers from routes the
5+
// hydrated router has already resolved instead.
6+
export const routeProvider = createCachedRouteProvider();

‎packages/react-router/src/client/tracingIntegration.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { browserTracingIntegration as originalBrowserTracingIntegration } from '@sentry/browser';
22
import type { Integration } from '@sentry/core';
3+
import { setRouteProvider } from '@sentry/core';
34
import type { ClientInstrumentation } from 'react-router';
45
import {
56
createSentryClientInstrumentation,
67
type CreateSentryClientInstrumentationOptions,
78
} from './createClientInstrumentation';
89
import { instrumentHydratedRouter } from './hydratedRouter';
10+
import { routeProvider } from './routeCache';
911

1012
/**
1113
* Options for the React Router tracing integration.
@@ -53,6 +55,10 @@ export function reactRouterTracingIntegration(
5355
return {
5456
...browserTracingIntegrationInstance,
5557
name: 'ReactRouterTracingIntegration',
58+
setup(client) {
59+
setRouteProvider(routeProvider, client);
60+
browserTracingIntegrationInstance.setup?.(client);
61+
},
5662
afterAllSetup(client) {
5763
browserTracingIntegrationInstance.afterAllSetup(client);
5864
instrumentHydratedRouter();

‎packages/react-router/src/client/utils.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,20 @@ export function normalizePathname(pathname: string): string {
119119
}
120120

121121
export function getParameterizedRoute(routerState: RouterState): string {
122+
return getMatchedRoute(routerState) ?? normalizePathname(routerState.location.pathname);
123+
}
124+
125+
/**
126+
* The parameterized route the router matched, or `undefined` when nothing matched.
127+
*
128+
* Unlike {@link getParameterizedRoute} this does not fall back to the raw pathname, so callers that
129+
* must not treat a URL as a route (the route provider) can tell the two apart.
130+
*/
131+
export function getMatchedRoute(routerState: RouterState): string | undefined {
122132
const lastMatch = routerState.matches[routerState.matches.length - 1];
123-
return normalizePathname(lastMatch?.route.path || routerState.location.pathname);
133+
const path = lastMatch?.route.path;
134+
135+
return path ? normalizePathname(path) : undefined;
124136
}
125137

126138
/**

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL