From 4ae999c8488dcd99050b5a8f6b515fe67d582360 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 4 Nov 2025 15:51:26 +0200 Subject: [PATCH 1/6] feat: bump webvitals to 5.0.3 --- packages/browser-utils/src/metrics/web-vitals/README.md | 4 ++-- .../src/metrics/web-vitals/lib/whenIdleOrHidden.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/browser-utils/src/metrics/web-vitals/README.md b/packages/browser-utils/src/metrics/web-vitals/README.md index a57937246cdd..1fe1582e32b2 100644 --- a/packages/browser-utils/src/metrics/web-vitals/README.md +++ b/packages/browser-utils/src/metrics/web-vitals/README.md @@ -2,10 +2,10 @@ > A modular library for measuring the [Web Vitals](https://web.dev/vitals/) metrics on real users. -This was vendored from: https://github.com/GoogleChrome/web-vitals: v5.0.2 +This was vendored from: https://github.com/GoogleChrome/web-vitals: v5.0.3 The commit SHA used is: -[463abbd425cda01ed65e0b5d18be9f559fe446cb](https://github.com/GoogleChrome/web-vitals/tree/463abbd425cda01ed65e0b5d18be9f559fe446cb) +[e22d23b22c1440e69c5fc25a2f373b1a425cc940](https://github.com/GoogleChrome/web-vitals/tree/e22d23b22c1440e69c5fc25a2f373b1a425cc940) Current vendored web vitals are: diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts index 32dae5f30f8b..9677e25ba802 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts @@ -32,7 +32,12 @@ export const whenIdleOrHidden = (cb: () => void) => { } else { // eslint-disable-next-line no-param-reassign cb = runOnce(cb); - rIC(cb); + rIC(() => { + cb(); + // Remove the above event listener since no longer required. + // See: https://github.com/GoogleChrome/web-vitals/issues/622 + WINDOW.document?.removeEventListener('visibilitychange', cb); + }); // sentry: we use onHidden instead of directly listening to visibilitychange // because some browsers we still support (Safari <14.4) don't fully support // `visibilitychange` or have known bugs w.r.t the `visibilitychange` event. From 9afa070f6b3d4fbd32e0a005d00b814ec846c64a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 4 Nov 2025 16:13:19 +0200 Subject: [PATCH 2/6] feat: include web-vitals#634 --- packages/browser-utils/src/metrics/web-vitals/types/base.ts | 2 +- packages/browser-utils/src/metrics/web-vitals/types/cls.ts | 3 ++- packages/browser-utils/src/metrics/web-vitals/types/inp.ts | 3 ++- packages/browser-utils/src/metrics/web-vitals/types/lcp.ts | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/browser-utils/src/metrics/web-vitals/types/base.ts b/packages/browser-utils/src/metrics/web-vitals/types/base.ts index 02cb566011ac..cac7fdac1d11 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/base.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/base.ts @@ -116,7 +116,7 @@ export interface ReportOpts { } export interface AttributionReportOpts extends ReportOpts { - generateTarget?: (el: Node | null) => string; + generateTarget?: (el: Node | null) => string | undefined; } /** diff --git a/packages/browser-utils/src/metrics/web-vitals/types/cls.ts b/packages/browser-utils/src/metrics/web-vitals/types/cls.ts index 5acaaa27c9ab..6048c616e1f0 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/cls.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/cls.ts @@ -34,7 +34,8 @@ export interface CLSAttribution { * By default, a selector identifying the first element (in document order) * that shifted when the single largest layout shift that contributed to the * page's CLS score occurred. If the `generateTarget` configuration option - * was passed, then this will instead be the return value of that function. + * was passed, then this will instead be the return value of that function, + * falling back to the default if that returns null or undefined. */ largestShiftTarget?: string; /** diff --git a/packages/browser-utils/src/metrics/web-vitals/types/inp.ts b/packages/browser-utils/src/metrics/web-vitals/types/inp.ts index e73743866301..d2b2063c7d04 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/inp.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/inp.ts @@ -60,7 +60,8 @@ export interface INPAttribution { * occurred. If this value is an empty string, that generally means the * element was removed from the DOM after the interaction. If the * `generateTarget` configuration option was passed, then this will instead - * be the return value of that function. + * be the return value of that function, falling back to the default if that + * returns null or undefined. */ interactionTarget: string; /** diff --git a/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts b/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts index 293531b3d45c..9de6b32a5f94 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts @@ -34,7 +34,8 @@ export interface LCPAttribution { * By default, a selector identifying the element corresponding to the * largest contentful paint for the page. If the `generateTarget` * configuration option was passed, then this will instead be the return - * value of that function. + * value of that function, falling back to the default if that returns null + * or undefined. */ target?: string; /** From 9b5b72f3d593f46569d1594084b6a2c7a253a05d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 4 Nov 2025 16:19:48 +0200 Subject: [PATCH 3/6] feat: include web-vitals#635 --- .../src/metrics/web-vitals/getLCP.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts index 6eafee698673..a203c1e3fc81 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts @@ -88,18 +88,30 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = report(true); }); + // Need a separate wrapper to ensure the `runOnce` function above is + // common for all three functions + const stopListeningWrapper = (event: Event) => { + if (event.isTrusted) { + // Wrap the listener in an idle callback so it's run in a separate + // task to reduce potential INP impact. + // https://github.com/GoogleChrome/web-vitals/issues/383 + whenIdleOrHidden(stopListening); + if (WINDOW.document) { + removeEventListener(event.type, stopListeningWrapper, { + capture: true, + }); + } + } + }; + // Stop listening after input or visibilitychange. // Note: while scrolling is an input that stops LCP observation, it's // unreliable since it can be programmatically generated. // See: https://github.com/GoogleChrome/web-vitals/issues/75 for (const type of ['keydown', 'click', 'visibilitychange']) { - // Wrap the listener in an idle callback so it's run in a separate - // task to reduce potential INP impact. - // https://github.com/GoogleChrome/web-vitals/issues/383 if (WINDOW.document) { - addEventListener(type, () => whenIdleOrHidden(stopListening), { + addEventListener(type, stopListeningWrapper, { capture: true, - once: true, }); } } From bb2b7c1ffb765e2216c53a014ecf688fa8679cb2 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 4 Nov 2025 16:40:50 +0200 Subject: [PATCH 4/6] feat: include web-vitals#637 --- .../src/metrics/web-vitals/getCLS.ts | 10 +-- .../src/metrics/web-vitals/getINP.ts | 9 +- .../web-vitals/lib/getVisibilityWatcher.ts | 84 ++++++++++++------- .../web-vitals/lib/whenIdleOrHidden.ts | 3 +- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/packages/browser-utils/src/metrics/web-vitals/getCLS.ts b/packages/browser-utils/src/metrics/web-vitals/getCLS.ts index c40f993f8ca8..2e3f98c599e4 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getCLS.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getCLS.ts @@ -16,6 +16,7 @@ import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; import { initUnique } from './lib/initUnique'; import { LayoutShiftManager } from './lib/LayoutShiftManager'; @@ -55,6 +56,7 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts = runOnce(() => { const metric = initMetric('CLS', 0); let report: ReturnType; + const visibilityWatcher = getVisibilityWatcher(); const layoutShiftManager = initUnique(opts, LayoutShiftManager); @@ -76,11 +78,9 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts = if (po) { report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges); - WINDOW.document?.addEventListener('visibilitychange', () => { - if (WINDOW.document?.visibilityState === 'hidden') { - handleEntries(po.takeRecords() as CLSMetric['entries']); - report(true); - } + visibilityWatcher.onHidden(() => { + handleEntries(po.takeRecords() as CLSMetric['entries']); + report(true); }); // Queue a task to report (if nothing else triggers a report first). diff --git a/packages/browser-utils/src/metrics/web-vitals/getINP.ts b/packages/browser-utils/src/metrics/web-vitals/getINP.ts index f5efbcbc3afc..df8ac5e1c804 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getINP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getINP.ts @@ -15,11 +15,11 @@ */ import { bindReporter } from './lib/bindReporter'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; import { initUnique } from './lib/initUnique'; import { InteractionManager } from './lib/InteractionManager'; import { observe } from './lib/observe'; -import { onHidden } from './lib/onHidden'; import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill'; import { whenActivated } from './lib/whenActivated'; import { whenIdleOrHidden } from './lib/whenIdleOrHidden'; @@ -67,6 +67,8 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts return; } + const visibilityWatcher = getVisibilityWatcher(); + whenActivated(() => { // TODO(philipwalton): remove once the polyfill is no longer needed. initInteractionCountPolyfill(); @@ -116,10 +118,7 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts // where the first interaction is less than the `durationThreshold`. po.observe({ type: 'first-input', buffered: true }); - // sentry: we use onHidden instead of directly listening to visibilitychange - // because some browsers we still support (Safari <14.4) don't fully support - // `visibilitychange` or have known bugs w.r.t the `visibilitychange` event. - onHidden(() => { + visibilityWatcher.onHidden(() => { handleEntries(po.takeRecords() as INPMetric['entries']); report(true); }); diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts index 3a6c0a2e42a9..e69e73ec9291 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts @@ -18,6 +18,7 @@ import { WINDOW } from '../../../types'; import { getActivationStart } from './getActivationStart'; let firstHiddenTime = -1; +const onHiddenFunctions: Set<() => void> = new Set(); const initHiddenTime = () => { // If the document is hidden when this code runs, assume it was always @@ -29,35 +30,32 @@ const initHiddenTime = () => { }; const onVisibilityUpdate = (event: Event) => { - // If the document is 'hidden' and no previous hidden timestamp has been - // set, update it based on the current event data. - if (WINDOW.document!.visibilityState === 'hidden' && firstHiddenTime > -1) { - // If the event is a 'visibilitychange' event, it means the page was - // visible prior to this change, so the event timestamp is the first - // hidden time. - // However, if the event is not a 'visibilitychange' event, then it must - // be a 'prerenderingchange' event, and the fact that the document is - // still 'hidden' from the above check means the tab was activated - // in a background state and so has always been hidden. - firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0; + // Handle changes to hidden state + if (isPageHidden(event) && firstHiddenTime > -1) { + if (event.type === 'visibilitychange') { + for (const onHiddenFunction of onHiddenFunctions) { + onHiddenFunction(); + } + } - // Remove all listeners now that a `firstHiddenTime` value has been set. - removeChangeListeners(); - } -}; - -const addChangeListeners = () => { - addEventListener('visibilitychange', onVisibilityUpdate, true); - // IMPORTANT: when a page is prerendering, its `visibilityState` is - // 'hidden', so in order to account for cases where this module checks for - // visibility during prerendering, an additional check after prerendering - // completes is also required. - addEventListener('prerenderingchange', onVisibilityUpdate, true); -}; + // If the document is 'hidden' and no previous hidden timestamp has been + // set (so is infinity), update it based on the current event data. + if (!isFinite(firstHiddenTime)) { + // If the event is a 'visibilitychange' event, it means the page was + // visible prior to this change, so the event timestamp is the first + // hidden time. + // However, if the event is not a 'visibilitychange' event, then it must + // be a 'prerenderingchange' event, and the fact that the document is + // still 'hidden' from the above check means the tab was activated + // in a background state and so has always been hidden. + firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0; -const removeChangeListeners = () => { - removeEventListener('visibilitychange', onVisibilityUpdate, true); - removeEventListener('prerenderingchange', onVisibilityUpdate, true); + // We no longer need the `prerenderingchange` event listener now we've + // set an initial init time so remove that + // (we'll keep the visibilitychange one for onHiddenFunction above) + WINDOW.document?.removeEventListener('prerenderingchange', onVisibilityUpdate, true); + } + } }; export const getVisibilityWatcher = () => { @@ -75,14 +73,38 @@ export const getVisibilityWatcher = () => { // a perfect heuristic, but it's the best we can do until the // `visibility-state` performance entry becomes available in all browsers. firstHiddenTime = firstVisibilityStateHiddenTime ?? initHiddenTime(); - // We're still going to listen to for changes so we can handle things like - // bfcache restores and/or prerender without having to examine individual - // timestamps in detail. - addChangeListeners(); + // Listen for visibility changes so we can handle things like bfcache + // restores and/or prerender without having to examine individual + // timestamps in detail and also for onHidden function calls. + WINDOW.document?.addEventListener('visibilitychange', onVisibilityUpdate, true); + + // Some browsers have buggy implementations of visibilitychange, + // so we use pagehide in addition, just to be safe. + WINDOW.document?.addEventListener('pagehide', onVisibilityUpdate, true); + + // IMPORTANT: when a page is prerendering, its `visibilityState` is + // 'hidden', so in order to account for cases where this module checks for + // visibility during prerendering, an additional check after prerendering + // completes is also required. + WINDOW.document?.addEventListener('prerenderingchange', onVisibilityUpdate, true); } + return { get firstHiddenTime() { return firstHiddenTime; }, + onHidden(cb: () => void) { + onHiddenFunctions.add(cb); + }, }; }; + +/** + * Check if the page is hidden, uses the `pagehide` event for older browsers support that we used to have in `onHidden` function. + * Some browsers we still support (Safari <14.4) don't fully support `visibilitychange` + * or have known bugs w.r.t the `visibilitychange` event. + * // TODO (v11): If we decide to drop support for Safari 14.4, we can use the logic from web-vitals 4.2.4 + */ +function isPageHidden(event: Event) { + return event.type === 'pagehide' || WINDOW.document?.visibilityState === 'hidden'; +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts index 9677e25ba802..5f4367f1d456 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts @@ -32,11 +32,12 @@ export const whenIdleOrHidden = (cb: () => void) => { } else { // eslint-disable-next-line no-param-reassign cb = runOnce(cb); + WINDOW.document?.addEventListener('visibilitychange', cb, { once: true, capture: true }); rIC(() => { cb(); // Remove the above event listener since no longer required. // See: https://github.com/GoogleChrome/web-vitals/issues/622 - WINDOW.document?.removeEventListener('visibilitychange', cb); + WINDOW.document?.removeEventListener('visibilitychange', cb, { capture: true }); }); // sentry: we use onHidden instead of directly listening to visibilitychange // because some browsers we still support (Safari <14.4) don't fully support From 9593a19822e79f8229aee03e3d92daf0d6a0b87d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 4 Nov 2025 16:47:22 +0200 Subject: [PATCH 5/6] feat: remove document prefix to match web-vitals 5.1.0 --- .../src/metrics/web-vitals/getLCP.ts | 18 +++++++---------- .../web-vitals/lib/getVisibilityWatcher.ts | 13 ++++++------ .../metrics/web-vitals/lib/globalListeners.ts | 20 +++++++++++++++++++ .../src/metrics/web-vitals/lib/onHidden.ts | 11 +++++----- .../web-vitals/lib/whenIdleOrHidden.ts | 5 +++-- 5 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts diff --git a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts index a203c1e3fc81..9de413c745c0 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; import { getActivationStart } from './lib/getActivationStart'; import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; +import { addPageListener, removePageListener } from './lib/globalListeners'; import { initMetric } from './lib/initMetric'; import { initUnique } from './lib/initUnique'; import { LCPEntryManager } from './lib/LCPEntryManager'; @@ -96,11 +96,9 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = // task to reduce potential INP impact. // https://github.com/GoogleChrome/web-vitals/issues/383 whenIdleOrHidden(stopListening); - if (WINDOW.document) { - removeEventListener(event.type, stopListeningWrapper, { - capture: true, - }); - } + removePageListener(event.type, stopListeningWrapper, { + capture: true, + }); } }; @@ -109,11 +107,9 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = // unreliable since it can be programmatically generated. // See: https://github.com/GoogleChrome/web-vitals/issues/75 for (const type of ['keydown', 'click', 'visibilitychange']) { - if (WINDOW.document) { - addEventListener(type, stopListeningWrapper, { - capture: true, - }); - } + addPageListener(type, stopListeningWrapper, { + capture: true, + }); } } }); diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts index e69e73ec9291..1b79fc744e3c 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts @@ -16,6 +16,7 @@ import { WINDOW } from '../../../types'; import { getActivationStart } from './getActivationStart'; +import { addPageListener, removePageListener } from './globalListeners'; let firstHiddenTime = -1; const onHiddenFunctions: Set<() => void> = new Set(); @@ -53,7 +54,7 @@ const onVisibilityUpdate = (event: Event) => { // We no longer need the `prerenderingchange` event listener now we've // set an initial init time so remove that // (we'll keep the visibilitychange one for onHiddenFunction above) - WINDOW.document?.removeEventListener('prerenderingchange', onVisibilityUpdate, true); + removePageListener('prerenderingchange', onVisibilityUpdate, true); } } }; @@ -76,17 +77,17 @@ export const getVisibilityWatcher = () => { // Listen for visibility changes so we can handle things like bfcache // restores and/or prerender without having to examine individual // timestamps in detail and also for onHidden function calls. - WINDOW.document?.addEventListener('visibilitychange', onVisibilityUpdate, true); + addPageListener('visibilitychange', onVisibilityUpdate, true); - // Some browsers have buggy implementations of visibilitychange, - // so we use pagehide in addition, just to be safe. - WINDOW.document?.addEventListener('pagehide', onVisibilityUpdate, true); + // // Some browsers have buggy implementations of visibilitychange, + // // so we use pagehide in addition, just to be safe. + // addPageListener('pagehide', onVisibilityUpdate, true); // IMPORTANT: when a page is prerendering, its `visibilityState` is // 'hidden', so in order to account for cases where this module checks for // visibility during prerendering, an additional check after prerendering // completes is also required. - WINDOW.document?.addEventListener('prerenderingchange', onVisibilityUpdate, true); + addPageListener('prerenderingchange', onVisibilityUpdate, true); } return { diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts b/packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts new file mode 100644 index 000000000000..0e391cff17c2 --- /dev/null +++ b/packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts @@ -0,0 +1,20 @@ +import { WINDOW } from '../../../types'; + +/** + * web-vitals 5.1.0 switched listeners to be added on the window rather than the document. + * Instead of having to check for window/document every time we add a listener, we can use this function. + */ +export function addPageListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) { + if (WINDOW.document) { + WINDOW.addEventListener(type, listener, options); + } +} +/** + * web-vitals 5.1.0 switched listeners to be removed from the window rather than the document. + * Instead of having to check for window/document every time we remove a listener, we can use this function. + */ +export function removePageListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) { + if (WINDOW.document) { + WINDOW.removeEventListener(type, listener, options); + } +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts index 5a3c1b4fc810..d9dc2f6718ed 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts @@ -15,6 +15,7 @@ */ import { WINDOW } from '../../../types'; +import { addPageListener } from './globalListeners'; export interface OnHiddenCallback { (event: Event): void; @@ -37,10 +38,8 @@ export const onHidden = (cb: OnHiddenCallback) => { } }; - if (WINDOW.document) { - addEventListener('visibilitychange', onHiddenOrPageHide, true); - // Some browsers have buggy implementations of visibilitychange, - // so we use pagehide in addition, just to be safe. - addEventListener('pagehide', onHiddenOrPageHide, true); - } + addPageListener('visibilitychange', onHiddenOrPageHide, true); + // Some browsers have buggy implementations of visibilitychange, + // so we use pagehide in addition, just to be safe. + addPageListener('pagehide', onHiddenOrPageHide, true); }; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts index 5f4367f1d456..008aac8dc4c2 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts @@ -15,6 +15,7 @@ */ import { WINDOW } from '../../../types.js'; +import { addPageListener, removePageListener } from './globalListeners.js'; import { onHidden } from './onHidden.js'; import { runOnce } from './runOnce.js'; @@ -32,12 +33,12 @@ export const whenIdleOrHidden = (cb: () => void) => { } else { // eslint-disable-next-line no-param-reassign cb = runOnce(cb); - WINDOW.document?.addEventListener('visibilitychange', cb, { once: true, capture: true }); + addPageListener('visibilitychange', cb, { once: true, capture: true }); rIC(() => { cb(); // Remove the above event listener since no longer required. // See: https://github.com/GoogleChrome/web-vitals/issues/622 - WINDOW.document?.removeEventListener('visibilitychange', cb, { capture: true }); + removePageListener('visibilitychange', cb, { capture: true }); }); // sentry: we use onHidden instead of directly listening to visibilitychange // because some browsers we still support (Safari <14.4) don't fully support From 29c8b2882f5bcbc4e6304c94544ab362d374c524 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 5 Nov 2025 00:27:42 +0200 Subject: [PATCH 6/6] fix: listen on page hide as i have guessed --- .../web-vitals/lib/getVisibilityWatcher.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts index 1b79fc744e3c..3eaea296a655 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts @@ -33,7 +33,9 @@ const initHiddenTime = () => { const onVisibilityUpdate = (event: Event) => { // Handle changes to hidden state if (isPageHidden(event) && firstHiddenTime > -1) { - if (event.type === 'visibilitychange') { + // Sentry-specific change: Also call onHidden callbacks for pagehide events + // to support older browsers (Safari <14.4) that don't properly fire visibilitychange + if (event.type === 'visibilitychange' || event.type === 'pagehide') { for (const onHiddenFunction of onHiddenFunctions) { onHiddenFunction(); } @@ -46,14 +48,14 @@ const onVisibilityUpdate = (event: Event) => { // visible prior to this change, so the event timestamp is the first // hidden time. // However, if the event is not a 'visibilitychange' event, then it must - // be a 'prerenderingchange' event, and the fact that the document is + // be a 'prerenderingchange' or 'pagehide' event, and the fact that the document is // still 'hidden' from the above check means the tab was activated // in a background state and so has always been hidden. firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0; // We no longer need the `prerenderingchange` event listener now we've // set an initial init time so remove that - // (we'll keep the visibilitychange one for onHiddenFunction above) + // (we'll keep the visibilitychange and pagehide ones for onHiddenFunction above) removePageListener('prerenderingchange', onVisibilityUpdate, true); } } @@ -79,9 +81,10 @@ export const getVisibilityWatcher = () => { // timestamps in detail and also for onHidden function calls. addPageListener('visibilitychange', onVisibilityUpdate, true); - // // Some browsers have buggy implementations of visibilitychange, - // // so we use pagehide in addition, just to be safe. - // addPageListener('pagehide', onVisibilityUpdate, true); + // Sentry-specific change: Some browsers have buggy implementations of visibilitychange, + // so we use pagehide in addition, just to be safe. This is also required for older + // Safari versions (<14.4) that we still support. + addPageListener('pagehide', onVisibilityUpdate, true); // IMPORTANT: when a page is prerendering, its `visibilityState` is // 'hidden', so in order to account for cases where this module checks for