browserTracingIntegration.js 14 KB

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