this repo has no description
1import { getActiveTransaction } from '@sentry/core';
2import { browserPerformanceTimeOrigin, logger, htmlTreeAsString } from '@sentry/utils';
3import { WINDOW } from '../types.js';
4import { onCLS } from '../web-vitals/getCLS.js';
5import { onFID } from '../web-vitals/getFID.js';
6import { onLCP } from '../web-vitals/getLCP.js';
7import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher.js';
8import { observe } from '../web-vitals/lib/observe.js';
9import { _startChild, isMeasurementValue } from './utils.js';
10
11/**
12 * Converts from milliseconds to seconds
13 * @param time time in ms
14 */
15function msToSec(time) {
16 return time / 1000;
17}
18
19function getBrowserPerformanceAPI() {
20 // @ts-ignore we want to make sure all of these are available, even if TS is sure they are
21 return WINDOW && WINDOW.addEventListener && WINDOW.performance;
22}
23
24let _performanceCursor = 0;
25
26let _measurements = {};
27let _lcpEntry;
28let _clsEntry;
29
30/**
31 * Start tracking web vitals
32 *
33 * @returns A function that forces web vitals collection
34 */
35function startTrackingWebVitals() {
36 const performance = getBrowserPerformanceAPI();
37 if (performance && browserPerformanceTimeOrigin) {
38 // @ts-ignore we want to make sure all of these are available, even if TS is sure they are
39 if (performance.mark) {
40 WINDOW.performance.mark('sentry-tracing-init');
41 }
42 _trackFID();
43 const clsCallback = _trackCLS();
44 const lcpCallback = _trackLCP();
45
46 return () => {
47 if (clsCallback) {
48 clsCallback();
49 }
50 if (lcpCallback) {
51 lcpCallback();
52 }
53 };
54 }
55
56 return () => undefined;
57}
58
59/**
60 * Start tracking long tasks.
61 */
62function startTrackingLongTasks() {
63 const entryHandler = (entries) => {
64 for (const entry of entries) {
65 const transaction = getActiveTransaction() ;
66 if (!transaction) {
67 return;
68 }
69 const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime);
70 const duration = msToSec(entry.duration);
71
72 transaction.startChild({
73 description: 'Main UI thread blocked',
74 op: 'ui.long-task',
75 startTimestamp: startTime,
76 endTimestamp: startTime + duration,
77 });
78 }
79 };
80
81 observe('longtask', entryHandler);
82}
83
84/**
85 * Start tracking interaction events.
86 */
87function startTrackingInteractions() {
88 const entryHandler = (entries) => {
89 for (const entry of entries) {
90 const transaction = getActiveTransaction() ;
91 if (!transaction) {
92 return;
93 }
94
95 if (entry.name === 'click') {
96 const startTime = msToSec((browserPerformanceTimeOrigin ) + entry.startTime);
97 const duration = msToSec(entry.duration);
98
99 transaction.startChild({
100 description: htmlTreeAsString(entry.target),
101 op: `ui.interaction.${entry.name}`,
102 startTimestamp: startTime,
103 endTimestamp: startTime + duration,
104 });
105 }
106 }
107 };
108
109 observe('event', entryHandler, { durationThreshold: 0 });
110}
111
112/** Starts tracking the Cumulative Layout Shift on the current page. */
113function _trackCLS() {
114 // See:
115 // https://web.dev/evolving-cls/
116 // https://web.dev/cls-web-tooling/
117 return onCLS(metric => {
118 const entry = metric.entries.pop();
119 if (!entry) {
120 return;
121 }
122
123 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding CLS');
124 _measurements['cls'] = { value: metric.value, unit: '' };
125 _clsEntry = entry ;
126 });
127}
128
129/** Starts tracking the Largest Contentful Paint on the current page. */
130function _trackLCP() {
131 return onLCP(metric => {
132 const entry = metric.entries.pop();
133 if (!entry) {
134 return;
135 }
136
137 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding LCP');
138 _measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
139 _lcpEntry = entry ;
140 });
141}
142
143/** Starts tracking the First Input Delay on the current page. */
144function _trackFID() {
145 onFID(metric => {
146 const entry = metric.entries.pop();
147 if (!entry) {
148 return;
149 }
150
151 const timeOrigin = msToSec(browserPerformanceTimeOrigin );
152 const startTime = msToSec(entry.startTime);
153 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding FID');
154 _measurements['fid'] = { value: metric.value, unit: 'millisecond' };
155 _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' };
156 });
157}
158
159/** Add performance related spans to a transaction */
160function addPerformanceEntries(transaction) {
161 const performance = getBrowserPerformanceAPI();
162 if (!performance || !WINDOW.performance.getEntries || !browserPerformanceTimeOrigin) {
163 // Gatekeeper if performance API not available
164 return;
165 }
166
167 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Tracing] Adding & adjusting spans using Performance API');
168 const timeOrigin = msToSec(browserPerformanceTimeOrigin);
169
170 const performanceEntries = performance.getEntries();
171
172 let responseStartTimestamp;
173 let requestStartTimestamp;
174
175 // eslint-disable-next-line @typescript-eslint/no-explicit-any
176 performanceEntries.slice(_performanceCursor).forEach((entry) => {
177 const startTime = msToSec(entry.startTime);
178 const duration = msToSec(entry.duration);
179
180 if (transaction.op === 'navigation' && timeOrigin + startTime < transaction.startTimestamp) {
181 return;
182 }
183
184 switch (entry.entryType) {
185 case 'navigation': {
186 _addNavigationSpans(transaction, entry, timeOrigin);
187 responseStartTimestamp = timeOrigin + msToSec(entry.responseStart);
188 requestStartTimestamp = timeOrigin + msToSec(entry.requestStart);
189 break;
190 }
191 case 'mark':
192 case 'paint':
193 case 'measure': {
194 _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin);
195
196 // capture web vitals
197 const firstHidden = getVisibilityWatcher();
198 // Only report if the page wasn't hidden prior to the web vital.
199 const shouldRecord = entry.startTime < firstHidden.firstHiddenTime;
200
201 if (entry.name === 'first-paint' && shouldRecord) {
202 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding FP');
203 _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' };
204 }
205 if (entry.name === 'first-contentful-paint' && shouldRecord) {
206 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding FCP');
207 _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' };
208 }
209 break;
210 }
211 case 'resource': {
212 const resourceName = (entry.name ).replace(WINDOW.location.origin, '');
213 _addResourceSpans(transaction, entry, resourceName, startTime, duration, timeOrigin);
214 break;
215 }
216 // Ignore other entry types.
217 }
218 });
219
220 _performanceCursor = Math.max(performanceEntries.length - 1, 0);
221
222 _trackNavigator(transaction);
223
224 // Measurements are only available for pageload transactions
225 if (transaction.op === 'pageload') {
226 // Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the
227 // start of the response in milliseconds
228 if (typeof responseStartTimestamp === 'number') {
229 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding TTFB');
230 _measurements['ttfb'] = {
231 value: (responseStartTimestamp - transaction.startTimestamp) * 1000,
232 unit: 'millisecond',
233 };
234
235 if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) {
236 // Capture the time spent making the request and receiving the first byte of the response.
237 // This is the time between the start of the request and the start of the response in milliseconds.
238 _measurements['ttfb.requestTime'] = {
239 value: (responseStartTimestamp - requestStartTimestamp) * 1000,
240 unit: 'millisecond',
241 };
242 }
243 }
244
245 ['fcp', 'fp', 'lcp'].forEach(name => {
246 if (!_measurements[name] || timeOrigin >= transaction.startTimestamp) {
247 return;
248 }
249 // The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin.
250 // Unfortunately, timeOrigin is not captured within the transaction span data, so these web vitals will need
251 // to be adjusted to be relative to transaction.startTimestamp.
252 const oldValue = _measurements[name].value;
253 const measurementTimestamp = timeOrigin + msToSec(oldValue);
254
255 // normalizedValue should be in milliseconds
256 const normalizedValue = Math.abs((measurementTimestamp - transaction.startTimestamp) * 1000);
257 const delta = normalizedValue - oldValue;
258
259 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) &&
260 logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`);
261 _measurements[name].value = normalizedValue;
262 });
263
264 const fidMark = _measurements['mark.fid'];
265 if (fidMark && _measurements['fid']) {
266 // create span for FID
267 _startChild(transaction, {
268 description: 'first input delay',
269 endTimestamp: fidMark.value + msToSec(_measurements['fid'].value),
270 op: 'ui.action',
271 startTimestamp: fidMark.value,
272 });
273
274 // Delete mark.fid as we don't want it to be part of final payload
275 delete _measurements['mark.fid'];
276 }
277
278 // If FCP is not recorded we should not record the cls value
279 // according to the new definition of CLS.
280 if (!('fcp' in _measurements)) {
281 delete _measurements.cls;
282 }
283
284 Object.keys(_measurements).forEach(measurementName => {
285 transaction.setMeasurement(
286 measurementName,
287 _measurements[measurementName].value,
288 _measurements[measurementName].unit,
289 );
290 });
291
292 _tagMetricInfo(transaction);
293 }
294
295 _lcpEntry = undefined;
296 _clsEntry = undefined;
297 _measurements = {};
298}
299
300/** Create measure related spans */
301function _addMeasureSpans(
302 transaction,
303 // eslint-disable-next-line @typescript-eslint/no-explicit-any
304 entry,
305 startTime,
306 duration,
307 timeOrigin,
308) {
309 const measureStartTimestamp = timeOrigin + startTime;
310 const measureEndTimestamp = measureStartTimestamp + duration;
311
312 _startChild(transaction, {
313 description: entry.name ,
314 endTimestamp: measureEndTimestamp,
315 op: entry.entryType ,
316 startTimestamp: measureStartTimestamp,
317 });
318
319 return measureStartTimestamp;
320}
321
322/** Instrument navigation entries */
323// eslint-disable-next-line @typescript-eslint/no-explicit-any
324function _addNavigationSpans(transaction, entry, timeOrigin) {
325 ['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'].forEach(event => {
326 _addPerformanceNavigationTiming(transaction, entry, event, timeOrigin);
327 });
328 _addPerformanceNavigationTiming(transaction, entry, 'secureConnection', timeOrigin, 'TLS/SSL', 'connectEnd');
329 _addPerformanceNavigationTiming(transaction, entry, 'fetch', timeOrigin, 'cache', 'domainLookupStart');
330 _addPerformanceNavigationTiming(transaction, entry, 'domainLookup', timeOrigin, 'DNS');
331 _addRequest(transaction, entry, timeOrigin);
332}
333
334/** Create performance navigation related spans */
335function _addPerformanceNavigationTiming(
336 transaction,
337 // eslint-disable-next-line @typescript-eslint/no-explicit-any
338 entry,
339 event,
340 timeOrigin,
341 description,
342 eventEnd,
343) {
344 const end = eventEnd ? (entry[eventEnd] ) : (entry[`${event}End`] );
345 const start = entry[`${event}Start`] ;
346 if (!start || !end) {
347 return;
348 }
349 _startChild(transaction, {
350 op: 'browser',
351 description: description || event,
352 startTimestamp: timeOrigin + msToSec(start),
353 endTimestamp: timeOrigin + msToSec(end),
354 });
355}
356
357/** Create request and response related spans */
358// eslint-disable-next-line @typescript-eslint/no-explicit-any
359function _addRequest(transaction, entry, timeOrigin) {
360 _startChild(transaction, {
361 op: 'browser',
362 description: 'request',
363 startTimestamp: timeOrigin + msToSec(entry.requestStart ),
364 endTimestamp: timeOrigin + msToSec(entry.responseEnd ),
365 });
366
367 _startChild(transaction, {
368 op: 'browser',
369 description: 'response',
370 startTimestamp: timeOrigin + msToSec(entry.responseStart ),
371 endTimestamp: timeOrigin + msToSec(entry.responseEnd ),
372 });
373}
374
375/** Create resource-related spans */
376function _addResourceSpans(
377 transaction,
378 entry,
379 resourceName,
380 startTime,
381 duration,
382 timeOrigin,
383) {
384 // we already instrument based on fetch and xhr, so we don't need to
385 // duplicate spans here.
386 if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
387 return;
388 }
389
390 // eslint-disable-next-line @typescript-eslint/no-explicit-any
391 const data = {};
392 if ('transferSize' in entry) {
393 data['http.response_transfer_size'] = entry.transferSize;
394 }
395 if ('encodedBodySize' in entry) {
396 data['http.response_content_length'] = entry.encodedBodySize;
397 }
398 if ('decodedBodySize' in entry) {
399 data['http.decoded_response_content_length'] = entry.decodedBodySize;
400 }
401 if ('renderBlockingStatus' in entry) {
402 data['resource.render_blocking_status'] = entry.renderBlockingStatus;
403 }
404
405 const startTimestamp = timeOrigin + startTime;
406 const endTimestamp = startTimestamp + duration;
407
408 _startChild(transaction, {
409 description: resourceName,
410 endTimestamp,
411 op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other',
412 startTimestamp,
413 data,
414 });
415}
416
417/**
418 * Capture the information of the user agent.
419 */
420function _trackNavigator(transaction) {
421 const navigator = WINDOW.navigator ;
422 if (!navigator) {
423 return;
424 }
425
426 // track network connectivity
427 const connection = navigator.connection;
428 if (connection) {
429 if (connection.effectiveType) {
430 transaction.setTag('effectiveConnectionType', connection.effectiveType);
431 }
432
433 if (connection.type) {
434 transaction.setTag('connectionType', connection.type);
435 }
436
437 if (isMeasurementValue(connection.rtt)) {
438 _measurements['connection.rtt'] = { value: connection.rtt, unit: 'millisecond' };
439 }
440 }
441
442 if (isMeasurementValue(navigator.deviceMemory)) {
443 transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`);
444 }
445
446 if (isMeasurementValue(navigator.hardwareConcurrency)) {
447 transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency));
448 }
449}
450
451/** Add LCP / CLS data to transaction to allow debugging */
452function _tagMetricInfo(transaction) {
453 if (_lcpEntry) {
454 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding LCP Data');
455
456 // Capture Properties of the LCP element that contributes to the LCP.
457
458 if (_lcpEntry.element) {
459 transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element));
460 }
461
462 if (_lcpEntry.id) {
463 transaction.setTag('lcp.id', _lcpEntry.id);
464 }
465
466 if (_lcpEntry.url) {
467 // Trim URL to the first 200 characters.
468 transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200));
469 }
470
471 transaction.setTag('lcp.size', _lcpEntry.size);
472 }
473
474 // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
475 if (_clsEntry && _clsEntry.sources) {
476 (typeof __SENTRY_DEBUG__ === 'undefined' || __SENTRY_DEBUG__) && logger.log('[Measurements] Adding CLS Data');
477 _clsEntry.sources.forEach((source, index) =>
478 transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)),
479 );
480 }
481}
482
483export { _addMeasureSpans, _addResourceSpans, addPerformanceEntries, startTrackingInteractions, startTrackingLongTasks, startTrackingWebVitals };
484//# sourceMappingURL=index.js.map