browsertracing.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import { TRACING_DEFAULTS, addTracingExtensions, startIdleTransaction, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveTransaction } from '@sentry/core';
  2. import { logger, 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 { instrumentRoutingWithDefaults } from './router.js';
  8. import { WINDOW } from './types.js';
  9. const BROWSER_TRACING_INTEGRATION_ID = 'BrowserTracing';
  10. /** Options for Browser Tracing integration */
  11. const DEFAULT_BROWSER_TRACING_OPTIONS = {
  12. ...TRACING_DEFAULTS,
  13. markBackgroundTransactions: true,
  14. routingInstrumentation: instrumentRoutingWithDefaults,
  15. startTransactionOnLocationChange: true,
  16. startTransactionOnPageLoad: true,
  17. enableLongTask: true,
  18. _experiments: {},
  19. ...defaultRequestInstrumentationOptions,
  20. };
  21. /**
  22. * The Browser Tracing integration automatically instruments browser pageload/navigation
  23. * actions as transactions, and captures requests, metrics and errors as spans.
  24. *
  25. * The integration can be configured with a variety of options, and can be extended to use
  26. * any routing library. This integration uses {@see IdleTransaction} to create transactions.
  27. *
  28. * @deprecated Use `browserTracingIntegration()` instead.
  29. */
  30. class BrowserTracing {
  31. // This class currently doesn't have a static `id` field like the other integration classes, because it prevented
  32. // @sentry/tracing from being treeshaken. Tree shakers do not like static fields, because they behave like side effects.
  33. // TODO: Come up with a better plan, than using static fields on integration classes, and use that plan on all
  34. // integrations.
  35. /** Browser Tracing integration options */
  36. /**
  37. * @inheritDoc
  38. */
  39. constructor(_options) {
  40. this.name = BROWSER_TRACING_INTEGRATION_ID;
  41. this._hasSetTracePropagationTargets = false;
  42. addTracingExtensions();
  43. if (DEBUG_BUILD) {
  44. this._hasSetTracePropagationTargets = !!(
  45. _options &&
  46. // eslint-disable-next-line deprecation/deprecation
  47. (_options.tracePropagationTargets || _options.tracingOrigins)
  48. );
  49. }
  50. this.options = {
  51. ...DEFAULT_BROWSER_TRACING_OPTIONS,
  52. ..._options,
  53. };
  54. // Special case: enableLongTask can be set in _experiments
  55. // TODO (v8): Remove this in v8
  56. if (this.options._experiments.enableLongTask !== undefined) {
  57. this.options.enableLongTask = this.options._experiments.enableLongTask;
  58. }
  59. // TODO (v8): remove this block after tracingOrigins is removed
  60. // Set tracePropagationTargets to tracingOrigins if specified by the user
  61. // In case both are specified, tracePropagationTargets takes precedence
  62. // eslint-disable-next-line deprecation/deprecation
  63. if (_options && !_options.tracePropagationTargets && _options.tracingOrigins) {
  64. // eslint-disable-next-line deprecation/deprecation
  65. this.options.tracePropagationTargets = _options.tracingOrigins;
  66. }
  67. this._collectWebVitals = startTrackingWebVitals();
  68. if (this.options.enableLongTask) {
  69. startTrackingLongTasks();
  70. }
  71. if (this.options._experiments.enableInteractions) {
  72. startTrackingInteractions();
  73. }
  74. }
  75. /**
  76. * @inheritDoc
  77. */
  78. setupOnce(_, getCurrentHub) {
  79. this._getCurrentHub = getCurrentHub;
  80. const hub = getCurrentHub();
  81. // eslint-disable-next-line deprecation/deprecation
  82. const client = hub.getClient();
  83. const clientOptions = client && client.getOptions();
  84. const {
  85. routingInstrumentation: instrumentRouting,
  86. startTransactionOnLocationChange,
  87. startTransactionOnPageLoad,
  88. markBackgroundTransactions,
  89. traceFetch,
  90. traceXHR,
  91. shouldCreateSpanForRequest,
  92. enableHTTPTimings,
  93. _experiments,
  94. } = this.options;
  95. const clientOptionsTracePropagationTargets = clientOptions && clientOptions.tracePropagationTargets;
  96. // There are three ways to configure tracePropagationTargets:
  97. // 1. via top level client option `tracePropagationTargets`
  98. // 2. via BrowserTracing option `tracePropagationTargets`
  99. // 3. via BrowserTracing option `tracingOrigins` (deprecated)
  100. //
  101. // To avoid confusion, favour top level client option `tracePropagationTargets`, and fallback to
  102. // BrowserTracing option `tracePropagationTargets` and then `tracingOrigins` (deprecated).
  103. // This is done as it minimizes bundle size (we don't have to have undefined checks).
  104. //
  105. // If both 1 and either one of 2 or 3 are set (from above), we log out a warning.
  106. // eslint-disable-next-line deprecation/deprecation
  107. const tracePropagationTargets = clientOptionsTracePropagationTargets || this.options.tracePropagationTargets;
  108. if (DEBUG_BUILD && this._hasSetTracePropagationTargets && clientOptionsTracePropagationTargets) {
  109. logger.warn(
  110. '[Tracing] The `tracePropagationTargets` option was set in the BrowserTracing integration and top level `Sentry.init`. The top level `Sentry.init` value is being used.',
  111. );
  112. }
  113. instrumentRouting(
  114. (context) => {
  115. const transaction = this._createRouteTransaction(context);
  116. this.options._experiments.onStartRouteTransaction &&
  117. this.options._experiments.onStartRouteTransaction(transaction, context, getCurrentHub);
  118. return transaction;
  119. },
  120. startTransactionOnPageLoad,
  121. startTransactionOnLocationChange,
  122. );
  123. if (markBackgroundTransactions) {
  124. registerBackgroundTabDetection();
  125. }
  126. if (_experiments.enableInteractions) {
  127. this._registerInteractionListener();
  128. }
  129. instrumentOutgoingRequests({
  130. traceFetch,
  131. traceXHR,
  132. tracePropagationTargets,
  133. shouldCreateSpanForRequest,
  134. enableHTTPTimings,
  135. });
  136. }
  137. /** Create routing idle transaction. */
  138. _createRouteTransaction(context) {
  139. if (!this._getCurrentHub) {
  140. DEBUG_BUILD &&
  141. logger.warn(`[Tracing] Did not create ${context.op} transaction because _getCurrentHub is invalid.`);
  142. return undefined;
  143. }
  144. const hub = this._getCurrentHub();
  145. const { beforeNavigate, idleTimeout, finalTimeout, heartbeatInterval } = this.options;
  146. const isPageloadTransaction = context.op === 'pageload';
  147. let expandedContext;
  148. if (isPageloadTransaction) {
  149. const sentryTrace = isPageloadTransaction ? getMetaContent('sentry-trace') : '';
  150. const baggage = isPageloadTransaction ? getMetaContent('baggage') : undefined;
  151. const { traceId, dsc, parentSpanId, sampled } = propagationContextFromHeaders(sentryTrace, baggage);
  152. expandedContext = {
  153. traceId,
  154. parentSpanId,
  155. parentSampled: sampled,
  156. ...context,
  157. metadata: {
  158. // eslint-disable-next-line deprecation/deprecation
  159. ...context.metadata,
  160. dynamicSamplingContext: dsc,
  161. },
  162. trimEnd: true,
  163. };
  164. } else {
  165. expandedContext = {
  166. trimEnd: true,
  167. ...context,
  168. };
  169. }
  170. const modifiedContext = typeof beforeNavigate === 'function' ? beforeNavigate(expandedContext) : expandedContext;
  171. // For backwards compatibility reasons, beforeNavigate can return undefined to "drop" the transaction (prevent it
  172. // from being sent to Sentry).
  173. const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext;
  174. // If `beforeNavigate` set a custom name, record that fact
  175. // eslint-disable-next-line deprecation/deprecation
  176. finalContext.metadata =
  177. finalContext.name !== expandedContext.name
  178. ? // eslint-disable-next-line deprecation/deprecation
  179. { ...finalContext.metadata, source: 'custom' }
  180. : // eslint-disable-next-line deprecation/deprecation
  181. finalContext.metadata;
  182. this._latestRouteName = finalContext.name;
  183. this._latestRouteSource = getSource(finalContext);
  184. // eslint-disable-next-line deprecation/deprecation
  185. if (finalContext.sampled === false) {
  186. DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`);
  187. }
  188. DEBUG_BUILD && logger.log(`[Tracing] Starting ${finalContext.op} transaction on scope`);
  189. const { location } = WINDOW;
  190. const idleTransaction = startIdleTransaction(
  191. hub,
  192. finalContext,
  193. idleTimeout,
  194. finalTimeout,
  195. true,
  196. { location }, // for use in the tracesSampler
  197. heartbeatInterval,
  198. isPageloadTransaction, // should wait for finish signal if it's a pageload transaction
  199. );
  200. if (isPageloadTransaction) {
  201. WINDOW.document.addEventListener('readystatechange', () => {
  202. if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
  203. idleTransaction.sendAutoFinishSignal();
  204. }
  205. });
  206. if (['interactive', 'complete'].includes(WINDOW.document.readyState)) {
  207. idleTransaction.sendAutoFinishSignal();
  208. }
  209. }
  210. idleTransaction.registerBeforeFinishCallback(transaction => {
  211. this._collectWebVitals();
  212. addPerformanceEntries(transaction);
  213. });
  214. return idleTransaction ;
  215. }
  216. /** Start listener for interaction transactions */
  217. _registerInteractionListener() {
  218. let inflightInteractionTransaction;
  219. const registerInteractionTransaction = () => {
  220. const { idleTimeout, finalTimeout, heartbeatInterval } = this.options;
  221. const op = 'ui.action.click';
  222. // eslint-disable-next-line deprecation/deprecation
  223. const currentTransaction = getActiveTransaction();
  224. if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) {
  225. DEBUG_BUILD &&
  226. logger.warn(
  227. `[Tracing] Did not create ${op} transaction because a pageload or navigation transaction is in progress.`,
  228. );
  229. return undefined;
  230. }
  231. if (inflightInteractionTransaction) {
  232. inflightInteractionTransaction.setFinishReason('interactionInterrupted');
  233. inflightInteractionTransaction.end();
  234. inflightInteractionTransaction = undefined;
  235. }
  236. if (!this._getCurrentHub) {
  237. DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _getCurrentHub is invalid.`);
  238. return undefined;
  239. }
  240. if (!this._latestRouteName) {
  241. DEBUG_BUILD && logger.warn(`[Tracing] Did not create ${op} transaction because _latestRouteName is missing.`);
  242. return undefined;
  243. }
  244. const hub = this._getCurrentHub();
  245. const { location } = WINDOW;
  246. const context = {
  247. name: this._latestRouteName,
  248. op,
  249. trimEnd: true,
  250. data: {
  251. [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRouteSource || 'url',
  252. },
  253. };
  254. inflightInteractionTransaction = startIdleTransaction(
  255. hub,
  256. context,
  257. idleTimeout,
  258. finalTimeout,
  259. true,
  260. { location }, // for use in the tracesSampler
  261. heartbeatInterval,
  262. );
  263. };
  264. ['click'].forEach(type => {
  265. addEventListener(type, registerInteractionTransaction, { once: false, capture: true });
  266. });
  267. }
  268. }
  269. /** Returns the value of a meta tag */
  270. function getMetaContent(metaName) {
  271. // Can't specify generic to `getDomElement` because tracing can be used
  272. // in a variety of environments, have to disable `no-unsafe-member-access`
  273. // as a result.
  274. const metaTag = getDomElement(`meta[name=${metaName}]`);
  275. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  276. return metaTag ? metaTag.getAttribute('content') : undefined;
  277. }
  278. function getSource(context) {
  279. const sourceFromAttributes = context.attributes && context.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
  280. // eslint-disable-next-line deprecation/deprecation
  281. const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
  282. // eslint-disable-next-line deprecation/deprecation
  283. const sourceFromMetadata = context.metadata && context.metadata.source;
  284. return sourceFromAttributes || sourceFromData || sourceFromMetadata;
  285. }
  286. export { BROWSER_TRACING_INTEGRATION_ID, BrowserTracing, getMetaContent };
  287. //# sourceMappingURL=browsertracing.js.map