diff --git a/packages/tracing/src/browser/metrics.ts b/packages/tracing/src/browser/metrics.ts index cace33b1cfa1..94e14272dbb3 100644 --- a/packages/tracing/src/browser/metrics.ts +++ b/packages/tracing/src/browser/metrics.ts @@ -9,7 +9,8 @@ import { msToSec } from '../utils'; import { getCLS, LayoutShift } from './web-vitals/getCLS'; import { getFID } from './web-vitals/getFID'; import { getLCP, LargestContentfulPaint } from './web-vitals/getLCP'; -import { getFirstHidden } from './web-vitals/lib/getFirstHidden'; +import { getUpdatedCLS } from './web-vitals/getUpdatedCLS'; +import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; import { NavigatorDeviceMemory, NavigatorNetworkInformation } from './web-vitals/types'; const global = getGlobalObject(); @@ -21,6 +22,7 @@ export class MetricsInstrumentation { private _performanceCursor: number = 0; private _lcpEntry: LargestContentfulPaint | undefined; private _clsEntry: LayoutShift | undefined; + private _updatedClsEntry: LayoutShift | undefined; public constructor() { if (!isNodeEnv() && global?.performance) { @@ -92,9 +94,9 @@ export class MetricsInstrumentation { // capture web vitals - const firstHidden = getFirstHidden(); + const firstHidden = getVisibilityWatcher(); // Only report if the page wasn't hidden prior to the web vital. - const shouldRecord = entry.startTime < firstHidden.timeStamp; + const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; if (entry.name === 'first-paint' && shouldRecord) { logger.log('[Measurements] Adding FP'); @@ -187,6 +189,12 @@ export class MetricsInstrumentation { }); } + // If FCP is not recorded we should not record the updated cls value + // according to the new definition of CLS. + if (!('fcp' in this._measurements)) { + delete this._measurements['updated-cls']; + } + transaction.setMeasurements(this._measurements); this._tagMetricInfo(transaction); } @@ -217,17 +225,23 @@ export class MetricsInstrumentation { // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift if (this._clsEntry && this._clsEntry.sources) { logger.log('[Measurements] Adding CLS Data'); - this._clsEntry.sources.map((source, index) => + this._clsEntry.sources.forEach((source, index) => transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); } + + if (this._updatedClsEntry && this._updatedClsEntry.sources) { + logger.log('[Measurements] Adding Updated CLS Data'); + this._updatedClsEntry.sources.forEach((source, index) => + transaction.setTag(`updated-cls.source.${index + 1}`, htmlTreeAsString(source.node)), + ); + } } /** Starts tracking the Cumulative Layout Shift on the current page. */ private _trackCLS(): void { getCLS(metric => { const entry = metric.entries.pop(); - if (!entry) { return; } @@ -236,6 +250,20 @@ export class MetricsInstrumentation { this._measurements['cls'] = { value: metric.value }; this._clsEntry = entry as LayoutShift; }); + + // See: + // https://web.dev/evolving-cls/ + // https://web.dev/cls-web-tooling/ + getUpdatedCLS(metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } + + logger.log('[Measurements] Adding Updated CLS'); + this._measurements['updated-cls'] = { value: metric.value }; + this._updatedClsEntry = entry as LayoutShift; + }); } /** @@ -243,13 +271,11 @@ export class MetricsInstrumentation { */ private _trackNavigator(transaction: Transaction): void { const navigator = global.navigator as null | (Navigator & NavigatorNetworkInformation & NavigatorDeviceMemory); - if (!navigator) { return; } // track network connectivity - const connection = navigator.connection; if (connection) { if (connection.effectiveType) { diff --git a/packages/tracing/src/browser/web-vitals/README.md b/packages/tracing/src/browser/web-vitals/README.md index e4bf6340d1d4..9270917e337d 100644 --- a/packages/tracing/src/browser/web-vitals/README.md +++ b/packages/tracing/src/browser/web-vitals/README.md @@ -2,15 +2,35 @@ > 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 +This was vendored from: https://github.com/GoogleChrome/web-vitals: v2.1.0 -The commit SHA used is: [56c736b7c4e80f295bc8a98017671c95231fa225](https://github.com/GoogleChrome/web-vitals/tree/56c736b7c4e80f295bc8a98017671c95231fa225) +The commit SHA used is: [3f3338d994f182172d5b97b22a0fcce0c2846908](https://github.com/GoogleChrome/web-vitals/tree/3f3338d994f182172d5b97b22a0fcce0c2846908) Current vendored web vitals are: - LCP (Largest Contentful Paint) - FID (First Input Delay) +- CLS (Cumulative Layout Shift) -# License +## Notable Changes from web-vitals library + +This vendored web-vitals library is meant to be used in conjunction with the `@sentry/tracing` `BrowserTracing` integration. +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 + +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) diff --git a/packages/tracing/src/browser/web-vitals/getCLS.ts b/packages/tracing/src/browser/web-vitals/getCLS.ts index 71c6f06cec85..5a45fd6eec27 100644 --- a/packages/tracing/src/browser/web-vitals/getCLS.ts +++ b/packages/tracing/src/browser/web-vitals/getCLS.ts @@ -34,31 +34,27 @@ export interface LayoutShiftAttribution { currentRect: DOMRectReadOnly; } -export const getCLS = (onReport: ReportHandler, reportAllChanges = false): void => { +export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => { const metric = initMetric('CLS', 0); - let report: ReturnType; const entryHandler = (entry: LayoutShift): void => { - // Only count layout shifts without recent user input. if (!entry.hadRecentInput) { (metric.value as number) += entry.value; metric.entries.push(entry); - report(); + if (report) { + report(); + } } }; const po = observe('layout-shift', entryHandler as PerformanceEntryHandler); if (po) { - report = bindReporter(onReport, metric, po, reportAllChanges); + report = bindReporter(onReport, metric, reportAllChanges); - onHidden(({ isUnloading }) => { + onHidden(() => { po.takeRecords().map(entryHandler as PerformanceEntryHandler); - - if (isUnloading) { - metric.isFinal = true; - } - report(); + report(true); }); } }; diff --git a/packages/tracing/src/browser/web-vitals/getFID.ts b/packages/tracing/src/browser/web-vitals/getFID.ts index 2093c25b9fa4..c7149f70aadb 100644 --- a/packages/tracing/src/browser/web-vitals/getFID.ts +++ b/packages/tracing/src/browser/web-vitals/getFID.ts @@ -15,75 +15,32 @@ */ import { bindReporter } from './lib/bindReporter'; -import { getFirstHidden } from './lib/getFirstHidden'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; import { observe, PerformanceEntryHandler } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { ReportHandler } from './types'; +import { PerformanceEventTiming, ReportHandler } from './types'; -interface FIDPolyfillCallback { - (value: number, event: Event): void; -} - -interface FIDPolyfill { - onFirstInputDelay: (onReport: FIDPolyfillCallback) => void; -} - -declare global { - interface Window { - perfMetrics: FIDPolyfill; - } -} - -// https://wicg.github.io/event-timing/#sec-performance-event-timing -interface PerformanceEventTiming extends PerformanceEntry { - processingStart: DOMHighResTimeStamp; - cancelable?: boolean; - target?: Element; -} - -export const getFID = (onReport: ReportHandler): void => { +export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => { + const visibilityWatcher = getVisibilityWatcher(); const metric = initMetric('FID'); - const firstHidden = getFirstHidden(); + let report: ReturnType; const entryHandler = (entry: PerformanceEventTiming): void => { // Only report if the page wasn't hidden prior to the first input. - if (entry.startTime < firstHidden.timeStamp) { + if (report && entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = entry.processingStart - entry.startTime; metric.entries.push(entry); - metric.isFinal = true; - report(); + report(true); } }; const po = observe('first-input', entryHandler as PerformanceEntryHandler); - const report = bindReporter(onReport, metric, po); - if (po) { + report = bindReporter(onReport, metric, reportAllChanges); onHidden(() => { po.takeRecords().map(entryHandler as PerformanceEntryHandler); po.disconnect(); }, true); - } else { - if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) { - window.perfMetrics.onFirstInputDelay((value: number, event: Event) => { - // Only report if the page wasn't hidden prior to the first input. - if (event.timeStamp < firstHidden.timeStamp) { - metric.value = value; - metric.isFinal = true; - metric.entries = [ - { - entryType: 'first-input', - name: event.type, - target: event.target, - cancelable: event.cancelable, - startTime: event.timeStamp, - processingStart: event.timeStamp + value, - } as PerformanceEventTiming, - ]; - report(); - } - }); - } } }; diff --git a/packages/tracing/src/browser/web-vitals/getLCP.ts b/packages/tracing/src/browser/web-vitals/getLCP.ts index 174aa1438eeb..8b00dc417746 100644 --- a/packages/tracing/src/browser/web-vitals/getLCP.ts +++ b/packages/tracing/src/browser/web-vitals/getLCP.ts @@ -15,11 +15,10 @@ */ import { bindReporter } from './lib/bindReporter'; -import { getFirstHidden } from './lib/getFirstHidden'; +import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; import { observe, PerformanceEntryHandler } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { whenInput } from './lib/whenInput'; import { ReportHandler } from './types'; // https://wicg.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface @@ -33,10 +32,11 @@ export interface LargestContentfulPaint extends PerformanceEntry { toJSON(): Record; } -export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void => { - const metric = initMetric('LCP'); - const firstHidden = getFirstHidden(); +const reportedMetricIDs: Record = {}; +export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => { + const visibilityWatcher = getVisibilityWatcher(); + const metric = initMetric('LCP'); let report: ReturnType; const entryHandler = (entry: PerformanceEntry): void => { @@ -46,30 +46,37 @@ export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void // If the page was hidden prior to paint time of the entry, // ignore it and mark the metric as final, otherwise add the entry. - if (value < firstHidden.timeStamp) { + if (value < visibilityWatcher.firstHiddenTime) { metric.value = value; metric.entries.push(entry); - } else { - metric.isFinal = true; } - report(); + if (report) { + report(); + } }; const po = observe('largest-contentful-paint', entryHandler); if (po) { - report = bindReporter(onReport, metric, po, reportAllChanges); + report = bindReporter(onReport, metric, reportAllChanges); - const onFinal = (): void => { - if (!metric.isFinal) { + const stopListening = (): void => { + if (!reportedMetricIDs[metric.id]) { po.takeRecords().map(entryHandler as PerformanceEntryHandler); - metric.isFinal = true; - report(); + po.disconnect(); + reportedMetricIDs[metric.id] = true; + report(true); } }; - void whenInput().then(onFinal); - onHidden(onFinal, true); + // Stop listening after input. Note: while scrolling is an input that + // stop LCP observation, it's unreliable since it can be programmatically + // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 + ['keydown', 'click'].forEach(type => { + addEventListener(type, stopListening, { once: true, capture: true }); + }); + + onHidden(stopListening, true); } }; diff --git a/packages/tracing/src/browser/web-vitals/getUpdatedCLS.ts b/packages/tracing/src/browser/web-vitals/getUpdatedCLS.ts new file mode 100644 index 000000000000..061c0b0a5c76 --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/getUpdatedCLS.ts @@ -0,0 +1,86 @@ +/* + * 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 { initMetric } from './lib/initMetric'; +import { observe, PerformanceEntryHandler } from './lib/observe'; +import { onHidden } from './lib/onHidden'; +import { ReportHandler } from './types'; + +// https://wicg.github.io/layout-instability/#sec-layout-shift +export interface LayoutShift extends PerformanceEntry { + value: number; + hadRecentInput: boolean; + sources: Array; + toJSON(): Record; +} + +export interface LayoutShiftAttribution { + node?: Node; + previousRect: DOMRectReadOnly; + currentRect: DOMRectReadOnly; +} + +export const getUpdatedCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => { + const metric = initMetric('UpdatedCLS', 0); + let report: ReturnType; + + let sessionValue = 0; + let sessionEntries: PerformanceEntry[] = []; + + const entryHandler = (entry: LayoutShift): void => { + // Only count layout shifts without recent user input. + if (!entry.hadRecentInput) { + const firstSessionEntry = sessionEntries[0]; + const lastSessionEntry = sessionEntries[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 ( + sessionValue && + entry.startTime - lastSessionEntry.startTime < 1000 && + entry.startTime - firstSessionEntry.startTime < 5000 + ) { + sessionValue += entry.value; + sessionEntries.push(entry); + } else { + sessionValue = entry.value; + sessionEntries = [entry]; + } + + // If the current session value is larger than the current CLS value, + // update CLS and the entries contributing to it. + if (sessionValue > metric.value) { + metric.value = sessionValue; + metric.entries = sessionEntries; + if (report) { + report(); + } + } + } + }; + + const po = observe('layout-shift', entryHandler as PerformanceEntryHandler); + if (po) { + report = bindReporter(onReport, metric, reportAllChanges); + + onHidden(() => { + po.takeRecords().map(entryHandler as PerformanceEntryHandler); + report(true); + }); + } +}; diff --git a/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts b/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts index bfabaa607b52..4ea33bd6ad97 100644 --- a/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts +++ b/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts @@ -19,25 +19,21 @@ import { Metric, ReportHandler } from '../types'; export const bindReporter = ( callback: ReportHandler, metric: Metric, - po: PerformanceObserver | undefined, - observeAllUpdates?: boolean, -): (() => void) => { + reportAllChanges?: boolean, +): ((forceReport?: boolean) => void) => { let prevValue: number; - return () => { - if (po && metric.isFinal) { - po.disconnect(); - } + return (forceReport?: boolean) => { if (metric.value >= 0) { - if (observeAllUpdates || metric.isFinal || document.visibilityState === 'hidden') { + if (forceReport || reportAllChanges) { metric.delta = metric.value - (prevValue || 0); - // Report the metric if there's a non-zero delta, if the metric is - // final, or if no previous value exists (which can happen in the case - // of the document becoming hidden when the metric value is 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 (metric.delta || metric.isFinal || prevValue === undefined) { - callback(metric); + if (metric.delta || prevValue === undefined) { prevValue = metric.value; + callback(metric); } } } diff --git a/packages/tracing/src/browser/web-vitals/lib/generateUniqueID.ts b/packages/tracing/src/browser/web-vitals/lib/generateUniqueID.ts index 96fc4ad0b27f..5aaf045803f8 100644 --- a/packages/tracing/src/browser/web-vitals/lib/generateUniqueID.ts +++ b/packages/tracing/src/browser/web-vitals/lib/generateUniqueID.ts @@ -15,10 +15,10 @@ */ /** - * Performantly generate a unique, 27-char string by combining the current - * timestamp with a 13-digit random number. + * 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 = (): string => { - return `${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; + return `v2-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; }; diff --git a/packages/tracing/src/browser/web-vitals/lib/getFirstHidden.ts b/packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts similarity index 65% rename from packages/tracing/src/browser/web-vitals/lib/getFirstHidden.ts rename to packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts index bd138bcd0eda..0ef4f22f3f1b 100644 --- a/packages/tracing/src/browser/web-vitals/lib/getFirstHidden.ts +++ b/packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts @@ -16,26 +16,32 @@ import { onHidden } from './onHidden'; -let firstHiddenTime: number; +let firstHiddenTime = -1; -type HiddenType = { - readonly timeStamp: number; +const initHiddenTime = (): number => { + return document.visibilityState === 'hidden' ? 0 : Infinity; }; -export const getFirstHidden = (): HiddenType => { - if (firstHiddenTime === undefined) { +const trackChanges = (): void => { + // Update the time if/when the document becomes hidden. + onHidden(({ timeStamp }) => { + firstHiddenTime = timeStamp; + }, true); +}; + +export const getVisibilityWatcher = (): { + readonly firstHiddenTime: number; +} => { + if (firstHiddenTime < 0) { // If the document is hidden when this code runs, assume it was hidden // since navigation start. This isn't a perfect heuristic, but it's the // best we can do until an API is available to support querying past // visibilityState. - firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity; - - // Update the time if/when the document becomes hidden. - onHidden(({ timeStamp }) => (firstHiddenTime = timeStamp), true); + firstHiddenTime = initHiddenTime(); + trackChanges(); } - return { - get timeStamp() { + get firstHiddenTime() { return firstHiddenTime; }, }; diff --git a/packages/tracing/src/browser/web-vitals/lib/initMetric.ts b/packages/tracing/src/browser/web-vitals/lib/initMetric.ts index c41e484a72b2..84ebed341653 100644 --- a/packages/tracing/src/browser/web-vitals/lib/initMetric.ts +++ b/packages/tracing/src/browser/web-vitals/lib/initMetric.ts @@ -17,13 +17,12 @@ import { Metric } from '../types'; import { generateUniqueID } from './generateUniqueID'; -export const initMetric = (name: Metric['name'], value = -1): Metric => { +export const initMetric = (name: Metric['name'], value?: number): Metric => { return { name, - value, + value: value ?? -1, delta: 0, entries: [], id: generateUniqueID(), - isFinal: false, }; }; diff --git a/packages/tracing/src/browser/web-vitals/lib/observe.ts b/packages/tracing/src/browser/web-vitals/lib/observe.ts index 219d53b7af66..c7dd05ca63ae 100644 --- a/packages/tracing/src/browser/web-vitals/lib/observe.ts +++ b/packages/tracing/src/browser/web-vitals/lib/observe.ts @@ -29,6 +29,12 @@ export interface PerformanceEntryHandler { export const observe = (type: string, callback: PerformanceEntryHandler): PerformanceObserver | undefined => { try { if (PerformanceObserver.supportedEntryTypes.includes(type)) { + // More extensive feature detect needed for Firefox due to: + // https://github.com/GoogleChrome/web-vitals/issues/142 + if (type === 'first-input' && !('PerformanceEventTiming' in self)) { + return; + } + const po: PerformanceObserver = new PerformanceObserver(l => l.getEntries().map(callback)); po.observe({ type, buffered: true }); diff --git a/packages/tracing/src/browser/web-vitals/lib/onHidden.ts b/packages/tracing/src/browser/web-vitals/lib/onHidden.ts index 6d982079e93e..88b1d4993911 100644 --- a/packages/tracing/src/browser/web-vitals/lib/onHidden.ts +++ b/packages/tracing/src/browser/web-vitals/lib/onHidden.ts @@ -15,39 +15,21 @@ */ export interface OnHiddenCallback { - // TODO(philipwalton): add `isPersisted` if needed for bfcache. - ({ timeStamp, isUnloading }: { timeStamp: number; isUnloading: boolean }): void; + (event: Event): void; } -let isUnloading = false; -let listenersAdded = false; - -const onPageHide = (event: PageTransitionEvent): void => { - isUnloading = !event.persisted; -}; - -const addListeners = (): void => { - addEventListener('pagehide', onPageHide); - - // `beforeunload` is needed to fix this bug: - // https://bugs.chromium.org/p/chromium/issues/detail?id=987409 - // eslint-disable-next-line @typescript-eslint/no-empty-function - addEventListener('beforeunload', () => {}); -}; - -export const onHidden = (cb: OnHiddenCallback, once = false): void => { - if (!listenersAdded) { - addListeners(); - listenersAdded = true; - } - - addEventListener( - 'visibilitychange', - ({ timeStamp }) => { - if (document.visibilityState === 'hidden') { - cb({ timeStamp, isUnloading }); +export const onHidden = (cb: OnHiddenCallback, once?: boolean): void => { + const onHiddenOrPageHide = (event: Event): void => { + if (event.type === 'pagehide' || document.visibilityState === 'hidden') { + cb(event); + if (once) { + removeEventListener('visibilitychange', onHiddenOrPageHide, true); + removeEventListener('pagehide', onHiddenOrPageHide, true); } - }, - { capture: true, once }, - ); + } + }; + addEventListener('visibilitychange', onHiddenOrPageHide, true); + // Some browsers have buggy implementations of visibilitychange, + // so we use pagehide in addition, just to be safe. + addEventListener('pagehide', onHiddenOrPageHide, true); }; diff --git a/packages/tracing/src/browser/web-vitals/lib/whenInput.ts b/packages/tracing/src/browser/web-vitals/lib/whenInput.ts deleted file mode 100644 index 6a057fa90e7c..000000000000 --- a/packages/tracing/src/browser/web-vitals/lib/whenInput.ts +++ /dev/null @@ -1,32 +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. - */ - -let inputPromise: Promise; - -export const whenInput = (): Promise => { - if (!inputPromise) { - inputPromise = new Promise(r => { - return ['scroll', 'keydown', 'pointerdown'].map(type => { - addEventListener(type, r, { - once: true, - passive: true, - capture: true, - }); - }); - }); - } - return inputPromise; -}; diff --git a/packages/tracing/src/browser/web-vitals/types.ts b/packages/tracing/src/browser/web-vitals/types.ts index 72d846e1eb2b..a9732b3fbfbb 100644 --- a/packages/tracing/src/browser/web-vitals/types.ts +++ b/packages/tracing/src/browser/web-vitals/types.ts @@ -16,7 +16,7 @@ export interface Metric { // The name of the metric (in acronym form). - name: 'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB'; + name: 'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB' | 'UpdatedCLS'; // The current value of the metric. value: number; @@ -25,25 +25,39 @@ export interface Metric { // On the first report, `delta` and `value` will always be the same. delta: number; - // A unique ID representing this particular metric that's specific to the - // current page. This ID can be used by an analytics tool to dedupe - // multiple values sent for the same metric, or to group multiple deltas - // together and calculate a total. + // 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; - // `false` if the value of the metric may change in the future, - // for the current page. - isFinal: boolean; - // Any performance entries used in the metric value calculation. // Note, entries will be added to the array as the value changes. - entries: PerformanceEntry[]; + entries: (PerformanceEntry | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[]; } export interface ReportHandler { (metric: Metric): void; } +// https://wicg.github.io/event-timing/#sec-performance-event-timing +export interface PerformanceEventTiming extends PerformanceEntry { + processingStart: DOMHighResTimeStamp; + processingEnd: DOMHighResTimeStamp; + duration: DOMHighResTimeStamp; + cancelable?: boolean; + target?: Element; +} + +export type FirstInputPolyfillEntry = Omit; + +export interface FirstInputPolyfillCallback { + (entry: FirstInputPolyfillEntry): void; +} + // http://wicg.github.io/netinfo/#navigatornetworkinformation-interface export interface NavigatorNetworkInformation { readonly connection?: NetworkInformation; @@ -93,3 +107,18 @@ export type NavigationTimingPolyfillEntry = Omit< | 'decodedBodySize' | 'toJSON' >; + +export interface WebVitalsGlobal { + firstInputPolyfill: (onFirstInput: FirstInputPolyfillCallback) => void; + resetFirstInputPolyfill: () => void; + firstHiddenTime: number; +} + +declare global { + interface Window { + webVitals: WebVitalsGlobal; + + // Build flags: + __WEB_VITALS_POLYFILL__: boolean; + } +}