reactrouter.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import { browserTracingIntegration, startBrowserTracingPageLoadSpan, startBrowserTracingNavigationSpan, WINDOW } from '@sentry/browser';
  2. import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getRootSpan, spanToJSON } from '@sentry/core';
  3. import hoistNonReactStatics from 'hoist-non-react-statics';
  4. import * as React from 'react';
  5. const _jsxFileName = "/home/runner/work/sentry-javascript/sentry-javascript/packages/react/src/reactrouter.tsx";
  6. // We need to disable eslint no-explict-any because any is required for the
  7. // react-router typings.
  8. let activeTransaction;
  9. /**
  10. * A browser tracing integration that uses React Router v4 to instrument navigations.
  11. * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
  12. */
  13. function reactRouterV4BrowserTracingIntegration(
  14. options,
  15. ) {
  16. const integration = browserTracingIntegration({
  17. ...options,
  18. instrumentPageLoad: false,
  19. instrumentNavigation: false,
  20. });
  21. const { history, routes, matchPath, instrumentPageLoad = true, instrumentNavigation = true } = options;
  22. return {
  23. ...integration,
  24. afterAllSetup(client) {
  25. integration.afterAllSetup(client);
  26. const startPageloadCallback = (startSpanOptions) => {
  27. startBrowserTracingPageLoadSpan(client, startSpanOptions);
  28. return undefined;
  29. };
  30. const startNavigationCallback = (startSpanOptions) => {
  31. startBrowserTracingNavigationSpan(client, startSpanOptions);
  32. return undefined;
  33. };
  34. // eslint-disable-next-line deprecation/deprecation
  35. const instrumentation = reactRouterV4Instrumentation(history, routes, matchPath);
  36. // Now instrument page load & navigation with correct settings
  37. instrumentation(startPageloadCallback, instrumentPageLoad, false);
  38. instrumentation(startNavigationCallback, false, instrumentNavigation);
  39. },
  40. };
  41. }
  42. /**
  43. * A browser tracing integration that uses React Router v5 to instrument navigations.
  44. * Expects `history` (and optionally `routes` and `matchPath`) to be passed as options.
  45. */
  46. function reactRouterV5BrowserTracingIntegration(
  47. options,
  48. ) {
  49. const integration = browserTracingIntegration({
  50. ...options,
  51. instrumentPageLoad: false,
  52. instrumentNavigation: false,
  53. });
  54. const { history, routes, matchPath } = options;
  55. return {
  56. ...integration,
  57. afterAllSetup(client) {
  58. integration.afterAllSetup(client);
  59. const startPageloadCallback = (startSpanOptions) => {
  60. startBrowserTracingPageLoadSpan(client, startSpanOptions);
  61. return undefined;
  62. };
  63. const startNavigationCallback = (startSpanOptions) => {
  64. startBrowserTracingNavigationSpan(client, startSpanOptions);
  65. return undefined;
  66. };
  67. // eslint-disable-next-line deprecation/deprecation
  68. const instrumentation = reactRouterV5Instrumentation(history, routes, matchPath);
  69. // Now instrument page load & navigation with correct settings
  70. instrumentation(startPageloadCallback, options.instrumentPageLoad, false);
  71. instrumentation(startNavigationCallback, false, options.instrumentNavigation);
  72. },
  73. };
  74. }
  75. /**
  76. * @deprecated Use `browserTracingReactRouterV4()` instead.
  77. */
  78. function reactRouterV4Instrumentation(
  79. history,
  80. routes,
  81. matchPath,
  82. ) {
  83. return createReactRouterInstrumentation(history, 'reactrouter_v4', routes, matchPath);
  84. }
  85. /**
  86. * @deprecated Use `browserTracingReactRouterV5()` instead.
  87. */
  88. function reactRouterV5Instrumentation(
  89. history,
  90. routes,
  91. matchPath,
  92. ) {
  93. return createReactRouterInstrumentation(history, 'reactrouter_v5', routes, matchPath);
  94. }
  95. function createReactRouterInstrumentation(
  96. history,
  97. instrumentationName,
  98. allRoutes = [],
  99. matchPath,
  100. ) {
  101. function getInitPathName() {
  102. if (history && history.location) {
  103. return history.location.pathname;
  104. }
  105. if (WINDOW && WINDOW.location) {
  106. return WINDOW.location.pathname;
  107. }
  108. return undefined;
  109. }
  110. /**
  111. * Normalizes a transaction name. Returns the new name as well as the
  112. * source of the transaction.
  113. *
  114. * @param pathname The initial pathname we normalize
  115. */
  116. function normalizeTransactionName(pathname) {
  117. if (allRoutes.length === 0 || !matchPath) {
  118. return [pathname, 'url'];
  119. }
  120. const branches = matchRoutes(allRoutes, pathname, matchPath);
  121. // eslint-disable-next-line @typescript-eslint/prefer-for-of
  122. for (let x = 0; x < branches.length; x++) {
  123. if (branches[x].match.isExact) {
  124. return [branches[x].match.path, 'route'];
  125. }
  126. }
  127. return [pathname, 'url'];
  128. }
  129. return (customStartTransaction, startTransactionOnPageLoad = true, startTransactionOnLocationChange = true) => {
  130. const initPathName = getInitPathName();
  131. if (startTransactionOnPageLoad && initPathName) {
  132. const [name, source] = normalizeTransactionName(initPathName);
  133. activeTransaction = customStartTransaction({
  134. name,
  135. attributes: {
  136. [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
  137. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.pageload.react.${instrumentationName}`,
  138. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
  139. },
  140. });
  141. }
  142. if (startTransactionOnLocationChange && history.listen) {
  143. history.listen((location, action) => {
  144. if (action && (action === 'PUSH' || action === 'POP')) {
  145. if (activeTransaction) {
  146. activeTransaction.end();
  147. }
  148. const [name, source] = normalizeTransactionName(location.pathname);
  149. activeTransaction = customStartTransaction({
  150. name,
  151. attributes: {
  152. [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
  153. [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: `auto.navigation.react.${instrumentationName}`,
  154. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
  155. },
  156. });
  157. }
  158. });
  159. }
  160. };
  161. }
  162. /**
  163. * Matches a set of routes to a pathname
  164. * Based on implementation from
  165. */
  166. function matchRoutes(
  167. routes,
  168. pathname,
  169. matchPath,
  170. branch = [],
  171. ) {
  172. routes.some(route => {
  173. const match = route.path
  174. ? matchPath(pathname, route)
  175. : branch.length
  176. ? branch[branch.length - 1].match // use parent match
  177. : computeRootMatch(pathname); // use default "root" match
  178. if (match) {
  179. branch.push({ route, match });
  180. if (route.routes) {
  181. matchRoutes(route.routes, pathname, matchPath, branch);
  182. }
  183. }
  184. return !!match;
  185. });
  186. return branch;
  187. }
  188. function computeRootMatch(pathname) {
  189. return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
  190. }
  191. /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
  192. function withSentryRouting(Route) {
  193. const componentDisplayName = (Route ).displayName || (Route ).name;
  194. const activeRootSpan = getActiveRootSpan();
  195. const WrappedRoute = (props) => {
  196. if (activeRootSpan && props && props.computedMatch && props.computedMatch.isExact) {
  197. activeRootSpan.updateName(props.computedMatch.path);
  198. activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
  199. }
  200. // @ts-expect-error Setting more specific React Component typing for `R` generic above
  201. // will break advanced type inference done by react router params:
  202. // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/13dc4235c069e25fe7ee16e11f529d909f9f3ff8/types/react-router/index.d.ts#L154-L164
  203. return React.createElement(Route, { ...props, __self: this, __source: {fileName: _jsxFileName, lineNumber: 277}} );
  204. };
  205. WrappedRoute.displayName = `sentryRoute(${componentDisplayName})`;
  206. hoistNonReactStatics(WrappedRoute, Route);
  207. // @ts-expect-error Setting more specific React Component typing for `R` generic above
  208. // will break advanced type inference done by react router params:
  209. // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/13dc4235c069e25fe7ee16e11f529d909f9f3ff8/types/react-router/index.d.ts#L154-L164
  210. return WrappedRoute;
  211. }
  212. /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access */
  213. function getActiveRootSpan() {
  214. // Legacy behavior for "old" react router instrumentation
  215. if (activeTransaction) {
  216. return activeTransaction;
  217. }
  218. const span = getActiveSpan();
  219. const rootSpan = span ? getRootSpan(span) : undefined;
  220. if (!rootSpan) {
  221. return undefined;
  222. }
  223. const op = spanToJSON(rootSpan).op;
  224. // Only use this root span if it is a pageload or navigation span
  225. return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
  226. }
  227. export { reactRouterV4BrowserTracingIntegration, reactRouterV4Instrumentation, reactRouterV5BrowserTracingIntegration, reactRouterV5Instrumentation, withSentryRouting };
  228. //# sourceMappingURL=reactrouter.js.map