Skip to content

Commit db15649

Browse files
authored
feat(tracing): Update to Web Vitals v3 (#5987)
This merges in changes from Web Vitals v3.0.4 while maintaining the original changes and simplifications to the vendored code.
1 parent ef59700 commit db15649

File tree

18 files changed

+771
-173
lines changed

18 files changed

+771
-173
lines changed

packages/tracing/src/browser/metrics/index.ts

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { browserPerformanceTimeOrigin, htmlTreeAsString, logger, WINDOW } from '
55
import { IdleTransaction } from '../../idletransaction';
66
import { Transaction } from '../../transaction';
77
import { getActiveTransaction, msToSec } from '../../utils';
8-
import { getCLS, LayoutShift } from '../web-vitals/getCLS';
9-
import { getFID } from '../web-vitals/getFID';
10-
import { getLCP, LargestContentfulPaint } from '../web-vitals/getLCP';
8+
import { onCLS } from '../web-vitals/getCLS';
9+
import { onFID } from '../web-vitals/getFID';
10+
import { onLCP } from '../web-vitals/getLCP';
1111
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher';
12-
import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe';
12+
import { observe } from '../web-vitals/lib/observe';
1313
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types';
1414
import { _startChild, isMeasurementValue } from './utils';
1515

@@ -42,19 +42,22 @@ export function startTrackingWebVitals(reportAllChanges: boolean = false): void
4242
* Start tracking long tasks.
4343
*/
4444
export function startTrackingLongTasks(): void {
45-
const entryHandler: PerformanceEntryHandler = (entry: PerformanceEntry): void => {
46-
const transaction = getActiveTransaction() as IdleTransaction | undefined;
47-
if (!transaction) {
48-
return;
45+
const entryHandler = (entries: PerformanceEntry[]): void => {
46+
for (const entry of entries) {
47+
const transaction = getActiveTransaction() as IdleTransaction | undefined;
48+
if (!transaction) {
49+
return;
50+
}
51+
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
52+
const duration = msToSec(entry.duration);
53+
54+
transaction.startChild({
55+
description: 'Main UI thread blocked',
56+
op: 'ui.long-task',
57+
startTimestamp: startTime,
58+
endTimestamp: startTime + duration,
59+
});
4960
}
50-
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
51-
const duration = msToSec(entry.duration);
52-
transaction.startChild({
53-
description: 'Main UI thread blocked',
54-
op: 'ui.long-task',
55-
startTimestamp: startTime,
56-
endTimestamp: startTime + duration,
57-
});
5861
};
5962

6063
observe('longtask', entryHandler);
@@ -65,7 +68,7 @@ function _trackCLS(): void {
6568
// See:
6669
// https://web.dev/evolving-cls/
6770
// https://web.dev/cls-web-tooling/
68-
getCLS(metric => {
71+
onCLS(metric => {
6972
const entry = metric.entries.pop();
7073
if (!entry) {
7174
return;
@@ -79,21 +82,24 @@ function _trackCLS(): void {
7982

8083
/** Starts tracking the Largest Contentful Paint on the current page. */
8184
function _trackLCP(reportAllChanges: boolean): void {
82-
getLCP(metric => {
83-
const entry = metric.entries.pop();
84-
if (!entry) {
85-
return;
86-
}
85+
onLCP(
86+
metric => {
87+
const entry = metric.entries.pop();
88+
if (!entry) {
89+
return;
90+
}
8791

88-
__DEBUG_BUILD__ && logger.log('[Measurements] Adding LCP');
89-
_measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
90-
_lcpEntry = entry as LargestContentfulPaint;
91-
}, reportAllChanges);
92+
__DEBUG_BUILD__ && logger.log('[Measurements] Adding LCP');
93+
_measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
94+
_lcpEntry = entry as LargestContentfulPaint;
95+
},
96+
{ reportAllChanges },
97+
);
9298
}
9399

94100
/** Starts tracking the First Input Delay on the current page. */
95101
function _trackFID(): void {
96-
getFID(metric => {
102+
onFID(metric => {
97103
const entry = metric.entries.pop();
98104
if (!entry) {
99105
return;

packages/tracing/src/browser/web-vitals/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
> A modular library for measuring the [Web Vitals](https://web.dev/vitals/) metrics on real users.
44
5-
This was vendored from: https://github.com/GoogleChrome/web-vitals: v2.1.0
5+
This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.0.4
66

7-
The commit SHA used is: [3f3338d994f182172d5b97b22a0fcce0c2846908](https://github.com/GoogleChrome/web-vitals/tree/3f3338d994f182172d5b97b22a0fcce0c2846908)
7+
The commit SHA used is: [7f0ed0bfb03c356e348a558a3eda111b498a2a11](https://github.com/GoogleChrome/web-vitals/tree/7f0ed0bfb03c356e348a558a3eda111b498a2a11)
88

99
Current vendored web vitals are:
1010

@@ -23,6 +23,9 @@ As such, logic around `BFCache` and multiple reports were removed from the libra
2323

2424
## CHANGELOG
2525

26+
https://github.com/getsentry/sentry-javascript/pull/5987
27+
- Bumped from Web Vitals v2.1.0 to v3.0.4
28+
2629
https://github.com/getsentry/sentry-javascript/pull/3781
2730
- Bumped from Web Vitals v0.2.4 to v2.1.0
2831

packages/tracing/src/browser/web-vitals/getCLS.ts

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,72 +16,80 @@
1616

1717
import { bindReporter } from './lib/bindReporter';
1818
import { initMetric } from './lib/initMetric';
19-
import { observe, PerformanceEntryHandler } from './lib/observe';
19+
import { observe } from './lib/observe';
2020
import { onHidden } from './lib/onHidden';
21-
import { ReportHandler } from './types';
21+
import { CLSMetric, ReportCallback, ReportOpts } from './types';
2222

23-
// https://wicg.github.io/layout-instability/#sec-layout-shift
24-
export interface LayoutShift extends PerformanceEntry {
25-
value: number;
26-
hadRecentInput: boolean;
27-
sources: Array<LayoutShiftAttribution>;
28-
toJSON(): Record<string, unknown>;
29-
}
30-
31-
export interface LayoutShiftAttribution {
32-
node?: Node;
33-
previousRect: DOMRectReadOnly;
34-
currentRect: DOMRectReadOnly;
35-
}
36-
37-
export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
23+
/**
24+
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
25+
* calls the `callback` function once the value is ready to be reported, along
26+
* with all `layout-shift` performance entries that were used in the metric
27+
* value calculation. The reported value is a `double` (corresponding to a
28+
* [layout shift score](https://web.dev/cls/#layout-shift-score)).
29+
*
30+
* If the `reportAllChanges` configuration option is set to `true`, the
31+
* `callback` function will be called as soon as the value is initially
32+
* determined as well as any time the value changes throughout the page
33+
* lifespan.
34+
*
35+
* _**Important:** CLS should be continually monitored for changes throughout
36+
* the entire lifespan of a page—including if the user returns to the page after
37+
* it's been hidden/backgrounded. However, since browsers often [will not fire
38+
* additional callbacks once the user has backgrounded a
39+
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
40+
* `callback` is always called when the page's visibility state changes to
41+
* hidden. As a result, the `callback` function might be called multiple times
42+
* during the same page load._
43+
*/
44+
export const onCLS = (onReport: ReportCallback, opts: ReportOpts = {}): void => {
3845
const metric = initMetric('CLS', 0);
3946
let report: ReturnType<typeof bindReporter>;
4047

4148
let sessionValue = 0;
4249
let sessionEntries: PerformanceEntry[] = [];
4350

44-
const entryHandler = (entry: LayoutShift): void => {
45-
// Only count layout shifts without recent user input.
46-
// TODO: Figure out why entry can be undefined
47-
if (entry && !entry.hadRecentInput) {
48-
const firstSessionEntry = sessionEntries[0];
49-
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
51+
// const handleEntries = (entries: Metric['entries']) => {
52+
const handleEntries = (entries: LayoutShift[]): void => {
53+
entries.forEach(entry => {
54+
// Only count layout shifts without recent user input.
55+
if (!entry.hadRecentInput) {
56+
const firstSessionEntry = sessionEntries[0];
57+
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
5058

51-
// If the entry occurred less than 1 second after the previous entry and
52-
// less than 5 seconds after the first entry in the session, include the
53-
// entry in the current session. Otherwise, start a new session.
54-
if (
55-
sessionValue &&
56-
sessionEntries.length !== 0 &&
57-
entry.startTime - lastSessionEntry.startTime < 1000 &&
58-
entry.startTime - firstSessionEntry.startTime < 5000
59-
) {
60-
sessionValue += entry.value;
61-
sessionEntries.push(entry);
62-
} else {
63-
sessionValue = entry.value;
64-
sessionEntries = [entry];
65-
}
59+
// If the entry occurred less than 1 second after the previous entry and
60+
// less than 5 seconds after the first entry in the session, include the
61+
// entry in the current session. Otherwise, start a new session.
62+
if (
63+
sessionValue &&
64+
entry.startTime - lastSessionEntry.startTime < 1000 &&
65+
entry.startTime - firstSessionEntry.startTime < 5000
66+
) {
67+
sessionValue += entry.value;
68+
sessionEntries.push(entry);
69+
} else {
70+
sessionValue = entry.value;
71+
sessionEntries = [entry];
72+
}
6673

67-
// If the current session value is larger than the current CLS value,
68-
// update CLS and the entries contributing to it.
69-
if (sessionValue > metric.value) {
70-
metric.value = sessionValue;
71-
metric.entries = sessionEntries;
72-
if (report) {
73-
report();
74+
// If the current session value is larger than the current CLS value,
75+
// update CLS and the entries contributing to it.
76+
if (sessionValue > metric.value) {
77+
metric.value = sessionValue;
78+
metric.entries = sessionEntries;
79+
if (report) {
80+
report();
81+
}
7482
}
7583
}
76-
}
84+
});
7785
};
7886

79-
const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
87+
const po = observe('layout-shift', handleEntries);
8088
if (po) {
81-
report = bindReporter(onReport, metric, reportAllChanges);
89+
report = bindReporter(onReport, metric, opts.reportAllChanges);
8290

8391
onHidden(() => {
84-
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
92+
handleEntries(po.takeRecords() as CLSMetric['entries']);
8593
report(true);
8694
});
8795
}

packages/tracing/src/browser/web-vitals/getFID.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,44 @@
1717
import { bindReporter } from './lib/bindReporter';
1818
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1919
import { initMetric } from './lib/initMetric';
20-
import { observe, PerformanceEntryHandler } from './lib/observe';
20+
import { observe } from './lib/observe';
2121
import { onHidden } from './lib/onHidden';
22-
import { PerformanceEventTiming, ReportHandler } from './types';
22+
import { FIDMetric, PerformanceEventTiming, ReportCallback, ReportOpts } from './types';
2323

24-
export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
24+
/**
25+
* Calculates the [FID](https://web.dev/fid/) value for the current page and
26+
* calls the `callback` function once the value is ready, along with the
27+
* relevant `first-input` performance entry used to determine the value. The
28+
* reported value is a `DOMHighResTimeStamp`.
29+
*
30+
* _**Important:** since FID is only reported after the user interacts with the
31+
* page, it's possible that it will not be reported for some page loads._
32+
*/
33+
export const onFID = (onReport: ReportCallback, opts: ReportOpts = {}): void => {
2534
const visibilityWatcher = getVisibilityWatcher();
2635
const metric = initMetric('FID');
36+
// eslint-disable-next-line prefer-const
2737
let report: ReturnType<typeof bindReporter>;
2838

29-
const entryHandler = (entry: PerformanceEventTiming): void => {
39+
const handleEntry = (entry: PerformanceEventTiming): void => {
3040
// Only report if the page wasn't hidden prior to the first input.
31-
if (report && entry.startTime < visibilityWatcher.firstHiddenTime) {
41+
if (entry.startTime < visibilityWatcher.firstHiddenTime) {
3242
metric.value = entry.processingStart - entry.startTime;
3343
metric.entries.push(entry);
3444
report(true);
3545
}
3646
};
3747

38-
const po = observe('first-input', entryHandler as PerformanceEntryHandler);
48+
const handleEntries = (entries: FIDMetric['entries']): void => {
49+
(entries as PerformanceEventTiming[]).forEach(handleEntry);
50+
};
51+
52+
const po = observe('first-input', handleEntries);
53+
report = bindReporter(onReport, metric, opts.reportAllChanges);
54+
3955
if (po) {
40-
report = bindReporter(onReport, metric, reportAllChanges);
4156
onHidden(() => {
42-
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
57+
handleEntries(po.takeRecords() as FIDMetric['entries']);
4358
po.disconnect();
4459
}, true);
4560
}

packages/tracing/src/browser/web-vitals/getLCP.ts

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,55 +15,57 @@
1515
*/
1616

1717
import { bindReporter } from './lib/bindReporter';
18+
import { getActivationStart } from './lib/getActivationStart';
1819
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1920
import { initMetric } from './lib/initMetric';
20-
import { observe, PerformanceEntryHandler } from './lib/observe';
21+
import { observe } from './lib/observe';
2122
import { onHidden } from './lib/onHidden';
22-
import { ReportHandler } from './types';
23-
24-
// https://wicg.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface
25-
export interface LargestContentfulPaint extends PerformanceEntry {
26-
renderTime: DOMHighResTimeStamp;
27-
loadTime: DOMHighResTimeStamp;
28-
size: number;
29-
id: string;
30-
url: string;
31-
element?: Element;
32-
toJSON(): Record<string, string>;
33-
}
23+
import { LCPMetric, ReportCallback, ReportOpts } from './types';
3424

3525
const reportedMetricIDs: Record<string, boolean> = {};
3626

37-
export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
27+
/**
28+
* Calculates the [LCP](https://web.dev/lcp/) value for the current page and
29+
* calls the `callback` function once the value is ready (along with the
30+
* relevant `largest-contentful-paint` performance entry used to determine the
31+
* value). The reported value is a `DOMHighResTimeStamp`.
32+
*
33+
* If the `reportAllChanges` configuration option is set to `true`, the
34+
* `callback` function will be called any time a new `largest-contentful-paint`
35+
* performance entry is dispatched, or once the final value of the metric has
36+
* been determined.
37+
*/
38+
export const onLCP = (onReport: ReportCallback, opts: ReportOpts = {}): void => {
3839
const visibilityWatcher = getVisibilityWatcher();
3940
const metric = initMetric('LCP');
4041
let report: ReturnType<typeof bindReporter>;
4142

42-
const entryHandler = (entry: PerformanceEntry): void => {
43-
// The startTime attribute returns the value of the renderTime if it is not 0,
44-
// and the value of the loadTime otherwise.
45-
const value = entry.startTime;
43+
const handleEntries = (entries: LCPMetric['entries']): void => {
44+
const lastEntry = entries[entries.length - 1] as LargestContentfulPaint;
45+
if (lastEntry) {
46+
// The startTime attribute returns the value of the renderTime if it is
47+
// not 0, and the value of the loadTime otherwise. The activationStart
48+
// reference is used because LCP should be relative to page activation
49+
// rather than navigation start if the page was prerendered.
50+
const value = Math.max(lastEntry.startTime - getActivationStart(), 0);
4651

47-
// If the page was hidden prior to paint time of the entry,
48-
// ignore it and mark the metric as final, otherwise add the entry.
49-
if (value < visibilityWatcher.firstHiddenTime) {
50-
metric.value = value;
51-
metric.entries.push(entry);
52-
}
53-
54-
if (report) {
55-
report();
52+
// Only report if the page wasn't hidden prior to LCP.
53+
if (value < visibilityWatcher.firstHiddenTime) {
54+
metric.value = value;
55+
metric.entries = [lastEntry];
56+
report();
57+
}
5658
}
5759
};
5860

59-
const po = observe('largest-contentful-paint', entryHandler);
61+
const po = observe('largest-contentful-paint', handleEntries);
6062

6163
if (po) {
62-
report = bindReporter(onReport, metric, reportAllChanges);
64+
report = bindReporter(onReport, metric, opts.reportAllChanges);
6365

6466
const stopListening = (): void => {
6567
if (!reportedMetricIDs[metric.id]) {
66-
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
68+
handleEntries(po.takeRecords() as LCPMetric['entries']);
6769
po.disconnect();
6870
reportedMetricIDs[metric.id] = true;
6971
report(true);

0 commit comments

Comments
 (0)