Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/browser-utils/src/metrics/web-vitals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
10 changes: 5 additions & 5 deletions packages/browser-utils/src/metrics/web-vitals/getCLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,6 +56,7 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts =
runOnce(() => {
const metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;
const visibilityWatcher = getVisibilityWatcher();

const layoutShiftManager = initUnique(opts, LayoutShiftManager);

Expand All @@ -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).
Expand Down
9 changes: 4 additions & 5 deletions packages/browser-utils/src/metrics/web-vitals/getINP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
});
Expand Down
28 changes: 18 additions & 10 deletions packages/browser-utils/src/metrics/web-vitals/getLCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -88,20 +88,28 @@ 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);
removePageListener(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), {
capture: true,
once: true,
});
}
addPageListener(type, stopListeningWrapper, {
capture: true,
});
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

import { WINDOW } from '../../../types';
import { getActivationStart } from './getActivationStart';
import { addPageListener, removePageListener } from './globalListeners';

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

const initHiddenTime = () => {
// If the document is hidden when this code runs, assume it was always
Expand All @@ -29,35 +31,34 @@ 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) {
// 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();
}
}

// 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' 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;

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 and pagehide ones for onHiddenFunction above)
removePageListener('prerenderingchange', onVisibilityUpdate, true);
}
}
};

export const getVisibilityWatcher = () => {
Expand All @@ -75,14 +76,39 @@ 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.
addPageListener('visibilitychange', 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
// visibility during prerendering, an additional check after prerendering
// completes is also required.
addPageListener('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';
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 5 additions & 6 deletions packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { WINDOW } from '../../../types';
import { addPageListener } from './globalListeners';

export interface OnHiddenCallback {
(event: Event): void;
Expand All @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -32,7 +33,13 @@ export const whenIdleOrHidden = (cb: () => void) => {
} else {
// eslint-disable-next-line no-param-reassign
cb = runOnce(cb);
rIC(cb);
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
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
// `visibilitychange` or have known bugs w.r.t the `visibilitychange` event.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export interface ReportOpts {
}

export interface AttributionReportOpts extends ReportOpts {
generateTarget?: (el: Node | null) => string;
generateTarget?: (el: Node | null) => string | undefined;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/src/metrics/web-vitals/types/cls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/src/metrics/web-vitals/types/inp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/src/metrics/web-vitals/types/lcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
Loading