browsertracing.js 12 KB

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