10000 feat(tracing): Upgrade to web-vitals 2.1.0 (#3781) · tomoat/sentry-javascript@41cf211 · GitHub
[go: up one dir, main page]

Skip to content

Commit 41cf211

Browse files
authored
feat(tracing): Upgrade to web-vitals 2.1.0 (getsentry#3781)
* feat(tracing): Upgrade to web-vitals 2.1.0
1 parent 5f35c8c commit 41cf211

File tree

14 files changed

+270
-192
lines changed

14 files changed

+270
-192
lines changed

packages/tracing/src/browser/metrics.ts

Lines changed: 33 additions & 7 deletions
6D40
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { msToSec } from '../utils';
99
import { getCLS, LayoutShift } from './web-vitals/getCLS';
1010
import { getFID } from './web-vitals/getFID';
1111
import { getLCP, LargestContentfulPaint } from './web-vitals/getLCP';
12-
import { getFirstHidden } from './web-vitals/lib/getFirstHidden';
12+
import { getUpdatedCLS } from './web-vitals/getUpdatedCLS';
13+
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
1314
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from './web-vitals/types';
1415

1516
const global = getGlobalObject<Window>();
@@ -21,6 +22,7 @@ export class MetricsInstrumentation {
2122
private _performanceCursor: number = 0;
2223
private _lcpEntry: LargestContentfulPaint | undefined;
2324
private _clsEntry: LayoutShift | undefined;
25+
private _updatedClsEntry: LayoutShift | undefined;
2426

2527
public constructor() {
2628
if (!isNodeEnv() && global?.performance) {
@@ -92,9 +94,9 @@ export class MetricsInstrumentation {
9294

9395
// capture web vitals
9496

95-
const firstHidden = getFirstHidden();
97+
const firstHidden = getVisibilityWatcher();
9698
// Only report if the page wasn't hidden prior to the web vital.
97-
const shouldRecord = entry.startTime < firstHidden.timeStamp;
99+
const shouldRecord = entry.startTime < firstHidden.firstHiddenTime;
98100

99101
if (entry.name === 'first-paint' && shouldRecord) {
100102
logger.log('[Measurements] Adding FP');
@@ -187,6 +189,12 @@ export class MetricsInstrumentation {
187189
});
188190
}
189191

192+
// If FCP is not recorded we should not record the updated cls value
193+
// according to the new definition of CLS.
194+
if (!('fcp' in this._measurements)) {
195+
delete this._measurements['updated-cls'];
196+
}
197+
190198
transaction.setMeasurements(this._measurements);
191199
this._tagMetricInfo(transaction);
192200
}
@@ -217,17 +225,23 @@ export class MetricsInstrumentation {
217225
// See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
218226
if (this._clsEntry && this._clsEntry.sources) {
219227
logger.log('[Measurements] Adding CLS Data');
220-
this._clsEntry.sources.map((source, index) =>
228+
this._clsEntry.sources.forEach((source, index) =>
221229
transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)),
222230
);
223231
}
232+
233+
if (this._updatedClsEntry && this._updatedClsEntry.sources) {
234+
logger.log('[Measurements] Adding Updated CLS Data');
235+
this._updatedClsEntry.sources.forEach((source, index) =>
236+
transaction.setTag(`updated-cls.source.${index + 1}`, htmlTreeAsString(source.node)),
237+
);
238+
}
224239
}
225240

226241
/** Starts tracking the Cumulative Layout Shift on the current page. */
227242
private _trackCLS(): void {
228243
getCLS(metric => {
229244
const entry = metric.entries.pop();
230-
231245
if (!entry) {
232246
return;
233247
}
@@ -236,20 +250,32 @@ export class MetricsInstrumentation {
236250
this._measurements['cls'] = { value: metric.value };
237251
this._clsEntry = entry as LayoutShift;
238252
});
253+
254+
// See:
255+
// https://web.dev/evolving-cls/
256+
// https://web.dev/cls-web-tooling/
257+
getUpdatedCLS(metric => {
258+
const entry = metric.entries.pop();
259+
if (!entry) {
260+
return;
261+
}
262+
263+
logger.log('[Measurements] Adding Updated CLS');
264+
this._measurements['updated-cls'] = { value: metric.value };
265+
this._updatedClsEntry = entry as LayoutShift;
266+
});
239267
}
240268

241269
/**
242270
* Capture the information of the user agent.
243271
*/
244272
private _trackNavigator(transaction: Transaction): void {
245273
const navigator = global.navigator as null | (Navigator & NavigatorNetworkInformation & NavigatorDeviceMemory);
246-
247274
if (!navigator) {
248275
return;
249276
}
250277

251278
// track network connectivity
252-
253279
const connection = navigator.connection;
254280
if (connection) {
255281
if (connection.effectiveType) {

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,35 @@
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
5+
This was vendored from: https://github.com/GoogleChrome/web-vitals: v2.1.0
66

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

99
Current vendored web vitals are:
1010

1111
- LCP (Largest Contentful Paint)
1212
- FID (First Input Delay)
13+
- CLS (Cumulative Layout Shift)
1314

14-
# License
15+
## Notable Changes from web-vitals library
16+
17+
This vendored web-vitals library is meant to be used in conjunction with the `@sentry/tracing` `BrowserTracing` integration.
18+
As such, logic around `BFCache` and multiple reports were removed from the library as our web-vitals only report once per pageload.
19+
20+
## License
1521

1622
[Apache 2.0](https://github.com/GoogleChrome/web-vitals/blob/master/LICENSE)
23+
24+
## CHANGELOG
25+
26+
https://github.com/getsentry/sentry-javascript/pull/3781
27+
- Bumped from Web Vitals v0.2.4 to v2.1.0
28+
29+
https://github.com/getsentry/sentry-javascript/pull/3515
30+
- Remove support for Time to First Byte (TTFB)
31+
32+
https://github.com/getsentry/sentry-javascript/pull/2964
33+
- Added support for Cumulative Layout Shift (CLS) and Time to First Byte (TTFB)
34+
35+
https://github.com/getsentry/sentry-javascript/pull/2909
36+
- Added support for FID (First Input Delay) and LCP (Largest Contentful Paint)

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,27 @@ export interface LayoutShiftAttribution {
3434
currentRect: DOMRectReadOnly;
3535
}
3636

37-
export const getCLS = (onReport: ReportHandler, reportAllChanges = false): void => {
37+
export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
3838
const metric = initMetric('CLS', 0);
39-
4039
let report: ReturnType<typeof bindReporter>;
4140

4241
const entryHandler = (entry: LayoutShift): void => {
43-
// Only count layout shifts without recent user input.
4442
if (!entry.hadRecentInput) {
4543
(metric.value as number) += entry.value;
4644
metric.entries.push(entry);
47-
report();
45+
if (report) {
46+
report();
47+
}
4848
}
4949
};
5050

5151
const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
5252
if (po) {
53-
report = bindReporter(onReport, metric, po, reportAllChanges);
53+
report = bindReporter(onReport, metric, reportAllChanges);
5454

55-
onHidden(({ isUnloading }) => {
55+
onHidden(() => {
5656
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
57-
58-
if (isUnloading) {
59-
metric.isFinal = true;
60-
}
61-
report();
57+
report(true);
6258
});
6359
}
6460
};

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

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,75 +15,32 @@
1515
*/
1616

1717
import { bindReporter } from './lib/bindReporter';
18-
import { getFirstHidden } from './lib/getFirstHidden';
18+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1919
import { initMetric } from './lib/initMetric';
2020
import { observe, PerformanceEntryHandler } from './lib/observe';
2121
import { onHidden } from './lib/onHidden';
22-
import { ReportHandler } from './types';
22+
import { PerformanceEventTiming, ReportHandler } from './types';
2323

24-
interface FIDPolyfillCallback {
25-
(value: number, event: Event): void;
26-
}
27-
28-
interface FIDPolyfill {
29-
onFirstInputDelay: (onReport: FIDPolyfillCallback) => void;
30-
}
31-
32-
declare global {
33-
interface Window {
34-
perfMetrics: FIDPolyfill;
35-
}
36-
}
37-
38-
// https://wicg.github.io/event-timing/#sec-performance-event-timing
39-
interface PerformanceEventTiming extends PerformanceEntry {
40-
processingStart: DOMHighResTimeStamp;
41-
cancelable?: boolean;
42-
target?: Element;
43-
}
44-
45-
export const getFID = (onReport: ReportHandler): void => {
24+
export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
25+
const visibilityWatcher = getVisibilityWatcher();
4626
const metric = initMetric('FID');
47-
const firstHidden = getFirstHidden();
27+
let report: ReturnType<typeof bindReporter>;
4828

4929
const entryHandler = (entry: PerformanceEventTiming): void => {
5030
// Only report if the page wasn't hidden prior to the first input.
51-
if (entry.startTime < firstHidden.timeStamp) {
31+
if (report && entry.startTime < visibilityWatcher.firstHiddenTime) {
5232
metric.value = entry.processingStart - entry.startTime;
5333
metric.entries.push(entry);
54-
metric.isFinal = true;
55-
report();
34+
report(true);
5635
}
5736
};
5837

5938
const po = observe('first-input', entryHandler as PerformanceEntryHandler);
60-
const report = bindReporter(onReport, metric, po);
61-
6239
if (po) {
40+
report = bindReporter(onReport, metric, reportAllChanges);
6341
onHidden(() => {
6442
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
6543
po.disconnect();
6644
}, true);
67-
} else {
68-
if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) {
69-
window.perfMetrics.onFirstInputDelay((value: number, event: Event) => {
70-
// Only report if the page wasn't hidden prior to the first input.
71-
if (event.timeStamp < firstHidden.timeStamp) {
72-
metric.value = value;
73-
metric.isFinal = true;
74-
metric.entries = [
75-
{
76-
entryType: 'first-input',
77-
name: event.type,
78-
target: event.target,
79-
cancelable: event.cancelable,
80-
startTime: event.timeStamp,
81-
processingStart: event.timeStamp + value,
82-
} as PerformanceEventTiming,
83-
];
84-
report();
85-
}< 10000 /span>
86-
});
87-
}
8845
}
8946
};

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

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515
*/
1616

1717
import { bindReporter } from './lib/bindReporter';
18-
import { getFirstHidden } from './lib/getFirstHidden';
18+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1919
import { initMetric } from './lib/initMetric';
2020
import { observe, PerformanceEntryHandler } from './lib/observe';
2121
import { onHidden } from './lib/onHidden';
22-
import { whenInput } from './lib/whenInput';
2322
import { ReportHandler } from './types';
2423

2524
// https://wicg.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface
@@ -33,10 +32,11 @@ export interface LargestContentfulPaint extends PerformanceEntry {
3332
toJSON(): Record<string, string>;
3433
}
3534

36-
export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void => {
37-
const metric = initMetric('LCP');
38-
const firstHidden = getFirstHidden();
35+
const reportedMetricIDs: Record<string, boolean> = {};
3936

37+
export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
38+
const visibilityWatcher = getVisibilityWatcher();
39+
const metric = initMetric('LCP');
4040
let report: ReturnType<typeof bindReporter>;
4141

4242
const entryHandler = (entry: PerformanceEntry): void => {
@@ -46,30 +46,37 @@ export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void
4646

4747
// If the page was hidden prior to paint time of the entry,
4848
// ignore it and mark the metric as final, otherwise add the entry.
49-
if (value < firstHidden.timeStamp) {
49+
if (value < visibilityWatcher.firstHiddenTime) {
5050
metric.value = value;
5151
metric.entries.push(entry);
52-
} else {
53-
metric.isFinal = true;
5452
}
5553

56-
report();
54+
if (report) {
55+
report();
56+
}
5757
};
5858

5959
const po = observe('largest-contentful-paint', entryHandler);
6060

6161
if (po) {
62-
report = bindReporter(onReport, metric, po, reportAllChanges);
62+
report = bindReporter(onReport, metric, reportAllChanges);
6363

64-
const onFinal = (): void => {
65-
if (!metric.isFinal) {
64+
const stopListening = (): void => {
65+
if (!reportedMetricIDs[metric.id]) {
6666
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
67-
metric.isFinal = true;
68-
report();
67+
po.disconnect();
68+
reportedMetricIDs[metric.id] = true;
69+
report(true);
6970
}
7071
};
7172

72-
void whenInput().then(onFinal);
73-
onHidden(onFinal, true);
73+
// Stop listening after input. Note: while scrolling is an input that
74+
// stop LCP observation, it's unreliable since it can be programmatically
75+
// generated. See: https://github.com/GoogleChrome/web-vitals/issues/75
76+
['keydown', 'click'].forEach(type => {
77+
addEventListener(type, stopListening, { once: true, capture: true });
78+
});
79+
80+
onHidden(stopListening, true);
7481
}
7582
};

0 commit comments

Comments
 (0)
0