123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- import { browserTracingIntegration, WINDOW, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan } from '@sentry/browser';
- import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan, getRootSpan, spanToJSON } from '@sentry/core';
- import { logger, getNumberOfUrlSegments } from '@sentry/utils';
- import hoistNonReactStatics from 'hoist-non-react-statics';
- import * as React from 'react';
- import { DEBUG_BUILD } from './debug-build.js';
- const _jsxFileName = "/home/runner/work/sentry-javascript/sentry-javascript/packages/react/src/reactrouterv6.tsx";/* eslint-disable max-lines */
- let activeTransaction;
- let _useEffect;
- let _useLocation;
- let _useNavigationType;
- let _createRoutesFromChildren;
- let _matchRoutes;
- let _customStartTransaction;
- let _startTransactionOnLocationChange;
- let _stripBasename = false;
- /**
- * A browser tracing integration that uses React Router v3 to instrument navigations.
- * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
- */
- function reactRouterV6BrowserTracingIntegration(
- options,
- ) {
- const integration = browserTracingIntegration({
- ...options,
- instrumentPageLoad: false,
- instrumentNavigation: false,
- });
- const {
- useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- stripBasename,
- instrumentPageLoad = true,
- instrumentNavigation = true,
- } = options;
- return {
- ...integration,
- afterAllSetup(client) {
- integration.afterAllSetup(client);
- const startNavigationCallback = (startSpanOptions) => {
- startBrowserTracingNavigationSpan(client, startSpanOptions);
- return undefined;
- };
- const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname;
- if (instrumentPageLoad && initPathName) {
- startBrowserTracingPageLoadSpan(client, {
- name: initPathName,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
- },
- });
- }
- _useEffect = useEffect;
- _useLocation = useLocation;
- _useNavigationType = useNavigationType;
- _matchRoutes = matchRoutes;
- _createRoutesFromChildren = createRoutesFromChildren;
- _stripBasename = stripBasename || false;
- _customStartTransaction = startNavigationCallback;
- _startTransactionOnLocationChange = instrumentNavigation;
- },
- };
- }
- /**
- * @deprecated Use `reactRouterV6BrowserTracingIntegration()` instead.
- */
- function reactRouterV6Instrumentation(
- useEffect,
- useLocation,
- useNavigationType,
- createRoutesFromChildren,
- matchRoutes,
- stripBasename,
- ) {
- return (
- customStartTransaction,
- startTransactionOnPageLoad = true,
- startTransactionOnLocationChange = true,
- ) => {
- const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname;
- if (startTransactionOnPageLoad && initPathName) {
- activeTransaction = customStartTransaction({
- name: initPathName,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
- },
- });
- }
- _useEffect = useEffect;
- _useLocation = useLocation;
- _useNavigationType = useNavigationType;
- _matchRoutes = matchRoutes;
- _createRoutesFromChildren = createRoutesFromChildren;
- _stripBasename = stripBasename || false;
- _customStartTransaction = customStartTransaction;
- _startTransactionOnLocationChange = startTransactionOnLocationChange;
- };
- }
- /**
- * Strip the basename from a pathname if exists.
- *
- * Vendored and modified from `react-router`
- * https://github.com/remix-run/react-router/blob/462bb712156a3f739d6139a0f14810b76b002df6/packages/router/utils.ts#L1038
- */
- function stripBasenameFromPathname(pathname, basename) {
- if (!basename || basename === '/') {
- return pathname;
- }
- if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
- return pathname;
- }
- // We want to leave trailing slash behavior in the user's control, so if they
- // specify a basename with a trailing slash, we should support it
- const startIndex = basename.endsWith('/') ? basename.length - 1 : basename.length;
- const nextChar = pathname.charAt(startIndex);
- if (nextChar && nextChar !== '/') {
- // pathname does not start with basename/
- return pathname;
- }
- return pathname.slice(startIndex) || '/';
- }
- function getNormalizedName(
- routes,
- location,
- branches,
- basename = '',
- ) {
- if (!routes || routes.length === 0) {
- return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url'];
- }
- let pathBuilder = '';
- if (branches) {
- // eslint-disable-next-line @typescript-eslint/prefer-for-of
- for (let x = 0; x < branches.length; x++) {
- const branch = branches[x];
- const route = branch.route;
- if (route) {
- // Early return if index route
- if (route.index) {
- return [_stripBasename ? stripBasenameFromPathname(branch.pathname, basename) : branch.pathname, 'route'];
- }
- const path = route.path;
- if (path) {
- const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`;
- pathBuilder += newPath;
- if (basename + branch.pathname === location.pathname) {
- if (
- // If the route defined on the element is something like
- // <Route path="/stores/:storeId/products/:productId" element={<div>Product</div>} />
- // We should check against the branch.pathname for the number of / seperators
- getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) &&
- // We should not count wildcard operators in the url segments calculation
- pathBuilder.slice(-2) !== '/*'
- ) {
- return [(_stripBasename ? '' : basename) + newPath, 'route'];
- }
- return [(_stripBasename ? '' : basename) + pathBuilder, 'route'];
- }
- }
- }
- }
- }
- return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url'];
- }
- function updatePageloadTransaction(
- activeRootSpan,
- location,
- routes,
- matches,
- basename,
- ) {
- const branches = Array.isArray(matches)
- ? matches
- : (_matchRoutes(routes, location, basename) );
- if (activeRootSpan && branches) {
- const [name, source] = getNormalizedName(routes, location, branches, basename);
- activeRootSpan.updateName(name);
- activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
- }
- }
- function handleNavigation(
- location,
- routes,
- navigationType,
- matches,
- basename,
- ) {
- const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename);
- if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP') && branches) {
- if (activeTransaction) {
- activeTransaction.end();
- }
- const [name, source] = getNormalizedName(routes, location, branches, basename);
- activeTransaction = _customStartTransaction({
- name,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
- },
- });
- }
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- function withSentryReactRouterV6Routing(Routes) {
- if (
- !_useEffect ||
- !_useLocation ||
- !_useNavigationType ||
- !_createRoutesFromChildren ||
- !_matchRoutes ||
- !_customStartTransaction
- ) {
- DEBUG_BUILD &&
- logger.warn(`reactRouterV6Instrumentation was unable to wrap Routes because of one or more missing parameters.
- useEffect: ${_useEffect}. useLocation: ${_useLocation}. useNavigationType: ${_useNavigationType}.
- createRoutesFromChildren: ${_createRoutesFromChildren}. matchRoutes: ${_matchRoutes}. customStartTransaction: ${_customStartTransaction}.`);
- return Routes;
- }
- let isMountRenderPass = true;
- const SentryRoutes = (props) => {
- const location = _useLocation();
- const navigationType = _useNavigationType();
- _useEffect(
- () => {
- const routes = _createRoutesFromChildren(props.children) ;
- if (isMountRenderPass) {
- updatePageloadTransaction(getActiveRootSpan(), location, routes);
- isMountRenderPass = false;
- } else {
- handleNavigation(location, routes, navigationType);
- }
- },
- // `props.children` is purpusely not included in the dependency array, because we do not want to re-run this effect
- // when the children change. We only want to start transactions when the location or navigation type change.
- [location, navigationType],
- );
- // @ts-expect-error Setting more specific React Component typing for `R` generic above
- // will break advanced type inference done by react router params
- return React.createElement(Routes, { ...props, __self: this, __source: {fileName: _jsxFileName, lineNumber: 329}} );
- };
- hoistNonReactStatics(SentryRoutes, Routes);
- // @ts-expect-error Setting more specific React Component typing for `R` generic above
- // will break advanced type inference done by react router params
- return SentryRoutes;
- }
- function wrapUseRoutes(origUseRoutes) {
- if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes || !_customStartTransaction) {
- DEBUG_BUILD &&
- logger.warn(
- 'reactRouterV6Instrumentation was unable to wrap `useRoutes` because of one or more missing parameters.',
- );
- return origUseRoutes;
- }
- let isMountRenderPass = true;
- const SentryRoutes
- = (props) => {
- const { routes, locationArg } = props;
- const Routes = origUseRoutes(routes, locationArg);
- const location = _useLocation();
- const navigationType = _useNavigationType();
- // A value with stable identity to either pick `locationArg` if available or `location` if not
- const stableLocationParam =
- typeof locationArg === 'string' || (locationArg && locationArg.pathname)
- ? (locationArg )
- : location;
- _useEffect(() => {
- const normalizedLocation =
- typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam;
- if (isMountRenderPass) {
- updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes);
- isMountRenderPass = false;
- } else {
- handleNavigation(normalizedLocation, routes, navigationType);
- }
- }, [navigationType, stableLocationParam]);
- return Routes;
- };
- // eslint-disable-next-line react/display-name
- return (routes, locationArg) => {
- return React.createElement(SentryRoutes, { routes: routes, locationArg: locationArg, __self: this, __source: {fileName: _jsxFileName, lineNumber: 386}} );
- };
- }
- function wrapCreateBrowserRouter
- (createRouterFunction) {
- // `opts` for createBrowserHistory and createMemoryHistory are different, but also not relevant for us at the moment.
- // `basename` is the only option that is relevant for us, and it is the same for all.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return function (routes, opts) {
- const router = createRouterFunction(routes, opts);
- const basename = opts && opts.basename;
- const activeRootSpan = getActiveRootSpan();
- // The initial load ends when `createBrowserRouter` is called.
- // This is the earliest convenient time to update the transaction name.
- // Callbacks to `router.subscribe` are not called for the initial load.
- if (router.state.historyAction === 'POP' && activeRootSpan) {
- updatePageloadTransaction(activeRootSpan, router.state.location, routes, undefined, basename);
- }
- router.subscribe((state) => {
- const location = state.location;
- if (_startTransactionOnLocationChange && (state.historyAction === 'PUSH' || state.historyAction === 'POP')) {
- handleNavigation(location, routes, state.historyAction, undefined, basename);
- }
- });
- return router;
- };
- }
- function getActiveRootSpan() {
- // 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;
- }
- export { reactRouterV6BrowserTracingIntegration, reactRouterV6Instrumentation, withSentryReactRouterV6Routing, wrapCreateBrowserRouter, wrapUseRoutes };
- //# sourceMappingURL=reactrouterv6.js.map
|