this repo has no description
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