Skip to content

Commit d352627

Browse files
committed
feat(tracing): Upgrade to web-vitals 2.1.0
1 parent 85f2fe8 commit d352627

File tree

17 files changed

+546
-201
lines changed

17 files changed

+546
-201
lines changed

packages/tracing/src/browser/metrics.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ 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 { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
1313
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from './web-vitals/types';
1414

1515
const global = getGlobalObject<Window>();
@@ -92,9 +92,9 @@ export class MetricsInstrumentation {
9292

9393
// capture web vitals
9494

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

9999
if (entry.name === 'first-paint' && shouldRecord) {
100100
logger.log('[Measurements] Adding FP');
@@ -243,13 +243,11 @@ export class MetricsInstrumentation {
243243
*/
244244
private _trackNavigator(transaction: Transaction): void {
245245
const navigator = global.navigator as null | (Navigator & NavigatorNetworkInformation & NavigatorDeviceMemory);
246-
247246
if (!navigator) {
248247
return;
249248
}
250249

251250
// track network connectivity
252-
253251
const connection = navigator.connection;
254252
if (connection) {
255253
if (connection.effectiveType) {

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,31 @@
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+
## License
1516

1617
[Apache 2.0](https://github.com/GoogleChrome/web-vitals/blob/master/LICENSE)
18+
19+
## Notable differences from `web-vitals` library
20+
21+
TODO(abhi): Fill this out
22+
23+
## CHANGELOG
24+
25+
https://github.com/getsentry/sentry-javascript/pull/3515
26+
- Remove support for Time to First Byte (TTFB)
27+
28+
https://github.com/getsentry/sentry-javascript/pull/2964
29+
- Added support for Cumulative Layout Shift (CLS) and Time to First Byte (TTFB)
30+
31+
https://github.com/getsentry/sentry-javascript/pull/2909
32+
- Added support for FID (First Input Delay) and LCP (Largest Contentful Paint)

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

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { getFCP } from './getFCP';
1718
import { bindReporter } from './lib/bindReporter';
1819
import { initMetric } from './lib/initMetric';
1920
import { observe, PerformanceEntryHandler } from './lib/observe';
21+
import { onBFCacheRestore } from './lib/onBFCacheRestore';
2022
import { onHidden } from './lib/onHidden';
2123
import { ReportHandler } from './types';
2224

@@ -34,31 +36,76 @@ export interface LayoutShiftAttribution {
3436
currentRect: DOMRectReadOnly;
3537
}
3638

37-
export const getCLS = (onReport: ReportHandler, reportAllChanges = false): void => {
38-
const metric = initMetric('CLS', 0);
39+
let isMonitoringFCP = false;
40+
let fcpValue = -1;
3941

42+
export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
43+
// Start monitoring FCP so we can only report CLS if FCP is also reported.
44+
// Note: this is done to match the current behavior of CrUX.
45+
if (!isMonitoringFCP) {
46+
getFCP(metric => {
47+
fcpValue = metric.value;
48+
});
49+
isMonitoringFCP = true;
50+
}
51+
52+
const onReportWrapped: ReportHandler = arg => {
53+
if (fcpValue > -1) {
54+
onReport(arg);
55+
}
56+
};
57+
58+
let metric = initMetric('CLS', 0);
4059
let report: ReturnType<typeof bindReporter>;
4160

61+
let sessionValue = 0;
62+
let sessionEntries: PerformanceEntry[] = [];
63+
4264
const entryHandler = (entry: LayoutShift): void => {
4365
// Only count layout shifts without recent user input.
4466
if (!entry.hadRecentInput) {
45-
(metric.value as number) += entry.value;
46-
metric.entries.push(entry);
47-
report();
67+
const firstSessionEntry = sessionEntries[0];
68+
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
69+
70+
// If the entry occurred less than 1 second after the previous entry and
71+
// less than 5 seconds after the first entry in the session, include the
72+
// entry in the current session. Otherwise, start a new session.
73+
if (
74+
sessionValue &&
75+
entry.startTime - lastSessionEntry.startTime < 1000 &&
76+
entry.startTime - firstSessionEntry.startTime < 5000
77+
) {
78+
sessionValue += entry.value;
79+
sessionEntries.push(entry);
80+
} else {
81+
sessionValue = entry.value;
82+
sessionEntries = [entry];
83+
}
84+
85+
// If the current session value is larger than the current CLS value,
86+
// update CLS and the entries contributing to it.
87+
if (sessionValue > metric.value) {
88+
metric.value = sessionValue;
89+
metric.entries = sessionEntries;
90+
report();
91+
}
4892
}
4993
};
5094

5195
const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
5296
if (po) {
53-
report = bindReporter(onReport, metric, po, reportAllChanges);
97+
report = bindReporter(onReportWrapped, metric, reportAllChanges);
5498

55-
onHidden(({ isUnloading }) => {
99+
onHidden(() => {
56100
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
101+
report(true);
102+
});
57103

58-
if (isUnloading) {
59-
metric.isFinal = true;
60-
}
61-
report();
104+
onBFCacheRestore(() => {
105+
sessionValue = 0;
106+
fcpValue = -1;
107+
metric = initMetric('CLS', 0);
108+
report = bindReporter(onReportWrapped, metric, reportAllChanges);
62109
});
63110
}
64111
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { bindReporter } from './lib/bindReporter';
18+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
19+
import { initMetric } from './lib/initMetric';
20+
import { observe } from './lib/observe';
21+
import { onBFCacheRestore } from './lib/onBFCacheRestore';
22+
import { ReportHandler } from './types';
23+
24+
export const getFCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
25+
const visibilityWatcher = getVisibilityWatcher();
26+
let metric = initMetric('FCP');
27+
let report: ReturnType<typeof bindReporter>;
28+
29+
const entryHandler = (entry: PerformanceEntry): void => {
30+
if (entry.name === 'first-contentful-paint') {
31+
if (po) {
32+
po.disconnect();
33+
}
34+
35+
// Only report if the page wasn't hidden prior to the first paint.
36+
if (entry.startTime < visibilityWatcher.firstHiddenTime) {
37+
metric.value = entry.startTime;
38+
metric.entries.push(entry);
39+
report(true);
40+
}
41+
}
42+
};
43+
44+
// TODO(philipwalton): remove the use of `fcpEntry` once this bug is fixed.
45+
// https://bugs.webkit.org/show_bug.cgi?id=225305
46+
// Also, the check for `getEntriesByName` is needed to support Opera:
47+
// https://github.com/GoogleChrome/web-vitals/issues/159
48+
const fcpEntry = performance.getEntriesByName && performance.getEntriesByName('first-contentful-paint')[0];
49+
50+
const po = fcpEntry ? null : observe('paint', entryHandler);
51+
52+
if (fcpEntry || po) {
53+
report = bindReporter(onReport, metric, reportAllChanges);
54+
55+
if (fcpEntry) {
56+
entryHandler(fcpEntry);
57+
}
58+
59+
onBFCacheRestore(event => {
60+
metric = initMetric('FCP');
61+
report = bindReporter(onReport, metric, reportAllChanges);
62+
requestAnimationFrame(() => {
63+
requestAnimationFrame(() => {
64+
metric.value = performance.now() - event.timeStamp;
65+
report(true);
66+
});
67+
});
68+
});
69+
}
70+
};

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

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,75 +15,45 @@
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';
21+
import { onBFCacheRestore } from './lib/onBFCacheRestore';
2122
import { onHidden } from './lib/onHidden';
22-
import { ReportHandler } from './types';
23+
import { firstInputPolyfill, resetFirstInputPolyfill } from './lib/polyfills/firstInputPolyfill';
24+
import { FirstInputPolyfillCallback, PerformanceEventTiming, ReportHandler } from './types';
2325

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 => {
46-
const metric = initMetric('FID');
47-
const firstHidden = getFirstHidden();
26+
export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
27+
const visibilityWatcher = getVisibilityWatcher();
28+
let metric = initMetric('FID');
29+
let report: ReturnType<typeof bindReporter>;
4830

4931
const entryHandler = (entry: PerformanceEventTiming): void => {
5032
// Only report if the page wasn't hidden prior to the first input.
51-
if (entry.startTime < firstHidden.timeStamp) {
33+
if (entry.startTime < visibilityWatcher.firstHiddenTime) {
5234
metric.value = entry.processingStart - entry.startTime;
5335
metric.entries.push(entry);
54-
metric.isFinal = true;
55-
report();
36+
report(true);
5637
}
5738
};
5839

5940
const po = observe('first-input', entryHandler as PerformanceEntryHandler);
60-
const report = bindReporter(onReport, metric, po);
41+
report = bindReporter(onReport, metric, reportAllChanges);
6142

6243
if (po) {
6344
onHidden(() => {
6445
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
6546
po.disconnect();
6647
}, 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-
}
86-
});
87-
}
48+
}
49+
50+
// Only monitor bfcache restores if the browser supports FID natively.
51+
if (po) {
52+
onBFCacheRestore(() => {
53+
metric = initMetric('FID');
54+
report = bindReporter(onReport, metric, reportAllChanges);
55+
resetFirstInputPolyfill();
56+
firstInputPolyfill(entryHandler as FirstInputPolyfillCallback);
57+
});
8858
}
8959
};

0 commit comments

Comments
 (0)