reactrouterv6.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import { browserTracingIntegration, WINDOW, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan } from '@sentry/browser';
  2. import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan, getRootSpan, spanToJSON } from '@sentry/core';
  3. import { logger, getNumberOfUrlSegments } from '@sentry/utils';
  4. import hoistNonReactStatics from 'hoist-non-react-statics';
  5. import * as React from 'react';
  6. import { DEBUG_BUILD } from './debug-build.js';
  7. const _jsxFileName = "/home/runner/work/sentry-javascript/sentry-javascript/packages/react/src/reactrouterv6.tsx";/* eslint-disable max-lines */
  8. let activeTransaction;
  9. let _useEffect;
  10. let _useLocation;
  11. let _useNavigationType;
  12. let _createRoutesFromChildren;
  13. let _matchRoutes;
  14. let _customStartTransaction;
  15. let _startTransactionOnLocationChange;
  16. let _stripBasename = false;
  17. /**
  18. * A browser tracing integration that uses React Router v3 to instrument navigations.
  19. * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
  20. */
  21. function reactRouterV6BrowserTracingIntegration(
  22. options,
  23. ) {
  24. const integration = browserTracingIntegration({
  25. ...options,
  26. instrumentPageLoad: false,
  27. instrumentNavigation: false,
  28. });
  29. const {
  30. useEffect,
  31. useLocation,
  32. useNavigationType,
  33. createRoutesFromChildren,
  34. matchRoutes,
  35. stripBasename,
  36. instrumentPageLoad = true,
  37. instrumentNavigation = true,
  38. } = options;
  39. return {
  40. ...integration,
  41. afterAllSetup(client) {
  42. integration.afterAllSetup(client);
  43. const startNavigationCallback = (startSpanOptions) => {
  44. startBrowserTracingNavigationSpan(client, startSpanOptions);
  45. return undefined;
  46. };
  47. const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname;
  48. if (instrumentPageLoad && initPathName) {
  49. startBrowserTracingPageLoadSpan(client, {
  50. name: initPathName,
  51. attributes: {
  52. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
  53. [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
  54. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
  55. },
  56. });
  57. }
  58. _useEffect = useEffect;
  59. _useLocation = useLocation;
  60. _useNavigationType = useNavigationType;
  61. _matchRoutes = matchRoutes;
  62. _createRoutesFromChildren = createRoutesFromChildren;
  63. _stripBasename = stripBasename || false;
  64. _customStartTransaction = startNavigationCallback;
  65. _startTransactionOnLocationChange = instrumentNavigation;
  66. },
  67. };
  68. }
  69. /**
  70. * @deprecated Use `reactRouterV6BrowserTracingIntegration()` instead.
  71. */
  72. function reactRouterV6Instrumentation(
  73. useEffect,
  74. useLocation,
  75. useNavigationType,
  76. createRoutesFromChildren,
  77. matchRoutes,
  78. stripBasename,
  79. ) {
  80. return (
  81. customStartTransaction,
  82. startTransactionOnPageLoad = true,
  83. startTransactionOnLocationChange = true,
  84. ) => {
  85. const initPathName = WINDOW && WINDOW.location && WINDOW.location.pathname;
  86. if (startTransactionOnPageLoad && initPathName) {
  87. activeTransaction = customStartTransaction({
  88. name: initPathName,
  89. attributes: {
  90. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
  91. [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
  92. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.react.reactrouter_v6',
  93. },
  94. });
  95. }
  96. _useEffect = useEffect;
  97. _useLocation = useLocation;
  98. _useNavigationType = useNavigationType;
  99. _matchRoutes = matchRoutes;
  100. _createRoutesFromChildren = createRoutesFromChildren;
  101. _stripBasename = stripBasename || false;
  102. _customStartTransaction = customStartTransaction;
  103. _startTransactionOnLocationChange = startTransactionOnLocationChange;
  104. };
  105. }
  106. /**
  107. * Strip the basename from a pathname if exists.
  108. *
  109. * Vendored and modified from `react-router`
  110. * https://github.com/remix-run/react-router/blob/462bb712156a3f739d6139a0f14810b76b002df6/packages/router/utils.ts#L1038
  111. */
  112. function stripBasenameFromPathname(pathname, basename) {
  113. if (!basename || basename === '/') {
  114. return pathname;
  115. }
  116. if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
  117. return pathname;
  118. }
  119. // We want to leave trailing slash behavior in the user's control, so if they
  120. // specify a basename with a trailing slash, we should support it
  121. const startIndex = basename.endsWith('/') ? basename.length - 1 : basename.length;
  122. const nextChar = pathname.charAt(startIndex);
  123. if (nextChar && nextChar !== '/') {
  124. // pathname does not start with basename/
  125. return pathname;
  126. }
  127. return pathname.slice(startIndex) || '/';
  128. }
  129. function getNormalizedName(
  130. routes,
  131. location,
  132. branches,
  133. basename = '',
  134. ) {
  135. if (!routes || routes.length === 0) {
  136. return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url'];
  137. }
  138. let pathBuilder = '';
  139. if (branches) {
  140. // eslint-disable-next-line @typescript-eslint/prefer-for-of
  141. for (let x = 0; x < branches.length; x++) {
  142. const branch = branches[x];
  143. const route = branch.route;
  144. if (route) {
  145. // Early return if index route
  146. if (route.index) {
  147. return [_stripBasename ? stripBasenameFromPathname(branch.pathname, basename) : branch.pathname, 'route'];
  148. }
  149. const path = route.path;
  150. if (path) {
  151. const newPath = path[0] === '/' || pathBuilder[pathBuilder.length - 1] === '/' ? path : `/${path}`;
  152. pathBuilder += newPath;
  153. if (basename + branch.pathname === location.pathname) {
  154. if (
  155. // If the route defined on the element is something like
  156. // <Route path="/stores/:storeId/products/:productId" element={<div>Product</div>} />
  157. // We should check against the branch.pathname for the number of / seperators
  158. getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) &&
  159. // We should not count wildcard operators in the url segments calculation
  160. pathBuilder.slice(-2) !== '/*'
  161. ) {
  162. return [(_stripBasename ? '' : basename) + newPath, 'route'];
  163. }
  164. return [(_stripBasename ? '' : basename) + pathBuilder, 'route'];
  165. }
  166. }
  167. }
  168. }
  169. }
  170. return [_stripBasename ? stripBasenameFromPathname(location.pathname, basename) : location.pathname, 'url'];
  171. }
  172. function updatePageloadTransaction(
  173. activeRootSpan,
  174. location,
  175. routes,
  176. matches,
  177. basename,
  178. ) {
  179. const branches = Array.isArray(matches)
  180. ? matches
  181. : (_matchRoutes(routes, location, basename) );
  182. if (activeRootSpan && branches) {
  183. const [name, source] = getNormalizedName(routes, location, branches, basename);
  184. activeRootSpan.updateName(name);
  185. activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source);
  186. }
  187. }
  188. function handleNavigation(
  189. location,
  190. routes,
  191. navigationType,
  192. matches,
  193. basename,
  194. ) {
  195. const branches = Array.isArray(matches) ? matches : _matchRoutes(routes, location, basename);
  196. if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP') && branches) {
  197. if (activeTransaction) {
  198. activeTransaction.end();
  199. }
  200. const [name, source] = getNormalizedName(routes, location, branches, basename);
  201. activeTransaction = _customStartTransaction({
  202. name,
  203. attributes: {
  204. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
  205. [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
  206. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.react.reactrouter_v6',
  207. },
  208. });
  209. }
  210. }
  211. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  212. function withSentryReactRouterV6Routing(Routes) {
  213. if (
  214. !_useEffect ||
  215. !_useLocation ||
  216. !_useNavigationType ||
  217. !_createRoutesFromChildren ||
  218. !_matchRoutes ||
  219. !_customStartTransaction
  220. ) {
  221. DEBUG_BUILD &&
  222. logger.warn(`reactRouterV6Instrumentation was unable to wrap Routes because of one or more missing parameters.
  223. useEffect: ${_useEffect}. useLocation: ${_useLocation}. useNavigationType: ${_useNavigationType}.
  224. createRoutesFromChildren: ${_createRoutesFromChildren}. matchRoutes: ${_matchRoutes}. customStartTransaction: ${_customStartTransaction}.`);
  225. return Routes;
  226. }
  227. let isMountRenderPass = true;
  228. const SentryRoutes = (props) => {
  229. const location = _useLocation();
  230. const navigationType = _useNavigationType();
  231. _useEffect(
  232. () => {
  233. const routes = _createRoutesFromChildren(props.children) ;
  234. if (isMountRenderPass) {
  235. updatePageloadTransaction(getActiveRootSpan(), location, routes);
  236. isMountRenderPass = false;
  237. } else {
  238. handleNavigation(location, routes, navigationType);
  239. }
  240. },
  241. // `props.children` is purpusely not included in the dependency array, because we do not want to re-run this effect
  242. // when the children change. We only want to start transactions when the location or navigation type change.
  243. [location, navigationType],
  244. );
  245. // @ts-expect-error Setting more specific React Component typing for `R` generic above
  246. // will break advanced type inference done by react router params
  247. return React.createElement(Routes, { ...props, __self: this, __source: {fileName: _jsxFileName, lineNumber: 329}} );
  248. };
  249. hoistNonReactStatics(SentryRoutes, Routes);
  250. // @ts-expect-error Setting more specific React Component typing for `R` generic above
  251. // will break advanced type inference done by react router params
  252. return SentryRoutes;
  253. }
  254. function wrapUseRoutes(origUseRoutes) {
  255. if (!_useEffect || !_useLocation || !_useNavigationType || !_matchRoutes || !_customStartTransaction) {
  256. DEBUG_BUILD &&
  257. logger.warn(
  258. 'reactRouterV6Instrumentation was unable to wrap `useRoutes` because of one or more missing parameters.',
  259. );
  260. return origUseRoutes;
  261. }
  262. let isMountRenderPass = true;
  263. const SentryRoutes
  264. = (props) => {
  265. const { routes, locationArg } = props;
  266. const Routes = origUseRoutes(routes, locationArg);
  267. const location = _useLocation();
  268. const navigationType = _useNavigationType();
  269. // A value with stable identity to either pick `locationArg` if available or `location` if not
  270. const stableLocationParam =
  271. typeof locationArg === 'string' || (locationArg && locationArg.pathname)
  272. ? (locationArg )
  273. : location;
  274. _useEffect(() => {
  275. const normalizedLocation =
  276. typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam;
  277. if (isMountRenderPass) {
  278. updatePageloadTransaction(getActiveRootSpan(), normalizedLocation, routes);
  279. isMountRenderPass = false;
  280. } else {
  281. handleNavigation(normalizedLocation, routes, navigationType);
  282. }
  283. }, [navigationType, stableLocationParam]);
  284. return Routes;
  285. };
  286. // eslint-disable-next-line react/display-name
  287. return (routes, locationArg) => {
  288. return React.createElement(SentryRoutes, { routes: routes, locationArg: locationArg, __self: this, __source: {fileName: _jsxFileName, lineNumber: 386}} );
  289. };
  290. }
  291. function wrapCreateBrowserRouter
  292. (createRouterFunction) {
  293. // `opts` for createBrowserHistory and createMemoryHistory are different, but also not relevant for us at the moment.
  294. // `basename` is the only option that is relevant for us, and it is the same for all.
  295. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  296. return function (routes, opts) {
  297. const router = createRouterFunction(routes, opts);
  298. const basename = opts && opts.basename;
  299. const activeRootSpan = getActiveRootSpan();
  300. // The initial load ends when `createBrowserRouter` is called.
  301. // This is the earliest convenient time to update the transaction name.
  302. // Callbacks to `router.subscribe` are not called for the initial load.
  303. if (router.state.historyAction === 'POP' && activeRootSpan) {
  304. updatePageloadTransaction(activeRootSpan, router.state.location, routes, undefined, basename);
  305. }
  306. router.subscribe((state) => {
  307. const location = state.location;
  308. if (_startTransactionOnLocationChange && (state.historyAction === 'PUSH' || state.historyAction === 'POP')) {
  309. handleNavigation(location, routes, state.historyAction, undefined, basename);
  310. }
  311. });
  312. return router;
  313. };
  314. }
  315. function getActiveRootSpan() {
  316. // Legacy behavior for "old" react router instrumentation
  317. if (activeTransaction) {
  318. return activeTransaction;
  319. }
  320. const span = getActiveSpan();
  321. const rootSpan = span ? getRootSpan(span) : undefined;
  322. if (!rootSpan) {
  323. return undefined;
  324. }
  325. const op = spanToJSON(rootSpan).op;
  326. // Only use this root span if it is a pageload or navigation span
  327. return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
  328. }
  329. export { reactRouterV6BrowserTracingIntegration, reactRouterV6Instrumentation, withSentryReactRouterV6Routing, wrapCreateBrowserRouter, wrapUseRoutes };
  330. //# sourceMappingURL=reactrouterv6.js.map