reactrouterv6.js 13 KB

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