Skip to content

Commit 610ae69

Browse files
authored
feat(browser-utils): bump web-vitals to 5.1.0 (#18091)
Bumps the vendored-in web vitals library to include the changes between `5.0.2` <-> `5.1.0` from upstream #### Changes from upstream - Remove `visibilitychange` event listeners when no longer required [#627](GoogleChrome/web-vitals#627) - Register visibility-change early [#637](GoogleChrome/web-vitals#637) - Only finalize LCP on user events (isTrusted=true) [#635](GoogleChrome/web-vitals#635) - Fallback to default getSelector if custom function is null or undefined [#634](GoogleChrome/web-vitals#634) #### Our own Changes - Added `addPageListener` and `removePageListener` utilities because the upstream package changed the listeners to be added on `window` instead of `document`, so I added those utilities to avoid having to check for window every time we try to add a listener.
1 parent 9482a02 commit 610ae69

File tree

13 files changed

+135
-67
lines changed

13 files changed

+135
-67
lines changed

.size-limit.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = [
3838
path: 'packages/browser/build/npm/esm/prod/index.js',
3939
import: createImport('init', 'browserTracingIntegration'),
4040
gzip: true,
41-
limit: '41.3 KB',
41+
limit: '41.38 KB',
4242
},
4343
{
4444
name: '@sentry/browser (incl. Tracing, Profiling)',
@@ -127,7 +127,7 @@ module.exports = [
127127
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
128128
ignore: ['react/jsx-runtime'],
129129
gzip: true,
130-
limit: '43.3 KB',
130+
limit: '43.33 KB',
131131
},
132132
// Vue SDK (ESM)
133133
{
@@ -142,7 +142,7 @@ module.exports = [
142142
path: 'packages/vue/build/esm/index.js',
143143
import: createImport('init', 'browserTracingIntegration'),
144144
gzip: true,
145-
limit: '43.1 KB',
145+
limit: '43.2 KB',
146146
},
147147
// Svelte SDK (ESM)
148148
{

packages/browser-utils/src/metrics/web-vitals/README.md

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

77
The commit SHA used is:
8-
[463abbd425cda01ed65e0b5d18be9f559fe446cb](https://github.com/GoogleChrome/web-vitals/tree/463abbd425cda01ed65e0b5d18be9f559fe446cb)
8+
[e22d23b22c1440e69c5fc25a2f373b1a425cc940](https://github.com/GoogleChrome/web-vitals/tree/e22d23b22c1440e69c5fc25a2f373b1a425cc940)
99

1010
Current vendored web vitals are:
1111

@@ -27,6 +27,12 @@ web-vitals only report once per pageload.
2727

2828
## CHANGELOG
2929

30+
- Bumped from Web Vitals 5.0.2 to 5.1.0
31+
- Remove `visibilitychange` event listeners when no longer required [#627](https://github.com/GoogleChrome/web-vitals/pull/627)
32+
- Register visibility-change early [#637](https://github.com/GoogleChrome/web-vitals/pull/637)
33+
- Only finalize LCP on user events (isTrusted=true) [#635](https://github.com/GoogleChrome/web-vitals/pull/635)
34+
- Fallback to default getSelector if custom function is null or undefined [#634](https://github.com/GoogleChrome/web-vitals/pull/634)
35+
3036
https://github.com/getsentry/sentry-javascript/pull/17076
3137

3238
- Removed FID-related code with v10 of the SDK

packages/browser-utils/src/metrics/web-vitals/getCLS.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { WINDOW } from '../../types';
1818
import { bindReporter } from './lib/bindReporter';
19+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1920
import { initMetric } from './lib/initMetric';
2021
import { initUnique } from './lib/initUnique';
2122
import { LayoutShiftManager } from './lib/LayoutShiftManager';
@@ -55,6 +56,7 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
5556
runOnce(() => {
5657
const metric = initMetric('CLS', 0);
5758
let report: ReturnType<typeof bindReporter>;
59+
const visibilityWatcher = getVisibilityWatcher();
5860

5961
const layoutShiftManager = initUnique(opts, LayoutShiftManager);
6062

@@ -76,11 +78,9 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
7678
if (po) {
7779
report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);
7880

79-
WINDOW.document?.addEventListener('visibilitychange', () => {
80-
if (WINDOW.document?.visibilityState === 'hidden') {
81-
handleEntries(po.takeRecords() as CLSMetric['entries']);
82-
report(true);
83-
}
81+
visibilityWatcher.onHidden(() => {
82+
handleEntries(po.takeRecords() as CLSMetric['entries']);
83+
report(true);
8484
});
8585

8686
// Queue a task to report (if nothing else triggers a report first).

packages/browser-utils/src/metrics/web-vitals/getINP.ts

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

1717
import { bindReporter } from './lib/bindReporter';
18+
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
1819
import { initMetric } from './lib/initMetric';
1920
import { initUnique } from './lib/initUnique';
2021
import { InteractionManager } from './lib/InteractionManager';
2122
import { observe } from './lib/observe';
22-
import { onHidden } from './lib/onHidden';
2323
import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill';
2424
import { whenActivated } from './lib/whenActivated';
2525
import { whenIdleOrHidden } from './lib/whenIdleOrHidden';
@@ -67,6 +67,8 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
6767
return;
6868
}
6969

70+
const visibilityWatcher = getVisibilityWatcher();
71+
7072
whenActivated(() => {
7173
// TODO(philipwalton): remove once the polyfill is no longer needed.
7274
initInteractionCountPolyfill();
@@ -116,10 +118,7 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts
116118
// where the first interaction is less than the `durationThreshold`.
117119
po.observe({ type: 'first-input', buffered: true });
118120

119-
// sentry: we use onHidden instead of directly listening to visibilitychange
120-
// because some browsers we still support (Safari <14.4) don't fully support
121-
// `visibilitychange` or have known bugs w.r.t the `visibilitychange` event.
122-
onHidden(() => {
121+
visibilityWatcher.onHidden(() => {
123122
handleEntries(po.takeRecords() as INPMetric['entries']);
124123
report(true);
125124
});

packages/browser-utils/src/metrics/web-vitals/getLCP.ts

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

17-
import { WINDOW } from '../../types';
1817
import { bindReporter } from './lib/bindReporter';
1918
import { getActivationStart } from './lib/getActivationStart';
2019
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
20+
import { addPageListener, removePageListener } from './lib/globalListeners';
2121
import { initMetric } from './lib/initMetric';
2222
import { initUnique } from './lib/initUnique';
2323
import { LCPEntryManager } from './lib/LCPEntryManager';
@@ -88,20 +88,28 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts =
8888
report(true);
8989
});
9090

91+
// Need a separate wrapper to ensure the `runOnce` function above is
92+
// common for all three functions
93+
const stopListeningWrapper = (event: Event) => {
94+
if (event.isTrusted) {
95+
// Wrap the listener in an idle callback so it's run in a separate
96+
// task to reduce potential INP impact.
97+
// https://github.com/GoogleChrome/web-vitals/issues/383
98+
whenIdleOrHidden(stopListening);
99+
removePageListener(event.type, stopListeningWrapper, {
100+
capture: true,
101+
});
102+
}
103+
};
104+
91105
// Stop listening after input or visibilitychange.
92106
// Note: while scrolling is an input that stops LCP observation, it's
93107
// unreliable since it can be programmatically generated.
94108
// See: https://github.com/GoogleChrome/web-vitals/issues/75
95109
for (const type of ['keydown', 'click', 'visibilitychange']) {
96-
// Wrap the listener in an idle callback so it's run in a separate
97-
// task to reduce potential INP impact.
98-
// https://github.com/GoogleChrome/web-vitals/issues/383
99-
if (WINDOW.document) {
100-
addEventListener(type, () => whenIdleOrHidden(stopListening), {
101-
capture: true,
102-
once: true,
103-
});
104-
}
110+
addPageListener(type, stopListeningWrapper, {
111+
capture: true,
112+
});
105113
}
106114
}
107115
});

packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616

1717
import { WINDOW } from '../../../types';
1818
import { getActivationStart } from './getActivationStart';
19+
import { addPageListener, removePageListener } from './globalListeners';
1920

2021
let firstHiddenTime = -1;
22+
const onHiddenFunctions: Set<() => void> = new Set();
2123

2224
const initHiddenTime = () => {
2325
// If the document is hidden when this code runs, assume it was always
@@ -29,35 +31,34 @@ const initHiddenTime = () => {
2931
};
3032

3133
const onVisibilityUpdate = (event: Event) => {
32-
// If the document is 'hidden' and no previous hidden timestamp has been
33-
// set, update it based on the current event data.
34-
if (WINDOW.document!.visibilityState === 'hidden' && firstHiddenTime > -1) {
35-
// If the event is a 'visibilitychange' event, it means the page was
36-
// visible prior to this change, so the event timestamp is the first
37-
// hidden time.
38-
// However, if the event is not a 'visibilitychange' event, then it must
39-
// be a 'prerenderingchange' event, and the fact that the document is
40-
// still 'hidden' from the above check means the tab was activated
41-
// in a background state and so has always been hidden.
42-
firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;
34+
// Handle changes to hidden state
35+
if (isPageHidden(event) && firstHiddenTime > -1) {
36+
// Sentry-specific change: Also call onHidden callbacks for pagehide events
37+
// to support older browsers (Safari <14.4) that don't properly fire visibilitychange
38+
if (event.type === 'visibilitychange' || event.type === 'pagehide') {
39+
for (const onHiddenFunction of onHiddenFunctions) {
40+
onHiddenFunction();
41+
}
42+
}
4343

44-
// Remove all listeners now that a `firstHiddenTime` value has been set.
45-
removeChangeListeners();
46-
}
47-
};
48-
49-
const addChangeListeners = () => {
50-
addEventListener('visibilitychange', onVisibilityUpdate, true);
51-
// IMPORTANT: when a page is prerendering, its `visibilityState` is
52-
// 'hidden', so in order to account for cases where this module checks for
53-
// visibility during prerendering, an additional check after prerendering
54-
// completes is also required.
55-
addEventListener('prerenderingchange', onVisibilityUpdate, true);
56-
};
44+
// If the document is 'hidden' and no previous hidden timestamp has been
45+
// set (so is infinity), update it based on the current event data.
46+
if (!isFinite(firstHiddenTime)) {
47+
// If the event is a 'visibilitychange' event, it means the page was
48+
// visible prior to this change, so the event timestamp is the first
49+
// hidden time.
50+
// However, if the event is not a 'visibilitychange' event, then it must
51+
// be a 'prerenderingchange' or 'pagehide' event, and the fact that the document is
52+
// still 'hidden' from the above check means the tab was activated
53+
// in a background state and so has always been hidden.
54+
firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;
5755

58-
const removeChangeListeners = () => {
59-
removeEventListener('visibilitychange', onVisibilityUpdate, true);
60-
removeEventListener('prerenderingchange', onVisibilityUpdate, true);
56+
// We no longer need the `prerenderingchange` event listener now we've
57+
// set an initial init time so remove that
58+
// (we'll keep the visibilitychange and pagehide ones for onHiddenFunction above)
59+
removePageListener('prerenderingchange', onVisibilityUpdate, true);
60+
}
61+
}
6162
};
6263

6364
export const getVisibilityWatcher = () => {
@@ -75,14 +76,39 @@ export const getVisibilityWatcher = () => {
7576
// a perfect heuristic, but it's the best we can do until the
7677
// `visibility-state` performance entry becomes available in all browsers.
7778
firstHiddenTime = firstVisibilityStateHiddenTime ?? initHiddenTime();
78-
// We're still going to listen to for changes so we can handle things like
79-
// bfcache restores and/or prerender without having to examine individual
80-
// timestamps in detail.
81-
addChangeListeners();
79+
// Listen for visibility changes so we can handle things like bfcache
80+
// restores and/or prerender without having to examine individual
81+
// timestamps in detail and also for onHidden function calls.
82+
addPageListener('visibilitychange', onVisibilityUpdate, true);
83+
84+
// Sentry-specific change: Some browsers have buggy implementations of visibilitychange,
85+
// so we use pagehide in addition, just to be safe. This is also required for older
86+
// Safari versions (<14.4) that we still support.
87+
addPageListener('pagehide', onVisibilityUpdate, true);
88+
89+
// IMPORTANT: when a page is prerendering, its `visibilityState` is
90+
// 'hidden', so in order to account for cases where this module checks for
91+
// visibility during prerendering, an additional check after prerendering
92+
// completes is also required.
93+
addPageListener('prerenderingchange', onVisibilityUpdate, true);
8294
}
95+
8396
return {
8497
get firstHiddenTime() {
8598
return firstHiddenTime;
8699
},
100+
onHidden(cb: () => void) {
101+
onHiddenFunctions.add(cb);
102+
},
87103
};
88104
};
105+
106+
/**
107+
* Check if the page is hidden, uses the `pagehide` event for older browsers support that we used to have in `onHidden` function.
108+
* Some browsers we still support (Safari <14.4) don't fully support `visibilitychange`
109+
* or have known bugs w.r.t the `visibilitychange` event.
110+
* // TODO (v11): If we decide to drop support for Safari 14.4, we can use the logic from web-vitals 4.2.4
111+
*/
112+
function isPageHidden(event: Event) {
113+
return event.type === 'pagehide' || WINDOW.document?.visibilityState === 'hidden';
114+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { WINDOW } from '../../../types';
2+
3+
/**
4+
* web-vitals 5.1.0 switched listeners to be added on the window rather than the document.
5+
* Instead of having to check for window/document every time we add a listener, we can use this function.
6+
*/
7+
export function addPageListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
8+
if (WINDOW.document) {
9+
WINDOW.addEventListener(type, listener, options);
10+
}
11+
}
12+
/**
13+
* web-vitals 5.1.0 switched listeners to be removed from the window rather than the document.
14+
* Instead of having to check for window/document every time we remove a listener, we can use this function.
15+
*/
16+
export function removePageListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
17+
if (WINDOW.document) {
18+
WINDOW.removeEventListener(type, listener, options);
19+
}
20+
}

packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { WINDOW } from '../../../types';
18+
import { addPageListener } from './globalListeners';
1819

1920
export interface OnHiddenCallback {
2021
(event: Event): void;
@@ -37,10 +38,8 @@ export const onHidden = (cb: OnHiddenCallback) => {
3738
}
3839
};
3940

40-
if (WINDOW.document) {
41-
addEventListener('visibilitychange', onHiddenOrPageHide, true);
42-
// Some browsers have buggy implementations of visibilitychange,
43-
// so we use pagehide in addition, just to be safe.
44-
addEventListener('pagehide', onHiddenOrPageHide, true);
45-
}
41+
addPageListener('visibilitychange', onHiddenOrPageHide, true);
42+
// Some browsers have buggy implementations of visibilitychange,
43+
// so we use pagehide in addition, just to be safe.
44+
addPageListener('pagehide', onHiddenOrPageHide, true);
4645
};

packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import { WINDOW } from '../../../types.js';
18+
import { addPageListener, removePageListener } from './globalListeners.js';
1819
import { onHidden } from './onHidden.js';
1920
import { runOnce } from './runOnce.js';
2021

@@ -32,7 +33,13 @@ export const whenIdleOrHidden = (cb: () => void) => {
3233
} else {
3334
// eslint-disable-next-line no-param-reassign
3435
cb = runOnce(cb);
35-
rIC(cb);
36+
addPageListener('visibilitychange', cb, { once: true, capture: true });
37+
rIC(() => {
38+
cb();
39+
// Remove the above event listener since no longer required.
40+
// See: https://github.com/GoogleChrome/web-vitals/issues/622
41+
removePageListener('visibilitychange', cb, { capture: true });
42+
});
3643
// sentry: we use onHidden instead of directly listening to visibilitychange
3744
// because some browsers we still support (Safari <14.4) don't fully support
3845
// `visibilitychange` or have known bugs w.r.t the `visibilitychange` event.

packages/browser-utils/src/metrics/web-vitals/types/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export interface ReportOpts {
116116
}
117117

118118
export interface AttributionReportOpts extends ReportOpts {
119-
generateTarget?: (el: Node | null) => string;
119+
generateTarget?: (el: Node | null) => string | undefined;
120120
}
121121

122122
/**

0 commit comments

Comments
 (0)