browserTracingIntegration.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import { TRACING_DEFAULTS, addTracingExtensions, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveSpan, getCurrentHub, startIdleTransaction, getActiveTransaction } from '@sentry/core';
  2. import { logger, browserPerformanceTimeOrigin, addHistoryInstrumentationHandler, propagationContextFromHeaders, getDomElement } from '@sentry/utils';
  3. import { DEBUG_BUILD } from '../common/debug-build.js';
  4. import { registerBackgroundTabDetection } from './backgroundtab.js';
  5. import { startTrackingWebVitals, startTrackingLongTasks, startTrackingInteractions, addPerformanceEntries } from './metrics/index.js';
  6. import { defaultRequestInstrumentationOptions, instrumentOutgoingRequests } from './request.js';
  7. import { WINDOW } from './types.js';
  8. const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
  9. /** Options for Browser Tracing integration */
  10. const DEFAULT_BROWSER_TRACING_OPTIONS = {
  11. ...TRACING_DEFAULTS,
  12. instrumentNavigation: true,
  13. instrumentPageLoad: true,
  14. markBackgroundSpan: true,
  15. enableLongTask: true,
  16. _experiments: {},
  17. ...defaultRequestInstrumentationOptions,
  18. };
  19. /**
  20. * The Browser Tracing integration automatically instruments browser pageload/navigation
  21. * actions as transactions, and captures requests, metrics and errors as spans.
  22. *
  23. * The integration can be configured with a variety of options, and can be extended to use
  24. * any routing library. This integration uses {@see IdleTransaction} to create transactions.
  25. *
  26. * We explicitly export the proper type here, as this has to be extended in some cases.
  27. */
  28. const browserTracingIntegration = ((_options = {}) => {
  29. const _hasSetTracePropagationTargets = DEBUG_BUILD
  30. ? !!(
  31. // eslint-disable-next-line deprecation/deprecation
  32. (_options.tracePropagationTargets || _options.tracingOrigins)
  33. )
  34. : false;
  35. addTracingExtensions();
  36. // TODO (v8): remove this block after tracingOrigins is removed
  37. // Set tracePropagationTargets to tracingOrigins if specified by the user
  38. // In case both are specified, tracePropagationTargets takes precedence
  39. // eslint-disable-next-line deprecation/deprecation
  40. if (!_options.tracePropagationTargets && _options.tracingOrigins) {
  41. // eslint-disable-next-line deprecation/deprecation
  42. _options.tracePropagationTargets = _options.tracingOrigins;
  43. }
  44. const options = {
  45. ...DEFAULT_BROWSER_TRACING_OPTIONS,
  46. ..._options,
  47. };
  48. const _collectWebVitals = startTrackingWebVitals();
  49. if (options.enableLongTask) {
  50. startTrackingLongTasks();
  51. }
  52. if (options._experiments.enableInteractions) {
  53. startTrackingInteractions();
  54. }
  55. let latestRouteName;
  56. let latestRouteSource;
  57. /** Create routing idle transaction. */
  58. function _createRouteTransaction(context) {
  59. // eslint-disable-next-line deprecation/deprecation
  60. const hub = getCurrentHub();
  61. const { beforeStartSpan, idleTimeout, finalTimeout, heartbeatInterval } = options;
  62. const isPageloadTransaction = context.op === 'pageload';
  63. let expandedContext;
  64. if (isPageloadTransaction) {
  65. const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
  66. const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined;
  67. const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
  68. expandedContext = {
  69. traceId,
  70. parentSpanId,
  71. parentSampled: sampled,
  72. ...context,
  73. metadata: {
  74. // eslint-disable-next-line deprecation/deprecation
  75. ...context.metadata,
  76. dynamicSamplingContext: dsc,
  77. },
  78. trimEnd: true,
  79. };
  80. } else {
  81. expandedContext = {
  82. trimEnd: true,
  83. ...context,
  84. };
  85. }
  86. const finalContext = beforeStartSpan ? beforeStartSpan(expandedContext) : expandedContext;
  87. // If `beforeStartSpan` set a custom name, record that fact
  88. // eslint-disable-next-line deprecation/deprecation
  89. finalContext.metadata =
  90. finalContext.name !== expandedContext.name
  91. ? // eslint-disable-next-line deprecation/deprecation
  92. { ...finalContext.metadata, source: 'custom' }
  93. : // eslint-disable-next-line deprecation/deprecation
  94. finalContext.metadata;
  95. latestRouteName = finalContext.name;
  96. latestRouteSource = getSource(finalContext);
  97. if (finalContext.sampled === false) {
  98. DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
  99. }
  100. DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
  101. const { location } = WINDOW;
  102. const idleTransaction = startIdleTransaction(
  103. hub,
  104. finalContext,
  105. idleTimeout,
  106. finalTimeout,
  107. true,
  108. { location }, // for use in the tracesSampler
  109. heartbeatInterval,
  110. isPageloadTransaction, // should wait for finish signal if it's a pageload transaction
  111. );
  112. if (isPageloadTransaction) {
  113. WINDOW.document.addEventListener('readystatechange', () => {
  114. if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
  115. idleTransaction.sendAutoFinishSignal();
  116. }
  117. });
  118. if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
  119. idleTransaction.sendAutoFinishSignal();
  120. }
  121. }
  122. idleTransaction.registerBeforeFinishCallback(transaction => {
  123. _collectWebVitals();
  124. addPerformanceEntries(transaction);
  125. });
  126. return idleTransaction ;
  127. }
  128. return {
  129. name: BROWSER_TRACING_INTEGRATION_ID,
  130. // eslint-disable-next-line @typescript-eslint/no-empty-function
  131. setupOnce: () => {},
  132. afterAllSetup(client) {
  133. const clientOptions = client.getOptions();
  134. const { markBackgroundSpan, traceFetch, traceXHR, shouldCreateSpanForRequest, enableHTTPTimings, _experiments } =
  135. options;
  136. const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets;
  137. // There are three ways to configure tracePropagationTargets:
  138. // 1. via top level client option `tracePropagationTargets`
  139. // 2. via BrowserTracing option `tracePropagationTargets`
  140. // 3. via BrowserTracing option `tracingOrigins` (deprecated)
  141. //
  142. // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to
  143. // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated).
  144. // This is done as it minimizes bundle size (we don't have to have undefined checks).
  145. //
  146. // If both 1 and either one of 2 or 3 are set (from above), we log out a warning.
  147. // eslint-disable-next-line deprecation/deprecation
  148. const tracePropagationTargets = clientOptionsTracePropagationTargets || options.tracePropagationTargets;
  149. if (DEBUG_BUILD && _hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) {
  150. logger.warn(
  151. '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.',
  152. );
  153. }
  154. let activeSpan;
  155. let startingUrl = WINDOW.location.href;
  156. if (client.on) {
  157. client.on('startNavigationSpan', (context) => {
  158. if (activeSpan) {
  159. DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
  160. // If there's an open transaction on the scope, we need to finish it before creating an new one.
  161. activeSpan.end();
  162. }
  163. activeSpan = _createRouteTransaction({
  164. op: 'navigation',
  165. ...context,
  166. });
  167. });
  168. client.on('startPageLoadSpan', (context) => {
  169. if (activeSpan) {
  170. DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${spanToJSON(activeSpan).op}`);
  171. // If there's an open transaction on the scope, we need to finish it before creating an new one.
  172. activeSpan.end();
  173. }
  174. activeSpan = _createRouteTransaction({
  175. op: 'pageload',
  176. ...context,
  177. });
  178. });
  179. }
  180. if (options.instrumentPageLoad && client.emit) {
  181. const context = {
  182. name: WINDOW.location.pathname,
  183. // pageload should always start at timeOrigin (and needs to be in s, not ms)
  184. startTimestamp: browserPerformanceTimeOrigin ? browserPerformanceTimeOrigin / 1000 : undefined,
  185. origin: 'auto.pageload.browser',
  186. attributes: {
  187. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
  188. },
  189. };
  190. startBrowserTracingPageLoadSpan(client, context);
  191. }
  192. if (options.instrumentNavigation && client.emit) {
  193. addHistoryInstrumentationHandler(({ to, from }) => {
  194. /**
  195. * This early return is there to account for some cases where a navigation transaction starts right after
  196. * long-running pageload. We make sure that if `from` is undefined and a valid `startingURL` exists, we don't
  197. * create an uneccessary navigation transaction.
  198. *
  199. * This was hard to duplicate, but this behavior stopped as soon as this fix was applied. This issue might also
  200. * only be caused in certain development environments where the usage of a hot module reloader is causing
  201. * errors.
  202. */
  203. if (from === undefined && startingUrl && startingUrl.indexOf(to) !== -1) {
  204. startingUrl = undefined;
  205. return;
  206. }
  207. if (from !== to) {
  208. startingUrl = undefined;
  209. const context = {
  210. name: WINDOW.location.pathname,
  211. origin: 'auto.navigation.browser',
  212. attributes: {
  213. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
  214. },
  215. };
  216. startBrowserTracingNavigationSpan(client, context);
  217. }
  218. });
  219. }
  220. if (markBackgroundSpan) {
  221. registerBackgroundTabDetection();
  222. }
  223. if (_experiments.enableInteractions) {
  224. registerInteractionListener(options, latestRouteName, latestRouteSource);
  225. }
  226. instrumentOutgoingRequests({
  227. traceFetch,
  228. traceXHR,
  229. tracePropagationTargets,
  230. shouldCreateSpanForRequest,
  231. enableHTTPTimings,
  232. });
  233. },
  234. // TODO v8: Remove this again
  235. // This is private API that we use to fix converted BrowserTracing integrations in Next.js & SvelteKit
  236. options,
  237. };
  238. }) ;
  239. /**
  240. * Manually start a page load span.
  241. * This will only do something if the BrowserTracing integration has been setup.
  242. */
  243. function startBrowserTracingPageLoadSpan(client, spanOptions) {
  244. if (!client.emit) {
  245. return;
  246. }
  247. client.emit('startPageLoadSpan', spanOptions);
  248. const span = getActiveSpan();
  249. const op = span && spanToJSON(span).op;
  250. return op === 'pageload' ? span : undefined;
  251. }
  252. /**
  253. * Manually start a navigation span.
  254. * This will only do something if the BrowserTracing integration has been setup.
  255. */
  256. function startBrowserTracingNavigationSpan(client, spanOptions) {
  257. if (!client.emit) {
  258. return;
  259. }
  260. client.emit('startNavigationSpan', spanOptions);
  261. const span = getActiveSpan();
  262. const op = span && spanToJSON(span).op;
  263. return op === 'navigation' ? span : undefined;
  264. }
  265. /** Returns the value of a meta tag */
  266. function getMetaContent(metaName) {
  267. // Can't specify generic to `getDomElement` because tracing can be used
  268. // in a variety of environments, have to disable `no-unsafe-member-access`
  269. // as a result.
  270. const metaTag = getDomElement(`meta[name=${metaName}]`);
  271. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  272. return metaTag ? metaTag.getAttribute('content') : undefined;
  273. }
  274. /** Start listener for interaction transactions */
  275. function registerInteractionListener(
  276. options,
  277. latestRouteName,
  278. latestRouteSource,
  279. ) {
  280. let inflightInteractionTransaction;
  281. const registerInteractionTransaction = () => {
  282. const { idleTimeout, finalTimeout, heartbeatInterval } = options;
  283. const op = 'ui.action.click';
  284. // eslint-disable-next-line deprecation/deprecation
  285. const currentTransaction = getActiveTransaction();
  286. if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) {
  287. DEBUG_BUILD &&
  288. logger.warn(
  289. `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`,
  290. );
  291. return undefined;
  292. }
  293. if (inflightInteractionTransaction) {
  294. inflightInteractionTransaction.setFinishReason('interactionInterrupted');
  295. inflightInteractionTransaction.end();
  296. inflightInteractionTransaction = undefined;
  297. }
  298. if (!latestRouteName) {
  299. DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
  300. return undefined;
  301. }
  302. const { location } = WINDOW;
  303. const context = {
  304. name: latestRouteName,
  305. op,
  306. trimEnd: true,
  307. data: {
  308. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: latestRouteSource || 'url',
  309. },
  310. };
  311. inflightInteractionTransaction = startIdleTransaction(
  312. // eslint-disable-next-line deprecation/deprecation
  313. getCurrentHub(),
  314. context,
  315. idleTimeout,
  316. finalTimeout,
  317. true,
  318. { location }, // for use in the tracesSampler
  319. heartbeatInterval,
  320. );
  321. };
  322. ['click'].forEach(type => {
  323. addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
  324. });
  325. }
  326. function getSource(context) {
  327. const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
  328. // eslint-disable-next-line deprecation/deprecation
  329. const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
  330. // eslint-disable-next-line deprecation/deprecation
  331. const sourceFromMetadata = context.metadata && context.metadata.source;
  332. return sourceFromAttributes || sourceFromData || sourceFromMetadata;
  333. }
  334. export { BROWSER_TRACING_INTEGRATION_ID, browserTracingIntegration, getMetaContent, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan };
  335. //# sourceMappingURL=browserTracingIntegration.js.map