Skip to content

Commit

Permalink
feat(react): Add reactRouterV4/V5BrowserTracingIntegration for reac…
Browse files Browse the repository at this point in the history
…t router v4 & v5 (#10488)

This adds new `reactRouterV4BrowserTracingIntegration()` and
`reactRouterV5BrowserTracingIntegration()` exports, deprecating these
old routing instrumentations.

I opted to leave as much as possible as-is for now, except for
streamlining the attributes/tags we use for the instrumentation.

Tests lifted from
#10430
  • Loading branch information
mydea committed Feb 5, 2024
1 parent 8176f01 commit ef9196b
Show file tree
Hide file tree
Showing 4 changed files with 934 additions and 106 deletions.
10 changes: 9 additions & 1 deletion packages/react/src/index.ts
Expand Up @@ -6,7 +6,15 @@ export type { ErrorBoundaryProps, FallbackRender } from './errorboundary';
export { ErrorBoundary, withErrorBoundary } from './errorboundary';
export { createReduxEnhancer } from './redux';
export { reactRouterV3Instrumentation } from './reactrouterv3';
export { reactRouterV4Instrumentation, reactRouterV5Instrumentation, withSentryRouting } from './reactrouter';
export {
// eslint-disable-next-line deprecation/deprecation
reactRouterV4Instrumentation,
// eslint-disable-next-line deprecation/deprecation
reactRouterV5Instrumentation,
withSentryRouting,
reactRouterV4BrowserTracingIntegration,
reactRouterV5BrowserTracingIntegration,
} from './reactrouter';
export {
reactRouterV6Instrumentation,
withSentryReactRouterV6Routing,
Expand Down
168 changes: 144 additions & 24 deletions packages/react/src/reactrouter.tsx
@@ -1,6 +1,18 @@
import { WINDOW } from '@sentry/browser';
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
import type { Transaction, TransactionSource } from '@sentry/types';
import {
WINDOW,
browserTracingIntegration,
startBrowserTracingNavigationSpan,
startBrowserTracingPageLoadSpan,
} from '@sentry/browser';
import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getActiveSpan,
getRootSpan,
spanToJSON,
} from '@sentry/core';
import type { Integration, Span, StartSpanOptions, Transaction, TransactionSource } from '@sentry/types';
import hoistNonReactStatics from 'hoist-non-react-statics';
import * as React from 'react';

Expand All @@ -23,29 +35,121 @@ export type RouteConfig = {
routes?: RouteConfig[];
};

type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null; // eslint-disable-line @typescript-eslint/no-explicit-any
export type MatchPath = (pathname: string, props: string | string[] | any, parent?: Match | null) => Match | null; // eslint-disable-line @typescript-eslint/no-explicit-any

interface ReactRouterOptions {
history: RouterHistory;
routes?: RouteConfig[];
matchPath?: MatchPath;
}

let activeTransaction: Transaction | undefined;

/**
* A browser tracing integration that uses React Router v4 to instrument navigations.
* Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
*/
export function reactRouterV4BrowserTracingIntegration(
options: Parameters<typeof browserTracingIntegration>[0] & ReactRouterOptions,
): Integration {
const integration = browserTracingIntegration({
...options,
instrumentPageLoad: false,
instrumentNavigation: false,
});

const { history, routes, matchPath, instrumentPageLoad = true, instrumentNavigation = true } = options;

return {
...integration,
afterAllSetup(client) {
integration.afterAllSetup(client);

const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => {
startBrowserTracingPageLoadSpan(client, startSpanOptions);
return undefined;
};

const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => {
startBrowserTracingNavigationSpan(client, startSpanOptions);
return undefined;
};

// eslint-disable-next-line deprecation/deprecation
const instrumentation = reactRouterV4Instrumentation(history, routes, matchPath);

// Now instrument page load & navigation with correct settings
instrumentation(startPageloadCallback, instrumentPageLoad, false);
instrumentation(startNavigationCallback, false, instrumentNavigation);
},
};
}

/**
* A browser tracing integration that uses React Router v5 to instrument navigations.
* Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
*/
export function reactRouterV5BrowserTracingIntegration(
options: Parameters<typeof browserTracingIntegration>[0] & ReactRouterOptions,
): Integration {
const integration = browserTracingIntegration({
...options,
instrumentPageLoad: false,
instrumentNavigation: false,
});

const { history, routes, matchPath } = options;

return {
...integration,
afterAllSetup(client) {
integration.afterAllSetup(client);

const startPageloadCallback = (startSpanOptions: StartSpanOptions): undefined => {
startBrowserTracingPageLoadSpan(client, startSpanOptions);
return undefined;
};

const startNavigationCallback = (startSpanOptions: StartSpanOptions): undefined => {
startBrowserTracingNavigationSpan(client, startSpanOptions);
return undefined;
};

// eslint-disable-next-line deprecation/deprecation
const instrumentation = reactRouterV5Instrumentation(history, routes, matchPath);

// Now instrument page load & navigation with correct settings
instrumentation(startPageloadCallback, options.instrumentPageLoad, false);
instrumentation(startNavigationCallback, false, options.instrumentNavigation);
},
};
}

/**
* @deprecated Use `browserTracingReactRouterV4()` instead.
*/
export function reactRouterV4Instrumentation(
history: RouterHistory,
routes?: RouteConfig[],
matchPath?: MatchPath,
): ReactRouterInstrumentation {
return createReactRouterInstrumentation(history, 'react-router-v4', routes, matchPath);
return createReactRouterInstrumentation(history, 'reactrouter_v4', routes, matchPath);
}

/**
* @deprecated Use `browserTracingReactRouterV5()` instead.
*/
export function reactRouterV5Instrumentation(
history: RouterHistory,
routes?: RouteConfig[],
matchPath?: MatchPath,
): ReactRouterInstrumentation {
return createReactRouterInstrumentation(history, 'react-router-v5', routes, matchPath);
return createReactRouterInstrumentation(history, 'reactrouter_v5', routes, matchPath);
}

function createReactRouterInstrumentation(
history: RouterHistory,
name: string,
instrumentationName: string,
allRoutes: RouteConfig[] = [],
matchPath?: MatchPath,
): ReactRouterInstrumentation {
Expand Down Expand Up @@ -83,21 +187,17 @@ function createReactRouterInstrumentation(
return [pathname, 'url'];
}

const tags = {
'routing.instrumentation': name,
};

return (customStartTransaction, startTransactionOnPageLoad = true, startTransactionOnLocationChange = true): void => {
const initPathName = getInitPathName();

if (startTransactionOnPageLoad && initPathName) {
const [name, source] = normalizeTransactionName(initPathName);
activeTransaction = customStartTransaction({
name,
op: 'pageload',
origin: 'auto.pageload.react.reactrouter',
tags,
metadata: {
source,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
},
});
}
Expand All @@ -112,11 +212,10 @@ function createReactRouterInstrumentation(
const [name, source] = normalizeTransactionName(location.pathname);
activeTransaction = customStartTransaction({
name,
op: 'navigation',
origin: 'auto.navigation.react.reactrouter',
tags,
metadata: {
source,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
},
});
}
Expand Down Expand Up @@ -164,10 +263,12 @@ function computeRootMatch(pathname: string): Match {
export function withSentryRouting<P extends Record<string, any>, R extends React.ComponentType<P>>(Route: R): R {
const componentDisplayName = (Route as any).displayName || (Route as any).name;

const activeRootSpan = getActiveRootSpan();

const WrappedRoute: React.FC<P> = (props: P) => {
if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) {
activeTransaction.updateName(props.computedMatch.path);
activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
if (activeRootSpan && props && props.computedMatch && props.computedMatch.isExact) {
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
Expand All @@ -184,3 +285,22 @@ export function withSentryRouting<P extends Record<string, any>, R extends React
return WrappedRoute;
}
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */

function getActiveRootSpan(): Span | undefined {
// Legacy behavior for "old" react router instrumentation
if (activeTransaction) {
return activeTransaction;
}

const span = getActiveSpan();
const rootSpan = span ? getRootSpan(span) : undefined;

if (!rootSpan) {
return undefined;
}

const op = spanToJSON(rootSpan).op;

// Only use this root span if it is a pageload or navigation span
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
}

0 comments on commit ef9196b

Please sign in to comment.