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