From a786f2c26e6313d4ff10a92988b3fa1087ef4105 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 5 Aug 2026 12:26:07 -0400 Subject: [PATCH 1/3] ref(browser): Replace vendored web-vitals with web-vitals@6 dependency Upstream web-vitals v6 covers the metrics we vendored, so consume it as a dependency instead of maintaining a ~1.9k-line in-tree copy. instrument.ts imports the metric functions from the package and observes performance entries with a local PerformanceObserver; the handful of generic browser helpers still needed (getNavigationEntry, getVisibilityWatcher, whenIdleOrHidden, etc.) move to metrics/web-vitals-helpers, and the rest of the vendored code is deleted. No behavior change: browser-utils unit tests and the browser-integration web-vitals suites (LCP/CLS/INP/FCP/TTFB + streamed spans) all pass. --- .size-limit.js | 12 +- packages/browser-utils/package.json | 3 +- packages/browser-utils/src/index.ts | 2 +- .../src/metrics/browserMetrics.ts | 6 +- .../browser-utils/src/metrics/instrument.ts | 95 ++++++---- .../browser-utils/src/metrics/userTiming.ts | 2 +- packages/browser-utils/src/metrics/utils.ts | 2 +- .../getActivationStart.ts | 0 .../getNavigationEntry.ts | 2 +- .../getVisibilityWatcher.ts | 2 +- .../globalListeners.ts | 2 +- .../lib => web-vitals-helpers}/onHidden.ts | 2 +- .../lib => web-vitals-helpers}/runOnce.ts | 0 .../whenIdleOrHidden.ts | 6 +- .../src/metrics/web-vitals/README.md | 80 -------- .../src/metrics/web-vitals/getCLS.ts | 93 ---------- .../src/metrics/web-vitals/getINP.ts | 127 ------------- .../src/metrics/web-vitals/getLCP.ts | 116 ------------ .../web-vitals/lib/InteractionManager.ts | 151 ---------------- .../metrics/web-vitals/lib/LCPEntryManager.ts | 26 --- .../web-vitals/lib/LayoutShiftManager.ts | 56 ------ .../metrics/web-vitals/lib/bindReporter.ts | 55 ------ .../web-vitals/lib/generateUniqueID.ts | 24 --- .../src/metrics/web-vitals/lib/initMetric.ts | 49 ----- .../src/metrics/web-vitals/lib/initUnique.ts | 37 ---- .../src/metrics/web-vitals/lib/observe.ts | 67 ------- .../lib/polyfills/interactionCountPolyfill.ts | 61 ------- .../metrics/web-vitals/lib/whenActivated.ts | 25 --- .../src/metrics/web-vitals/onFCP.ts | 65 ------- .../src/metrics/web-vitals/onTTFB.ts | 76 -------- .../src/metrics/web-vitals/types.ts | 132 -------------- .../src/metrics/web-vitals/types/base.ts | 137 -------------- .../src/metrics/web-vitals/types/cls.ts | 76 -------- .../src/metrics/web-vitals/types/fcp.ts | 65 ------- .../src/metrics/web-vitals/types/inp.ts | 171 ------------------ .../src/metrics/web-vitals/types/lcp.ts | 92 ---------- .../src/metrics/web-vitals/types/ttfb.ts | 78 -------- .../src/tracing/browserTracingIntegration.ts | 4 + yarn.lock | 5 + 39 files changed, 93 insertions(+), 1911 deletions(-) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/getActivationStart.ts (100%) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/getNavigationEntry.ts (97%) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/getVisibilityWatcher.ts (99%) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/globalListeners.ts (95%) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/onHidden.ts (97%) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/runOnce.ts (100%) rename packages/browser-utils/src/metrics/{web-vitals/lib => web-vitals-helpers}/whenIdleOrHidden.ts (94%) delete mode 100644 packages/browser-utils/src/metrics/web-vitals/README.md delete mode 100644 packages/browser-utils/src/metrics/web-vitals/getCLS.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/getINP.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/getLCP.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/observe.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/polyfills/interactionCountPolyfill.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/onFCP.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/onTTFB.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types/base.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types/cls.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types/fcp.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types/inp.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types/lcp.ts delete mode 100644 packages/browser-utils/src/metrics/web-vitals/types/ttfb.ts diff --git a/.size-limit.js b/.size-limit.js index b775eaadb04f..bd1aa8ce9dab 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -278,7 +278,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '148 KB', + limit: '152 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -294,7 +294,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '152 KB', + limit: '156 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -310,7 +310,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.min.js'), gzip: false, brotli: false, - limit: '267 KB', + limit: '272 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -318,7 +318,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '271 KB', + limit: '276 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -326,7 +326,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.min.js'), gzip: false, brotli: false, - limit: '281 KB', + limit: '286 KB', disablePlugins: ['@size-limit/esbuild'], }, { @@ -334,7 +334,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.replay.feedback.logs.metrics.min.js'), gzip: false, brotli: false, - limit: '285 KB', + limit: '290 KB', disablePlugins: ['@size-limit/esbuild'], }, // Next.js SDK (ESM) diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 453d108fd9d2..5893215b31aa 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -34,7 +34,8 @@ }, "dependencies": { "@sentry/core": "10.67.0", - "@sentry/conventions": "^0.16.0" + "@sentry/conventions": "^0.16.0", + "web-vitals": "^6.0.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index 7722fa9962cf..0dcff8bc2940 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -26,7 +26,7 @@ export { extractNetworkProtocol } from './metrics/utils'; export { trackClsAsSpan, trackInpAsSpan, trackLcpAsSpan } from './metrics/webVitalSpans'; -export { whenIdleOrHidden } from './metrics/web-vitals/lib/whenIdleOrHidden'; +export { whenIdleOrHidden } from './metrics/web-vitals-helpers/whenIdleOrHidden'; export { addClickKeypressInstrumentationHandler } from './instrument/dom'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 5427d568e07e..7b722c296547 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -22,9 +22,9 @@ import { import { isValidLcpMetric } from './lcp'; import { resourceTimingToSpanAttributes } from './resourceTiming'; import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; -import { getActivationStart } from './web-vitals/lib/getActivationStart'; -import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; -import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; +import { getActivationStart } from './web-vitals-helpers/getActivationStart'; +import { getNavigationEntry } from './web-vitals-helpers/getNavigationEntry'; +import { getVisibilityWatcher } from './web-vitals-helpers/getVisibilityWatcher'; import { DEBUG_BUILD } from '../debug-build'; import { SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes'; import { BROWSER_BROWSER_PAINT_SPAN_OP } from '@sentry/conventions/op'; diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index 608a5fd11511..e9bd3b5b9aaa 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -1,10 +1,6 @@ import { debug, getFunctionName } from '@sentry/core'; +import { onCLS, onINP, onLCP, onTTFB } from 'web-vitals'; import { DEBUG_BUILD } from '../debug-build'; -import { onCLS } from './web-vitals/getCLS'; -import { onINP } from './web-vitals/getINP'; -import { onLCP } from './web-vitals/getLCP'; -import { observe } from './web-vitals/lib/observe'; -import { onTTFB } from './web-vitals/onTTFB'; type InstrumentHandlerTypePerformanceObserver = | 'longtask' @@ -47,6 +43,10 @@ export interface PerformanceLongAnimationFrameTiming extends PerformanceEntry { scripts: PerformanceScriptTiming[]; } +// Locally-defined to match web-vitals' `Metric` shape without importing it: web-vitals' type +// entrypoint carries a `declare global` block that references DOM globals not present in every +// TypeScript lib version (e.g. `NavigationType`), which leaks into and breaks consumers on older +// TS. Keeping this local keeps web-vitals' global augmentations out of our published types. interface Metric { /** * The name of the metric (in acronym form). @@ -89,13 +89,20 @@ interface Metric { entries: PerformanceEntry[]; /** - * The type of navigation + * The type of navigation. * * Navigation Timing API (or `undefined` if the browser doesn't * support that API). For pages that are restored from the bfcache, this * value will be 'back-forward-cache'. */ - navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore'; + navigationType: + | 'navigate' + | 'reload' + | 'back-forward' + | 'back-forward-cache' + | 'prerender' + | 'restore' + | 'soft-navigation'; } type InstrumentHandlerType = InstrumentHandlerTypeMetric | InstrumentHandlerTypePerformanceObserver; @@ -213,14 +220,31 @@ function triggerHandlers(type: InstrumentHandlerType, data: unknown): void { } } +/** + * Wraps a metric callback so that metrics reported after a back/forward-cache restore are ignored. + * + * web-vitals re-reports each metric after a bfcache restore (tagged with a `back-forward-cache` + * navigation type). We intentionally drop those for now: our reporting assumes one set of vitals + * per page load, so surfacing bfcache re-reports would skew the data until we're ready to model + * and communicate them. + */ +function withoutBfcache(callback: (metric: Metric) => void): (metric: Metric) => void { + return metric => { + if (metric.navigationType === 'back-forward-cache') { + return; + } + callback(metric); + }; +} + function instrumentCls(): StopListening { return onCLS( - metric => { + withoutBfcache(metric => { triggerHandlers('cls', { metric, }); _previousCls = metric; - }, + }), // We want the callback to be called whenever the CLS value updates. // By default, the callback is only called when the tab goes to the background. { reportAllChanges: true }, @@ -229,12 +253,12 @@ function instrumentCls(): StopListening { function instrumentLcp(): StopListening { return onLCP( - metric => { + withoutBfcache(metric => { triggerHandlers('lcp', { metric, }); _previousLcp = metric; - }, + }), // We want the callback to be called whenever the LCP value updates. // By default, the callback is only called when the tab goes to the background. { reportAllChanges: true }, @@ -242,21 +266,25 @@ function instrumentLcp(): StopListening { } function instrumentTtfb(): StopListening { - return onTTFB(metric => { - triggerHandlers('ttfb', { - metric, - }); - _previousTtfb = metric; - }); + return onTTFB( + withoutBfcache(metric => { + triggerHandlers('ttfb', { + metric, + }); + _previousTtfb = metric; + }), + ); } function instrumentInp(): void { - return onINP(metric => { - triggerHandlers('inp', { - metric, - }); - _previousInp = metric; - }); + return onINP( + withoutBfcache(metric => { + triggerHandlers('inp', { + metric, + }); + _previousInp = metric; + }), + ); } function addMetricObserver( @@ -283,20 +311,23 @@ function addMetricObserver( } function instrumentPerformanceObserver(type: InstrumentHandlerTypePerformanceObserver): void { - const options: PerformanceObserverInit = {}; + const options: PerformanceObserverInit = { type, buffered: true }; // Special per-type options we want to use if (type === 'event') { - options.durationThreshold = 0; + (options as PerformanceObserverInit & { durationThreshold?: number }).durationThreshold = 0; } - observe( - type, - entries => { - triggerHandlers(type, { entries }); - }, - options, - ); + try { + if (PerformanceObserver.supportedEntryTypes.includes(type)) { + const po = new PerformanceObserver(list => { + triggerHandlers(type, { entries: list.getEntries() }); + }); + po.observe(options); + } + } catch { + // Unsupported entry type; nothing to observe. + } } function addHandler(type: InstrumentHandlerType, handler: InstrumentHandlerCallback): void { diff --git a/packages/browser-utils/src/metrics/userTiming.ts b/packages/browser-utils/src/metrics/userTiming.ts index 822a47c13991..153111381d8f 100644 --- a/packages/browser-utils/src/metrics/userTiming.ts +++ b/packages/browser-utils/src/metrics/userTiming.ts @@ -8,7 +8,7 @@ import { stringMatchesSomePattern, } from '@sentry/core'; import { getBrowserPerformanceAPI, msToSec, startAndEndSpan } from './utils'; -import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; +import { getNavigationEntry } from './web-vitals-helpers/getNavigationEntry'; interface UserTimingOptions { /** diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index df7a3985367d..e674a2ec798e 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -1,7 +1,7 @@ import type { Client, SentrySpan, Span, SpanTimeInput, StartSpanOptions } from '@sentry/core'; import { spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core'; import { WINDOW } from '../types'; -import { onHidden } from './web-vitals/lib/onHidden'; +import { onHidden } from './web-vitals-helpers/onHidden'; export type WebVitalReportEvent = 'pagehide' | 'navigation'; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/getActivationStart.ts similarity index 100% rename from packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/getActivationStart.ts diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/getNavigationEntry.ts similarity index 97% rename from packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/getNavigationEntry.ts index 77c68999b918..a49cbe55ed0b 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts +++ b/packages/browser-utils/src/metrics/web-vitals-helpers/getNavigationEntry.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../../types'; +import { WINDOW } from '../../types'; // sentry-specific change: // add optional param to not check for responseStart (see comment below) diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/getVisibilityWatcher.ts similarity index 99% rename from packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/getVisibilityWatcher.ts index 0ed58349fccf..f94a3c7314d1 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/browser-utils/src/metrics/web-vitals-helpers/getVisibilityWatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../../types'; +import { WINDOW } from '../../types'; import { getActivationStart } from './getActivationStart'; import { addPageListener, removePageListener } from './globalListeners'; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/globalListeners.ts similarity index 95% rename from packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/globalListeners.ts index 0e391cff17c2..8f324341a229 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/globalListeners.ts +++ b/packages/browser-utils/src/metrics/web-vitals-helpers/globalListeners.ts @@ -1,4 +1,4 @@ -import { WINDOW } from '../../../types'; +import { WINDOW } from '../../types'; /** * web-vitals 5.1.0 switched listeners to be added on the window rather than the document. diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/onHidden.ts similarity index 97% rename from packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/onHidden.ts index bbe5c758efa1..cc1a1ae3d3ee 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals-helpers/onHidden.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { WINDOW } from '../../../types'; +import { WINDOW } from '../../types'; import { addPageListener } from './globalListeners'; export interface OnHiddenCallback { diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/runOnce.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/runOnce.ts similarity index 100% rename from packages/browser-utils/src/metrics/web-vitals/lib/runOnce.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/runOnce.ts diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts b/packages/browser-utils/src/metrics/web-vitals-helpers/whenIdleOrHidden.ts similarity index 94% rename from packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts rename to packages/browser-utils/src/metrics/web-vitals-helpers/whenIdleOrHidden.ts index f145e293e72c..c6af66611441 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals-helpers/whenIdleOrHidden.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { WINDOW } from '../../../types.js'; -import { addPageListener, removePageListener } from './globalListeners.js'; -import { runOnce } from './runOnce.js'; +import { WINDOW } from '../../types'; +import { addPageListener, removePageListener } from './globalListeners'; +import { runOnce } from './runOnce'; /** * Runs the passed callback during the next idle period, or immediately diff --git a/packages/browser-utils/src/metrics/web-vitals/README.md b/packages/browser-utils/src/metrics/web-vitals/README.md deleted file mode 100644 index 1eeaf4df2420..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# web-vitals - -> 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.1.0 - -The commit SHA used is: -[e22d23b22c1440e69c5fc25a2f373b1a425cc940](https://github.com/GoogleChrome/web-vitals/tree/e22d23b22c1440e69c5fc25a2f373b1a425cc940) - -Current vendored web vitals are: - -- LCP (Largest Contentful Paint) -- FID (First Input Delay) -- CLS (Cumulative Layout Shift) -- INP (Interaction to Next Paint) -- TTFB (Time to First Byte) - -## Notable Changes from web-vitals library - -This vendored web-vitals library is meant to be used in conjunction with the `@sentry/browser` -`browserTracingIntegration`. As such, logic around `BFCache` and multiple reports were removed from the library as our -web-vitals only report once per pageload. - -## License - -[Apache 2.0](https://github.com/GoogleChrome/web-vitals/blob/master/LICENSE) - -## CHANGELOG - -- Bumped from Web Vitals 5.0.2 to 5.1.0 - - Remove `visibilitychange` event listeners when no longer required [#627](https://github.com/GoogleChrome/web-vitals/pull/627) - - Register visibility-change early [#637](https://github.com/GoogleChrome/web-vitals/pull/637) - - Only finalize LCP on user events (isTrusted=true) [#635](https://github.com/GoogleChrome/web-vitals/pull/635) - - Fallback to default getSelector if custom function is null or undefined [#634](https://github.com/GoogleChrome/web-vitals/pull/634) - -https://github.com/getsentry/sentry-javascript/pull/17076 - -- Removed FID-related code with v10 of the SDK - -https://github.com/getsentry/sentry-javascript/pull/16492 - -- Bumped from Web Vitals 4.2.5 to 5.0.2 - - Mainly fixes some INP, LCP and FCP edge cases - - Original library removed FID; we still keep it around for now - -https://github.com/getsentry/sentry-javascript/pull/14439 - -- Bumped from Web Vitals v3.5.2 to v4.2.4 - -https://github.com/getsentry/sentry-javascript/pull/11391 - -- Bumped from Web Vitals v3.0.4 to v3.5.2 - -https://github.com/getsentry/sentry-javascript/pull/5987 - -- Bumped from Web Vitals v2.1.0 to v3.0.4 - -https://github.com/getsentry/sentry-javascript/pull/3781 - -- Bumped from Web Vitals v0.2.4 to v2.1.0 - -https://github.com/getsentry/sentry-javascript/pull/3515 - -- Remove support for Time to First Byte (TTFB) - -https://github.com/getsentry/sentry-javascript/pull/2964 - -- Added support for Cumulative Layout Shift (CLS) and Time to First Byte (TTFB) - -https://github.com/getsentry/sentry-javascript/pull/2909 - -- Added support for FID (First Input Delay) and LCP (Largest Contentful Paint) - -https://github.com/getsentry/sentry-javascript/pull/9690 - -- Added support for INP (Interaction to Next Paint) - -TODO - -- Add support for TTFB (Time to First Byte) diff --git a/packages/browser-utils/src/metrics/web-vitals/getCLS.ts b/packages/browser-utils/src/metrics/web-vitals/getCLS.ts deleted file mode 100644 index 2e3f98c599e4..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/getCLS.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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'; -import { observe } from './lib/observe'; -import { runOnce } from './lib/runOnce'; -import { onFCP } from './onFCP'; -import type { CLSMetric, MetricRatingThresholds, ReportOpts } from './types'; - -/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */ -export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; - -/** - * Calculates the [CLS](https://web.dev/articles/cls) value for the current page and - * calls the `callback` function once the value is ready to be reported, along - * with all `layout-shift` performance entries that were used in the metric - * value calculation. The reported value is a `double` (corresponding to a - * [layout shift score](https://web.dev/articles/cls#layout_shift_score)). - * - * If the `reportAllChanges` configuration option is set to `true`, the - * `callback` function will be called as soon as the value is initially - * determined as well as any time the value changes throughout the page - * lifespan. - * - * _**Important:** CLS should be continually monitored for changes throughout - * the entire lifespan of a page—including if the user returns to the page after - * it's been hidden/backgrounded. However, since browsers often [will not fire - * additional callbacks once the user has backgrounded a - * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden), - * `callback` is always called when the page's visibility state changes to - * hidden. As a result, the `callback` function might be called multiple times - * during the same page load._ - */ -export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts = {}) => { - // Start monitoring FCP so we can only report CLS if FCP is also reported. - // Note: this is done to match the current behavior of CrUX. - onFCP( - runOnce(() => { - const metric = initMetric('CLS', 0); - let report: ReturnType; - const visibilityWatcher = getVisibilityWatcher(); - - const layoutShiftManager = initUnique(opts, LayoutShiftManager); - - const handleEntries = (entries: LayoutShift[]) => { - for (const entry of entries) { - layoutShiftManager._processEntry(entry); - } - - // If the current session value is larger than the current CLS value, - // update CLS and the entries contributing to it. - if (layoutShiftManager._sessionValue > metric.value) { - metric.value = layoutShiftManager._sessionValue; - metric.entries = layoutShiftManager._sessionEntries; - report(); - } - }; - - const po = observe('layout-shift', handleEntries); - if (po) { - report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges); - - visibilityWatcher.onHidden(() => { - handleEntries(po.takeRecords() as CLSMetric['entries']); - report(true); - }); - - // Queue a task to report (if nothing else triggers a report first). - // This allows CLS to be reported as soon as FCP fires when - // `reportAllChanges` is true. - WINDOW?.setTimeout?.(report); - } - }), - ); -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/getINP.ts b/packages/browser-utils/src/metrics/web-vitals/getINP.ts deleted file mode 100644 index df8ac5e1c804..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/getINP.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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 { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill'; -import { whenActivated } from './lib/whenActivated'; -import { whenIdleOrHidden } from './lib/whenIdleOrHidden'; -import type { INPMetric, INPReportOpts, MetricRatingThresholds } from './types'; - -/** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */ -export const INPThresholds: MetricRatingThresholds = [200, 500]; - -// The default `durationThreshold` used across this library for observing -// `event` entries via PerformanceObserver. -const DEFAULT_DURATION_THRESHOLD = 40; - -/** - * Calculates the [INP](https://web.dev/articles/inp) value for the current - * page and calls the `callback` function once the value is ready, along with - * the `event` performance entries reported for that interaction. The reported - * value is a `DOMHighResTimeStamp`. - * - * A custom `durationThreshold` configuration option can optionally be passed - * to control what `event-timing` entries are considered for INP reporting. The - * default threshold is `40`, which means INP scores of less than 40 will not - * be reported. To avoid reporting no interactions in these cases, the library - * will fall back to the input delay of the first interaction. Note that this - * will not affect your 75th percentile INP value unless that value is also - * less than 40 (well below the recommended - * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold). - * - * If the `reportAllChanges` configuration option is set to `true`, the - * `callback` function will be called as soon as the value is initially - * determined as well as any time the value changes throughout the page - * lifespan. - * - * _**Important:** INP should be continually monitored for changes throughout - * the entire lifespan of a page—including if the user returns to the page after - * it's been hidden/backgrounded. However, since browsers often [will not fire - * additional callbacks once the user has backgrounded a - * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden), - * `callback` is always called when the page's visibility state changes to - * hidden. As a result, the `callback` function might be called multiple times - * during the same page load._ - */ -export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts = {}) => { - // Return if the browser doesn't support all APIs needed to measure INP. - if (!(globalThis.PerformanceEventTiming && 'interactionId' in PerformanceEventTiming.prototype)) { - return; - } - - const visibilityWatcher = getVisibilityWatcher(); - - whenActivated(() => { - // TODO(philipwalton): remove once the polyfill is no longer needed. - initInteractionCountPolyfill(); - - const metric = initMetric('INP'); - // eslint-disable-next-line prefer-const - let report: ReturnType; - - const interactionManager = initUnique(opts, InteractionManager); - - const handleEntries = (entries: INPMetric['entries']) => { - // Queue the `handleEntries()` callback in the next idle task. - // This is needed to increase the chances that all event entries that - // occurred between the user interaction and the next paint - // have been dispatched. Note: there is currently an experiment - // running in Chrome (EventTimingKeypressAndCompositionInteractionId) - // 123+ that if rolled out fully may make this no longer necessary. - whenIdleOrHidden(() => { - for (const entry of entries) { - interactionManager._processEntry(entry); - } - - const inp = interactionManager._estimateP98LongestInteraction(); - - if (inp && inp._latency !== metric.value) { - metric.value = inp._latency; - metric.entries = inp.entries; - report(); - } - }); - }; - - const po = observe('event', handleEntries, { - // Event Timing entries have their durations rounded to the nearest 8ms, - // so a duration of 40ms would be any event that spans 2.5 or more frames - // at 60Hz. This threshold is chosen to strike a balance between usefulness - // and performance. Running this callback for any interaction that spans - // just one or two frames is likely not worth the insight that could be - // gained. - durationThreshold: opts.durationThreshold ?? DEFAULT_DURATION_THRESHOLD, - }); - - report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges); - - if (po) { - // Also observe entries of type `first-input`. This is useful in cases - // where the first interaction is less than the `durationThreshold`. - po.observe({ type: 'first-input', buffered: true }); - - visibilityWatcher.onHidden(() => { - handleEntries(po.takeRecords() as INPMetric['entries']); - report(true); - }); - } - }); -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts deleted file mode 100644 index 9de413c745c0..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -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'; -import { observe } from './lib/observe'; -import { runOnce } from './lib/runOnce'; -import { whenActivated } from './lib/whenActivated'; -import { whenIdleOrHidden } from './lib/whenIdleOrHidden'; -import type { LCPMetric, MetricRatingThresholds, ReportOpts } from './types'; - -/** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */ -export const LCPThresholds: MetricRatingThresholds = [2500, 4000]; - -/** - * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and - * calls the `callback` function once the value is ready (along with the - * relevant `largest-contentful-paint` performance entry used to determine the - * value). The reported value is a `DOMHighResTimeStamp`. - * - * If the `reportAllChanges` configuration option is set to `true`, the - * `callback` function will be called any time a new `largest-contentful-paint` - * performance entry is dispatched, or once the final value of the metric has - * been determined. - */ -export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = {}) => { - whenActivated(() => { - const visibilityWatcher = getVisibilityWatcher(); - const metric = initMetric('LCP'); - let report: ReturnType; - - const lcpEntryManager = initUnique(opts, LCPEntryManager); - - const handleEntries = (entries: LCPMetric['entries']) => { - // If reportAllChanges is set then call this function for each entry, - // otherwise only consider the last one. - if (!opts.reportAllChanges) { - // eslint-disable-next-line no-param-reassign - entries = entries.slice(-1); - } - - for (const entry of entries) { - lcpEntryManager._processEntry(entry); - - // Only report if the page wasn't hidden prior to LCP. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. But in cases - // where `activationStart` occurs after the LCP, this time should be - // clamped at 0. - metric.value = Math.max(entry.startTime - getActivationStart(), 0); - metric.entries = [entry]; - report(); - } - } - }; - - const po = observe('largest-contentful-paint', handleEntries); - - if (po) { - report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges); - - // Ensure this logic only runs once, since it can be triggered from - // any of three different event listeners below. - const stopListening = runOnce(() => { - handleEntries(po.takeRecords() as LCPMetric['entries']); - po.disconnect(); - 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']) { - addPageListener(type, stopListeningWrapper, { - capture: true, - }); - } - } - }); -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts deleted file mode 100644 index 726699bc2010..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { getInteractionCount } from './polyfills/interactionCountPolyfill.js'; - -export interface Interaction { - _latency: number; - // While the `id` and `entries` properties are also internal and could be - // mangled by prefixing with an underscore, since they correspond to public - // symbols there is no need to mangle them as the library will compress - // better if we reuse the existing names. - id: number; - entries: PerformanceEventTiming[]; -} - -// To prevent unnecessary memory usage on pages with lots of interactions, -// store at most 10 of the longest interactions to consider as INP candidates. -const MAX_INTERACTIONS_TO_CONSIDER = 10; - -// Used to store the interaction count after a bfcache restore, since p98 -// interaction latencies should only consider the current navigation. -let prevInteractionCount = 0; - -/** - * Returns the interaction count since the last bfcache restore (or for the - * full page lifecycle if there were no bfcache restores). - */ -const getInteractionCountForNavigation = () => { - return getInteractionCount() - prevInteractionCount; -}; - -/** - * - */ -export class InteractionManager { - /** - * A list of longest interactions on the page (by latency) sorted so the - * longest one is first. The list is at most MAX_INTERACTIONS_TO_CONSIDER - * long. - */ - // oxlint-disable-next-line sdk/no-class-field-initializers - _longestInteractionList: Interaction[] = []; - - /** - * A mapping of longest interactions by their interaction ID. - * This is used for faster lookup. - */ - // oxlint-disable-next-line sdk/no-class-field-initializers - _longestInteractionMap: Map = new Map(); - - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _onBeforeProcessingEntry?: (entry: PerformanceEventTiming) => void; - - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _onAfterProcessingINPCandidate?: (interaction: Interaction) => void; - - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, jsdoc/require-jsdoc - _resetInteractions() { - prevInteractionCount = getInteractionCount(); - this._longestInteractionList.length = 0; - this._longestInteractionMap.clear(); - } - - /** - * Returns the estimated p98 longest interaction based on the stored - * interaction candidates and the interaction count for the current page. - */ - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _estimateP98LongestInteraction() { - const candidateInteractionIndex = Math.min( - this._longestInteractionList.length - 1, - Math.floor(getInteractionCountForNavigation() / 50), - ); - - return this._longestInteractionList[candidateInteractionIndex]; - } - - /** - * Takes a performance entry and adds it to the list of worst interactions - * if its duration is long enough to make it among the worst. If the - * entry is part of an existing interaction, it is merged and the latency - * and entries list is updated as needed. - */ - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _processEntry(entry: PerformanceEventTiming) { - this._onBeforeProcessingEntry?.(entry); - - // Skip further processing for entries that cannot be INP candidates. - if (!(entry.interactionId || entry.entryType === 'first-input')) return; - - // The least-long of the 10 longest interactions. - const minLongestInteraction = this._longestInteractionList.at(-1); - - let interaction = this._longestInteractionMap.get(entry.interactionId); - - // Only process the entry if it's possibly one of the ten longest, - // or if it's part of an existing interaction. - if ( - interaction || - this._longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || - // If the above conditions are false, `minLongestInteraction` will be set. - entry.duration > minLongestInteraction!._latency - ) { - // If the interaction already exists, update it. Otherwise create one. - if (interaction) { - // If the new entry has a longer duration, replace the old entries, - // otherwise add to the array. - if (entry.duration > interaction._latency) { - interaction.entries = [entry]; - interaction._latency = entry.duration; - } else if (entry.duration === interaction._latency && entry.startTime === interaction.entries[0]!.startTime) { - interaction.entries.push(entry); - } - } else { - interaction = { - id: entry.interactionId, - entries: [entry], - _latency: entry.duration, - }; - this._longestInteractionMap.set(interaction.id, interaction); - this._longestInteractionList.push(interaction); - } - - // Sort the entries by latency (descending) and keep only the top ten. - this._longestInteractionList.sort((a, b) => b._latency - a._latency); - if (this._longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) { - const removedInteractions = this._longestInteractionList.splice(MAX_INTERACTIONS_TO_CONSIDER); - - for (const interaction of removedInteractions) { - this._longestInteractionMap.delete(interaction.id); - } - } - - // Call any post-processing on the interaction - this._onAfterProcessingINPCandidate?.(interaction); - } - } -} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts deleted file mode 100644 index 752c6c41469b..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// eslint-disable-next-line jsdoc/require-jsdoc -export class LCPEntryManager { - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _onBeforeProcessingEntry?: (entry: LargestContentfulPaint) => void; - - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, jsdoc/require-jsdoc - _processEntry(entry: LargestContentfulPaint) { - this._onBeforeProcessingEntry?.(entry); - } -} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts deleted file mode 100644 index 47d329162915..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable jsdoc/require-jsdoc */ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class LayoutShiftManager { - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _onAfterProcessingUnexpectedShift?: (entry: LayoutShift) => void; - - // oxlint-disable-next-line sdk/no-class-field-initializers - _sessionValue = 0; - // oxlint-disable-next-line sdk/no-class-field-initializers - _sessionEntries: LayoutShift[] = []; - - // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility - _processEntry(entry: LayoutShift) { - // Only count layout shifts without recent user input. - if (entry.hadRecentInput) return; - - const firstSessionEntry = this._sessionEntries[0]; - // This previously used `this._sessionEntries.at(-1)` but that is ES2022. We support ES2021 and earlier. - const lastSessionEntry = this._sessionEntries[this._sessionEntries.length - 1]; - - // If the entry occurred less than 1 second after the previous entry - // and less than 5 seconds after the first entry in the session, - // include the entry in the current session. Otherwise, start a new - // session. - if ( - this._sessionValue && - firstSessionEntry && - lastSessionEntry && - entry.startTime - lastSessionEntry.startTime < 1000 && - entry.startTime - firstSessionEntry.startTime < 5000 - ) { - this._sessionValue += entry.value; - this._sessionEntries.push(entry); - } else { - this._sessionValue = entry.value; - this._sessionEntries = [entry]; - } - - this._onAfterProcessingUnexpectedShift?.(entry); - } -} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts b/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts deleted file mode 100644 index 2eba91d9effb..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { MetricRatingThresholds, MetricType } from '../types'; - -const getRating = (value: number, thresholds: MetricRatingThresholds): MetricType['rating'] => { - if (value > thresholds[1]) { - return 'poor'; - } - if (value > thresholds[0]) { - return 'needs-improvement'; - } - return 'good'; -}; - -export const bindReporter = ( - callback: (metric: Extract) => void, - metric: Extract, - thresholds: MetricRatingThresholds, - reportAllChanges?: boolean, -) => { - let prevValue: number; - let delta: number; - return (forceReport?: boolean) => { - if (metric.value >= 0) { - if (forceReport || reportAllChanges) { - delta = metric.value - (prevValue ?? 0); - - // Report the metric if there's a non-zero delta or if no previous - // value exists (which can happen in the case of the document becoming - // hidden when the metric value is 0). - // See: https://github.com/GoogleChrome/web-vitals/issues/14 - if (delta || prevValue === undefined) { - prevValue = metric.value; - metric.delta = delta; - metric.rating = getRating(metric.value, thresholds); - callback(metric); - } - } - } - }; -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts b/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts deleted file mode 100644 index 983ebc81ea4a..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Performantly generate a unique, 30-char string by combining a version - * number, the current timestamp with a 13-digit number integer. - * @return {string} - */ -export const generateUniqueID = () => { - return `v5-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts deleted file mode 100644 index 8771a5966c9f..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { WINDOW } from '../../../types'; -import type { MetricType } from '../types'; -import { generateUniqueID } from './generateUniqueID'; -import { getActivationStart } from './getActivationStart'; -import { getNavigationEntry } from './getNavigationEntry'; - -export const initMetric = (name: MetricName, value: number = -1) => { - const navEntry = getNavigationEntry(); - let navigationType: MetricType['navigationType'] = 'navigate'; - - if (navEntry) { - if (WINDOW.document?.prerendering || getActivationStart() > 0) { - navigationType = 'prerender'; - } else if (WINDOW.document?.wasDiscarded) { - navigationType = 'restore'; - } else if (navEntry.type) { - navigationType = navEntry.type.replace(/_/g, '-') as MetricType['navigationType']; - } - } - - // Use `entries` type specific for the metric. - const entries: Extract['entries'] = []; - - return { - name, - value, - rating: 'good' as const, // If needed, will be updated when reported. `const` to keep the type from widening to `string`. - delta: 0, - entries, - id: generateUniqueID(), - navigationType, - }; -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts deleted file mode 100644 index ae955917bd46..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const instanceMap: WeakMap = new WeakMap(); - -/** - * A function that accepts and identity object and a class object and returns - * either a new instance of that class or an existing instance, if the - * identity object was previously used. - */ -export function initUnique(identityObj: object, ClassObj: new () => T): T { - try { - if (!instanceMap.get(identityObj)) { - instanceMap.set(identityObj, new ClassObj()); - } - return instanceMap.get(identityObj)! as T; - } catch { - // --- START Sentry-custom code (try/catch wrapping) --- - // Fix for cases where identityObj is not a valid key for WeakMap (sometimes a problem in Safari) - // Just return a new instance without caching it in instanceMap - return new ClassObj(); - } - // --- END Sentry-custom code --- -} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts deleted file mode 100644 index 6071893dfa8e..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -interface PerformanceEntryMap { - event: PerformanceEventTiming[]; - 'first-input': PerformanceEventTiming[]; - 'layout-shift': LayoutShift[]; - 'largest-contentful-paint': LargestContentfulPaint[]; - 'long-animation-frame': PerformanceLongAnimationFrameTiming[]; - paint: PerformancePaintTiming[]; - navigation: PerformanceNavigationTiming[]; - resource: PerformanceResourceTiming[]; - // Sentry-specific change: - // We add longtask as a supported entry type as we use this in - // our `instrumentPerformanceObserver` function also observes 'longtask' - // entries. - longtask: PerformanceEntry[]; - // Sentry-specific change: - // We add element as a supported entry type for ElementTiming API - element: PerformanceEntry[]; -} - -/** - * Takes a performance entry type and a callback function, and creates a - * `PerformanceObserver` instance that will observe the specified entry type - * with buffering enabled and call the callback _for each entry_. - * - * This function also feature-detects entry support and wraps the logic in a - * try/catch to avoid errors in unsupporting browsers. - */ -export const observe = ( - type: K, - callback: (entries: PerformanceEntryMap[K]) => void, - opts: PerformanceObserverInit = {}, -): PerformanceObserver | undefined => { - try { - if (PerformanceObserver.supportedEntryTypes.includes(type)) { - const po = new PerformanceObserver(list => { - // Delay by a microtask to workaround a bug in Safari where the - // callback is invoked immediately, rather than in a separate task. - // See: https://github.com/GoogleChrome/web-vitals/issues/277 - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.resolve().then(() => { - callback(list.getEntries() as PerformanceEntryMap[K]); - }); - }); - po.observe({ type, buffered: true, ...opts }); - return po; - } - } catch { - // Do nothing. - } - return; -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/polyfills/interactionCountPolyfill.ts b/packages/browser-utils/src/metrics/web-vitals/lib/polyfills/interactionCountPolyfill.ts deleted file mode 100644 index 89d5ac59f722..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/polyfills/interactionCountPolyfill.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { observe } from '../observe'; - -declare global { - interface Performance { - readonly interactionCount: number; - } -} - -let interactionCountEstimate = 0; -let minKnownInteractionId = Infinity; -let maxKnownInteractionId = 0; - -const updateEstimate = (entries: PerformanceEventTiming[]) => { - entries.forEach(e => { - if (e.interactionId) { - minKnownInteractionId = Math.min(minKnownInteractionId, e.interactionId); - maxKnownInteractionId = Math.max(maxKnownInteractionId, e.interactionId); - - interactionCountEstimate = maxKnownInteractionId ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 : 0; - } - }); -}; - -let po: PerformanceObserver | undefined; - -/** - * Returns the `interactionCount` value using the native API (if available) - * or the polyfill estimate in this module. - */ -export const getInteractionCount = (): number => { - return po ? interactionCountEstimate : performance.interactionCount || 0; -}; - -/** - * Feature detects native support or initializes the polyfill if needed. - */ -export const initInteractionCountPolyfill = (): void => { - if ('interactionCount' in performance || po) return; - - po = observe('event', updateEstimate, { - type: 'event', - buffered: true, - durationThreshold: 0, - }); -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts deleted file mode 100644 index e5e1ecd45385..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenActivated.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { WINDOW } from '../../../types'; - -export const whenActivated = (callback: () => void) => { - if (WINDOW.document?.prerendering) { - addEventListener('prerenderingchange', () => callback(), true); - } else { - callback(); - } -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/onFCP.ts b/packages/browser-utils/src/metrics/web-vitals/onFCP.ts deleted file mode 100644 index 12fd51e29ef7..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/onFCP.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { bindReporter } from './lib/bindReporter'; -import { getActivationStart } from './lib/getActivationStart'; -import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; -import { initMetric } from './lib/initMetric'; -import { observe } from './lib/observe'; -import { whenActivated } from './lib/whenActivated'; -import type { FCPMetric, MetricRatingThresholds, ReportOpts } from './types'; - -/** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */ -export const FCPThresholds: MetricRatingThresholds = [1800, 3000]; - -/** - * Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and - * calls the `callback` function once the value is ready, along with the - * relevant `paint` performance entry used to determine the value. The reported - * value is a `DOMHighResTimeStamp`. - */ -export const onFCP = (onReport: (metric: FCPMetric) => void, opts: ReportOpts = {}) => { - whenActivated(() => { - const visibilityWatcher = getVisibilityWatcher(); - const metric = initMetric('FCP'); - let report: ReturnType; - - const handleEntries = (entries: FCPMetric['entries']) => { - for (const entry of entries) { - if (entry.name === 'first-contentful-paint') { - po!.disconnect(); - - // Only report if the page wasn't hidden prior to the first paint. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { - // The activationStart reference is used because FCP should be - // relative to page activation rather than navigation start if the - // page was prerendered. But in cases where `activationStart` occurs - // after the FCP, this time should be clamped at 0. - metric.value = Math.max(entry.startTime - getActivationStart(), 0); - metric.entries.push(entry); - report(true); - } - } - } - }; - - const po = observe('paint', handleEntries); - - if (po) { - report = bindReporter(onReport, metric, FCPThresholds, opts.reportAllChanges); - } - }); -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts deleted file mode 100644 index 4633b3cd83cb..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { WINDOW } from '../../types'; -import { bindReporter } from './lib/bindReporter'; -import { getActivationStart } from './lib/getActivationStart'; -import { getNavigationEntry } from './lib/getNavigationEntry'; -import { initMetric } from './lib/initMetric'; -import { whenActivated } from './lib/whenActivated'; -import type { MetricRatingThresholds, ReportOpts, TTFBMetric } from './types'; - -/** Thresholds for TTFB. See https://web.dev/articles/ttfb#what_is_a_good_ttfb_score */ -export const TTFBThresholds: MetricRatingThresholds = [800, 1800]; - -/** - * Runs in the next task after the page is done loading and/or prerendering. - * @param callback - */ -const whenReady = (callback: () => void) => { - if (WINDOW.document?.prerendering) { - whenActivated(() => whenReady(callback)); - } else if (WINDOW.document?.readyState !== 'complete') { - addEventListener('load', () => whenReady(callback), true); - } else { - // Queue a task so the callback runs after `loadEventEnd`. - setTimeout(callback); - } -}; - -/** - * Calculates the [TTFB](https://web.dev/articles/ttfb) value for the - * current page and calls the `callback` function once the page has loaded, - * along with the relevant `navigation` performance entry used to determine the - * value. The reported value is a `DOMHighResTimeStamp`. - * - * Note, this function waits until after the page is loaded to call `callback` - * in order to ensure all properties of the `navigation` entry are populated. - * This is useful if you want to report on other metrics exposed by the - * [Navigation Timing API](https://w3c.github.io/navigation-timing/). For - * example, the TTFB metric starts from the page's [time - * origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it - * includes time spent on DNS lookup, connection negotiation, network latency, - * and server processing time. - */ -export const onTTFB = (onReport: (metric: TTFBMetric) => void, opts: ReportOpts = {}) => { - const metric = initMetric('TTFB'); - const report = bindReporter(onReport, metric, TTFBThresholds, opts.reportAllChanges); - - whenReady(() => { - const navigationEntry = getNavigationEntry(); - - if (navigationEntry) { - // The activationStart reference is used because TTFB should be - // relative to page activation rather than navigation start if the - // page was prerendered. But in cases where `activationStart` occurs - // after the first byte is received, this time should be clamped at 0. - metric.value = Math.max(navigationEntry.responseStart - getActivationStart(), 0); - - metric.entries = [navigationEntry]; - report(true); - } - }); -}; diff --git a/packages/browser-utils/src/metrics/web-vitals/types.ts b/packages/browser-utils/src/metrics/web-vitals/types.ts deleted file mode 100644 index 826cc8a5face..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './types/base'; - -export * from './types/cls'; -export * from './types/fcp'; -export * from './types/inp'; -export * from './types/lcp'; -export * from './types/ttfb'; - -// -------------------------------------------------------------------------- -// Everything below is modifications to built-in modules. -// -------------------------------------------------------------------------- - -interface PerformanceEntryMap { - navigation: PerformanceNavigationTiming; - resource: PerformanceResourceTiming; - paint: PerformancePaintTiming; -} - -// Update built-in types to be more accurate. -declare global { - interface Document { - // https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering - prerendering?: boolean; - // https://wicg.github.io/page-lifecycle/#sec-api - wasDiscarded?: boolean; - } - - interface Performance { - getEntriesByType(type: K): PerformanceEntryMap[K][]; - } - - // https://w3c.github.io/event-timing/#sec-modifications-perf-timeline - interface PerformanceObserverInit { - durationThreshold?: number; - } - - // https://wicg.github.io/nav-speculation/prerendering.html#performance-navigation-timing-extension - interface PerformanceNavigationTiming { - activationStart?: number; - } - - // https://wicg.github.io/event-timing/#sec-performance-event-timing - interface PerformanceEventTiming extends PerformanceEntry { - duration: DOMHighResTimeStamp; - readonly interactionId: number; - } - - // https://wicg.github.io/layout-instability/#sec-layout-shift-attribution - interface LayoutShiftAttribution { - node: Node | null; - previousRect: DOMRectReadOnly; - currentRect: DOMRectReadOnly; - } - - // https://wicg.github.io/layout-instability/#sec-layout-shift - interface LayoutShift extends PerformanceEntry { - value: number; - sources: LayoutShiftAttribution[]; - hadRecentInput: boolean; - } - - // https://w3c.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface - interface LargestContentfulPaint extends PerformanceEntry { - readonly renderTime: DOMHighResTimeStamp; - readonly loadTime: DOMHighResTimeStamp; - readonly size: number; - readonly id: string; - readonly url: string; - readonly element: Element | null; - } - - // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming - export type ScriptInvokerType = - | 'classic-script' - | 'module-script' - | 'event-listener' - | 'user-callback' - | 'resolve-promise' - | 'reject-promise'; - - // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming - export type ScriptWindowAttribution = 'self' | 'descendant' | 'ancestor' | 'same-page' | 'other'; - - // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming - interface PerformanceScriptTiming extends PerformanceEntry { - /* Overloading PerformanceEntry */ - readonly startTime: DOMHighResTimeStamp; - readonly duration: DOMHighResTimeStamp; - readonly name: string; - readonly entryType: string; - - readonly invokerType: ScriptInvokerType; - readonly invoker: string; - readonly executionStart: DOMHighResTimeStamp; - readonly sourceURL: string; - readonly sourceFunctionName: string; - readonly sourceCharPosition: number; - readonly pauseDuration: DOMHighResTimeStamp; - readonly forcedStyleAndLayoutDuration: DOMHighResTimeStamp; - readonly window?: Window; - readonly windowAttribution: ScriptWindowAttribution; - } - - // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming - interface PerformanceLongAnimationFrameTiming extends PerformanceEntry { - readonly startTime: DOMHighResTimeStamp; - readonly duration: DOMHighResTimeStamp; - readonly name: string; - readonly entryType: string; - readonly renderStart: DOMHighResTimeStamp; - readonly styleAndLayoutStart: DOMHighResTimeStamp; - readonly blockingDuration: DOMHighResTimeStamp; - readonly firstUIEventTimestamp: DOMHighResTimeStamp; - readonly scripts: PerformanceScriptTiming[]; - } -} diff --git a/packages/browser-utils/src/metrics/web-vitals/types/base.ts b/packages/browser-utils/src/metrics/web-vitals/types/base.ts deleted file mode 100644 index cac7fdac1d11..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types/base.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { CLSMetric, CLSMetricWithAttribution } from './cls'; -import type { FCPMetric, FCPMetricWithAttribution } from './fcp'; -import type { INPMetric, INPMetricWithAttribution } from './inp'; -import type { LCPMetric, LCPMetricWithAttribution } from './lcp'; -import type { TTFBMetric, TTFBMetricWithAttribution } from './ttfb'; - -export interface Metric { - /** - * The name of the metric (in acronym form). - */ - name: 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB'; - - /** - * The current value of the metric. - */ - value: number; - - /** - * The rating as to whether the metric value is within the "good", - * "needs improvement", or "poor" thresholds of the metric. - */ - rating: 'good' | 'needs-improvement' | 'poor'; - - /** - * The delta between the current value and the last-reported value. - * On the first report, `delta` and `value` will always be the same. - */ - delta: number; - - /** - * A unique ID representing this particular metric instance. This ID can - * be used by an analytics tool to dedupe multiple values sent for the same - * metric instance, or to group multiple deltas together and calculate a - * total. It can also be used to differentiate multiple different metric - * instances sent from the same page, which can happen if the page is - * restored from the back/forward cache (in that case new metrics object - * get created). - */ - id: string; - - /** - * Any performance entries relevant to the metric value calculation. - * The array may also be empty if the metric value was not based on any - * entries (e.g. a CLS value of 0 given no layout shifts). - */ - entries: PerformanceEntry[]; - - /** - * The type of navigation. - * - * This will be the value returned by the Navigation Timing API (or - * `undefined` if the browser doesn't support that API), with the following - * exceptions: - * - 'back-forward-cache': for pages that are restored from the bfcache. - * - 'back_forward' is renamed to 'back-forward' for consistency. - * - 'prerender': for pages that were prerendered. - * - 'restore': for pages that were discarded by the browser and then - * restored by the user. - */ - navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore'; -} - -/** The union of supported metric types. */ -export type MetricType = CLSMetric | FCPMetric | INPMetric | LCPMetric | TTFBMetric; - -/** The union of supported metric attribution types. */ -export type MetricWithAttribution = - | CLSMetricWithAttribution - | FCPMetricWithAttribution - | INPMetricWithAttribution - | LCPMetricWithAttribution - | TTFBMetricWithAttribution; - -/** - * The thresholds of metric's "good", "needs improvement", and "poor" ratings. - * - * - Metric values up to and including [0] are rated "good" - * - Metric values up to and including [1] are rated "needs improvement" - * - Metric values above [1] are "poor" - * - * | Metric value | Rating | - * | --------------- | ------------------- | - * | ≦ [0] | "good" | - * | > [0] and ≦ [1] | "needs improvement" | - * | > [1] | "poor" | - */ -export type MetricRatingThresholds = [number, number]; - -/** - * @deprecated Use metric-specific function types instead, such as: - * `(metric: LCPMetric) => void`. If a single callback type is needed for - * multiple metrics, use `(metric: MetricType) => void`. - */ -export interface ReportCallback { - (metric: MetricType): void; -} - -export interface ReportOpts { - reportAllChanges?: boolean; -} - -export interface AttributionReportOpts extends ReportOpts { - generateTarget?: (el: Node | null) => string | undefined; -} - -/** - * The loading state of the document. Note: this value is similar to - * `document.readyState` but it subdivides the "interactive" state into the - * time before and after the DOMContentLoaded event fires. - * - * State descriptions: - * - `loading`: the initial document response has not yet been fully downloaded - * and parsed. This is equivalent to the corresponding `readyState` value. - * - `dom-interactive`: the document has been fully loaded and parsed, but - * scripts may not have yet finished loading and executing. - * - `dom-content-loaded`: the document is fully loaded and parsed, and all - * scripts (except `async` scripts) have loaded and finished executing. - * - `complete`: the document and all of its sub-resources have finished - * loading. This is equivalent to the corresponding `readyState` value. - */ -export type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete'; diff --git a/packages/browser-utils/src/metrics/web-vitals/types/cls.ts b/packages/browser-utils/src/metrics/web-vitals/types/cls.ts deleted file mode 100644 index 6048c616e1f0..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types/cls.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { LoadState, Metric } from './base'; - -/** - * A CLS-specific version of the Metric object. - */ -export interface CLSMetric extends Metric { - name: 'CLS'; - entries: LayoutShift[]; -} - -/** - * An object containing potentially-helpful debugging information that - * can be sent along with the CLS value for the current page visit in order - * to help identify issues happening to real-users in the field. - */ -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, - * falling back to the default if that returns null or undefined. - */ - largestShiftTarget?: string; - /** - * The time when the single largest layout shift contributing to the page's - * CLS score occurred. - */ - largestShiftTime?: DOMHighResTimeStamp; - /** - * The layout shift score of the single largest layout shift contributing to - * the page's CLS score. - */ - largestShiftValue?: number; - /** - * The `LayoutShiftEntry` representing the single largest layout shift - * contributing to the page's CLS score. (Useful when you need more than just - * `largestShiftTarget`, `largestShiftTime`, and `largestShiftValue`). - */ - largestShiftEntry?: LayoutShift; - /** - * The first element source (in document order) among the `sources` list - * of the `largestShiftEntry` object. (Also useful when you need more than - * just `largestShiftTarget`, `largestShiftTime`, and `largestShiftValue`). - */ - largestShiftSource?: LayoutShiftAttribution; - /** - * The loading state of the document at the time when the largest layout - * shift contribution to the page's CLS score occurred (see `LoadState` - * for details). - */ - loadState?: LoadState; -} - -/** - * A CLS-specific version of the Metric object with attribution. - */ -export interface CLSMetricWithAttribution extends CLSMetric { - attribution: CLSAttribution; -} diff --git a/packages/browser-utils/src/metrics/web-vitals/types/fcp.ts b/packages/browser-utils/src/metrics/web-vitals/types/fcp.ts deleted file mode 100644 index ce668192766f..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types/fcp.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { LoadState, Metric } from './base'; - -/** - * An FCP-specific version of the Metric object. - */ -export interface FCPMetric extends Metric { - name: 'FCP'; - entries: PerformancePaintTiming[]; -} - -/** - * An object containing potentially-helpful debugging information that - * can be sent along with the FCP value for the current page visit in order - * to help identify issues happening to real-users in the field. - */ -export interface FCPAttribution { - /** - * The time from when the user initiates loading the page until when the - * browser receives the first byte of the response (a.k.a. TTFB). - */ - timeToFirstByte: number; - /** - * The delta between TTFB and the first contentful paint (FCP). - */ - firstByteToFCP: number; - /** - * The loading state of the document at the time when FCP `occurred (see - * `LoadState` for details). Ideally, documents can paint before they finish - * loading (e.g. the `loading` or `dom-interactive` phases). - */ - loadState: LoadState; - /** - * The `PerformancePaintTiming` entry corresponding to FCP. - */ - fcpEntry?: PerformancePaintTiming; - /** - * The `navigation` entry of the current page, which is useful for diagnosing - * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming - */ - navigationEntry?: PerformanceNavigationTiming; -} - -/** - * An FCP-specific version of the Metric object with attribution. - */ -export interface FCPMetricWithAttribution extends FCPMetric { - attribution: FCPAttribution; -} diff --git a/packages/browser-utils/src/metrics/web-vitals/types/inp.ts b/packages/browser-utils/src/metrics/web-vitals/types/inp.ts deleted file mode 100644 index d2b2063c7d04..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types/inp.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { AttributionReportOpts, LoadState, Metric, ReportOpts } from './base'; - -export interface INPReportOpts extends ReportOpts { - durationThreshold?: number; -} - -export interface INPAttributionReportOpts extends AttributionReportOpts { - durationThreshold?: number; -} - -/** - * An INP-specific version of the Metric object. - */ -export interface INPMetric extends Metric { - name: 'INP'; - entries: PerformanceEventTiming[]; -} - -export interface INPLongestScriptSummary { - /** - * The longest Long Animation Frame script entry that intersects the INP - * interaction. - */ - entry: PerformanceScriptTiming; - /** - * The INP subpart where the longest script ran. - */ - subpart: 'input-delay' | 'processing-duration' | 'presentation-delay'; - /** - * The amount of time the longest script intersected the INP duration. - */ - intersectingDuration: number; -} - -/** - * An object containing potentially-helpful debugging information that - * can be sent along with the INP value for the current page visit in order - * to help identify issues happening to real-users in the field. - */ -export interface INPAttribution { - /** - * By default, a selector identifying the element that the user first - * interacted with as part of the frame where the INP candidate interaction - * 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, falling back to the default if that - * returns null or undefined. - */ - interactionTarget: string; - /** - * The time when the user first interacted during the frame where the INP - * candidate interaction occurred (if more than one interaction occurred - * within the frame, only the first time is reported). - */ - interactionTime: DOMHighResTimeStamp; - /** - * The type of interaction, based on the event type of the `event` entry - * that corresponds to the interaction (i.e. the first `event` entry - * containing an `interactionId` dispatched in a given animation frame). - * For "pointerdown", "pointerup", or "click" events this will be "pointer", - * and for "keydown" or "keyup" events this will be "keyboard". - */ - interactionType: 'pointer' | 'keyboard'; - /** - * The best-guess timestamp of the next paint after the interaction. - * In general, this timestamp is the same as the `startTime + duration` of - * the event timing entry. However, since duration values are rounded to the - * nearest 8ms (and can be rounded down), this value is clamped to always be - * reported after the processing times. - */ - nextPaintTime: DOMHighResTimeStamp; - /** - * An array of Event Timing entries that were processed within the same - * animation frame as the INP candidate interaction. - */ - processedEventEntries: PerformanceEventTiming[]; - /** - * The time from when the user interacted with the page until when the - * browser was first able to start processing event listeners for that - * interaction. This time captures the delay before event processing can - * begin due to the main thread being busy with other work. - */ - inputDelay: number; - /** - * The time from when the first event listener started running in response to - * the user interaction until when all event listener processing has finished. - */ - processingDuration: number; - /** - * The time from when the browser finished processing all event listeners for - * the user interaction until the next frame is presented on the screen and - * visible to the user. This time includes work on the main thread (such as - * `requestAnimationFrame()` callbacks, `ResizeObserver` and - * `IntersectionObserver` callbacks, and style/layout calculation) as well - * as off-main-thread work (such as compositor, GPU, and raster work). - */ - presentationDelay: number; - /** - * The loading state of the document at the time when the interaction - * corresponding to INP occurred (see `LoadState` for details). If the - * interaction occurred while the document was loading and executing script - * (e.g. usually in the `dom-interactive` phase) it can result in long delays. - */ - loadState: LoadState; - /** - * If the browser supports the Long Animation Frame API, this array will - * include any `long-animation-frame` entries that intersect with the INP - * candidate interaction's `startTime` and the `processingEnd` time of the - * last event processed within that animation frame. If the browser does not - * support the Long Animation Frame API or no `long-animation-frame` entries - * are detected, this array will be empty. - */ - longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; - /** - * Summary information about the longest script entry intersecting the INP - * duration. Note, only script entries above 5 milliseconds are reported by - * the Long Animation Frame API. - */ - longestScript?: INPLongestScriptSummary; - /** - * The total duration of Long Animation Frame scripts that intersect the INP - * duration excluding any forced style and layout (that is included in - * totalStyleAndLayout). Note, this is limited to scripts > 5 milliseconds. - */ - totalScriptDuration?: number; - /** - * The total style and layout duration from any Long Animation Frames - * intersecting the INP interaction. This includes any end-of-frame style and - * layout duration + any forced style and layout duration. - */ - totalStyleAndLayoutDuration?: number; - /** - * The off main-thread presentation delay from the end of the last Long - * Animation Frame (where available) until the INP end point. - */ - totalPaintDuration?: number; - /** - * The total unattributed time not included in any of the previous totals. - * This includes scripts < 5 milliseconds and other timings not attributed - * by Long Animation Frame (including when a frame is < 50ms and so has no - * Long Animation Frame). - * When no Long Animation Frames are present this will be undefined, rather - * than everything being unattributed to make it clearer when it's expected - * to be small. - */ - totalUnattributedDuration?: number; -} - -/** - * An INP-specific version of the Metric object with attribution. - */ -export interface INPMetricWithAttribution extends INPMetric { - attribution: INPAttribution; -} diff --git a/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts b/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts deleted file mode 100644 index 9de6b32a5f94..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Metric } from './base.js'; - -/** - * An LCP-specific version of the Metric object. - */ -export interface LCPMetric extends Metric { - name: 'LCP'; - entries: LargestContentfulPaint[]; -} - -/** - * An object containing potentially-helpful debugging information that - * can be sent along with the LCP value for the current page visit in order - * to help identify issues happening to real-users in the field. - */ -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, falling back to the default if that returns null - * or undefined. - */ - target?: string; - /** - * The URL (if applicable) of the LCP image resource. If the LCP element - * is a text node, this value will not be set. - */ - url?: string; - /** - * The time from when the user initiates loading the page until when the - * browser receives the first byte of the response (a.k.a. TTFB). See - * [Optimize LCP](https://web.dev/articles/optimize-lcp) for details. - */ - timeToFirstByte: number; - /** - * The delta between TTFB and when the browser starts loading the LCP - * resource (if there is one, otherwise 0). See [Optimize - * LCP](https://web.dev/articles/optimize-lcp) for details. - */ - resourceLoadDelay: number; - /** - * The total time it takes to load the LCP resource itself (if there is one, - * otherwise 0). See [Optimize LCP](https://web.dev/articles/optimize-lcp) for - * details. - */ - resourceLoadDuration: number; - /** - * The delta between when the LCP resource finishes loading until the LCP - * element is fully rendered. See [Optimize - * LCP](https://web.dev/articles/optimize-lcp) for details. - */ - elementRenderDelay: number; - /** - * The `navigation` entry of the current page, which is useful for diagnosing - * general page load issues. This can be used to access `serverTiming` for example: - * navigationEntry?.serverTiming - */ - navigationEntry?: PerformanceNavigationTiming; - /** - * The `resource` entry for the LCP resource (if applicable), which is useful - * for diagnosing resource load issues. - */ - lcpResourceEntry?: PerformanceResourceTiming; - /** - * The `LargestContentfulPaint` entry corresponding to LCP. - */ - lcpEntry?: LargestContentfulPaint; -} - -/** - * An LCP-specific version of the Metric object with attribution. - */ -export interface LCPMetricWithAttribution extends LCPMetric { - attribution: LCPAttribution; -} diff --git a/packages/browser-utils/src/metrics/web-vitals/types/ttfb.ts b/packages/browser-utils/src/metrics/web-vitals/types/ttfb.ts deleted file mode 100644 index 2a43668d7d8f..000000000000 --- a/packages/browser-utils/src/metrics/web-vitals/types/ttfb.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2022 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Metric } from './base'; - -/** - * A TTFB-specific version of the Metric object. - */ -export interface TTFBMetric extends Metric { - name: 'TTFB'; - entries: PerformanceNavigationTiming[]; -} - -/** - * An object containing potentially-helpful debugging information that - * can be sent along with the TTFB value for the current page visit in order - * to help identify issues happening to real-users in the field. - * - * NOTE: these values are primarily useful for page loads not handled via - * service worker, as browsers differ in what they report when service worker - * is involved, see: https://github.com/w3c/navigation-timing/issues/199 - */ -export interface TTFBAttribution { - /** - * The total time from when the user initiates loading the page to when the - * page starts to handle the request. Large values here are typically due - * to HTTP redirects, though other browser processing contributes to this - * duration as well (so even without redirect it's generally not zero). - */ - waitingDuration: number; - /** - * The total time spent checking the HTTP cache for a match. For navigations - * handled via service worker, this duration usually includes service worker - * start-up time as well as time processing `fetch` event listeners, with - * some exceptions, see: https://github.com/w3c/navigation-timing/issues/199 - */ - cacheDuration: number; - /** - * The total time to resolve the DNS for the requested domain. - */ - dnsDuration: number; - /** - * The total time to create the connection to the requested domain. - */ - connectionDuration: number; - /** - * The total time from when the request was sent until the first byte of the - * response was received. This includes network time as well as server - * processing time. - */ - requestDuration: number; - /** - * The `navigation` entry of the current page, which is useful for diagnosing - * general page load issues. This can be used to access `serverTiming` for - * example: navigationEntry?.serverTiming - */ - navigationEntry?: PerformanceNavigationTiming; -} - -/** - * A TTFB-specific version of the Metric object with attribution. - */ -export interface TTFBMetricWithAttribution extends TTFBMetric { - attribution: TTFBAttribution; -} diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index b0c67a006185..0b24c5627a3f 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -787,6 +787,10 @@ export function getMetaContent(metaName: string): string | undefined { /** Returns the description of a server timing entry */ export function getServerTiming(name: string): string | undefined { + // The cast is required for the declaration build (`build:types`), which resolves + // `getEntriesByType('navigation')` to `PerformanceEntry[]` (no `serverTiming`). It only reads as + // "unnecessary" to the type-aware linter, which runs with web-vitals' global augmentation applied. + // oxlint-disable-next-line typescript/no-unnecessary-type-assertion const navigation = WINDOW.performance?.getEntriesByType?.('navigation')[0] as PerformanceNavigationTiming | undefined; const entry = navigation?.serverTiming?.find(entry => entry.name === name); return entry?.description; diff --git a/yarn.lock b/yarn.lock index 90fce03d6135..15ee0b0f4cfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30055,6 +30055,11 @@ web-streams-polyfill@^3.1.1: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== +web-vitals@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-6.0.1.tgz#774de5fba561154313bcc09d125990f046fdd174" + integrity sha512-iF3+kno3Anlqi5fPVTQk9gYLfsCiL8P69Hof+AmZyVVx00E3e/BiGVvu+EsOy6jvTLqTKuiaRloPyw0JtdKbSw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From f5bc6bc93e9a76ea5d599fa2eb3e24100df7f983 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 6 Aug 2026 10:02:53 -0400 Subject: [PATCH 2/3] fix(browser): Restore Safari microtask defer in performance observer The vendored `observe` helper wrapped PerformanceObserver callbacks in a microtask to work around a Safari bug where the callback fires synchronously during `observe()` instead of in a separate task (https://github.com/GoogleChrome/web-vitals/issues/277). The local `instrumentPerformanceObserver` dropped that defer, so paint/longtask/event/ element handlers could run synchronously at init on affected Safari versions. Upstream web-vitals@6 still ships this defer, so restore it here to match. --- packages/browser-utils/src/metrics/instrument.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index e9bd3b5b9aaa..4e5fee715c72 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -321,7 +321,12 @@ function instrumentPerformanceObserver(type: InstrumentHandlerTypePerformanceObs try { if (PerformanceObserver.supportedEntryTypes.includes(type)) { const po = new PerformanceObserver(list => { - triggerHandlers(type, { entries: list.getEntries() }); + // Delay by a microtask to work around a bug in Safari where the + // callback is invoked synchronously rather than in a separate task. + // See: https://github.com/GoogleChrome/web-vitals/issues/277 + void Promise.resolve().then(() => { + triggerHandlers(type, { entries: list.getEntries() }); + }); }); po.observe(options); } From a7f3f5faaff0f89baa261612de9a0451b64a7dd9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 6 Aug 2026 10:05:15 -0400 Subject: [PATCH 3/3] fix(browser): Correct instrumentInp return type `instrumentInp` returns the `StopListening` cleanup from `onINP`, matching its `instrumentCls`/`instrumentLcp`/`instrumentTtfb` siblings and the `addMetricObserver` `instrumentFn` parameter, but was typed `void`. That silently discarded the cleanup so a future `stopOnCallback` on INP would no-op. --- packages/browser-utils/src/metrics/instrument.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index 4e5fee715c72..463908bca008 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -276,7 +276,7 @@ function instrumentTtfb(): StopListening { ); } -function instrumentInp(): void { +function instrumentInp(): StopListening { return onINP( withoutBfcache(metric => { triggerHandlers('inp', {