From 1746cf229382577c810c06f2b33948fbaa9d39ee Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 18 Oct 2022 12:14:29 +0200 Subject: [PATCH 1/8] Merge v3 changes --- packages/tracing/src/browser/metrics/index.ts | 31 ++--- .../tracing/src/browser/web-vitals/getCLS.ts | 102 ++++++++------- .../tracing/src/browser/web-vitals/getFID.ts | 31 +++-- .../tracing/src/browser/web-vitals/getLCP.ts | 62 ++++----- .../browser/web-vitals/lib/bindReporter.ts | 10 +- .../web-vitals/lib/generateUniqueID.ts | 2 +- .../web-vitals/lib/getActivationStart.ts | 22 ++++ .../web-vitals/lib/getNavigationEntry.ts | 51 ++++++++ .../web-vitals/lib/getVisibilityWatcher.ts | 4 +- .../src/browser/web-vitals/lib/initMetric.ts | 19 ++- .../src/browser/web-vitals/lib/observe.ts | 40 ++++-- .../tracing/src/browser/web-vitals/types.ts | 119 ++++++++++++------ .../src/browser/web-vitals/types/base.ts | 107 ++++++++++++++++ .../src/browser/web-vitals/types/cls.ts | 89 +++++++++++++ .../src/browser/web-vitals/types/fid.ts | 82 ++++++++++++ .../src/browser/web-vitals/types/lcp.ts | 103 +++++++++++++++ .../src/browser/web-vitals/types/polyfills.ts | 27 ++++ 17 files changed, 744 insertions(+), 157 deletions(-) create mode 100644 packages/tracing/src/browser/web-vitals/lib/getActivationStart.ts create mode 100644 packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts create mode 100644 packages/tracing/src/browser/web-vitals/types/base.ts create mode 100644 packages/tracing/src/browser/web-vitals/types/cls.ts create mode 100644 packages/tracing/src/browser/web-vitals/types/fid.ts create mode 100644 packages/tracing/src/browser/web-vitals/types/lcp.ts create mode 100644 packages/tracing/src/browser/web-vitals/types/polyfills.ts diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts index cadbc9ca7caa..f6e4d28e0e1f 100644 --- a/packages/tracing/src/browser/metrics/index.ts +++ b/packages/tracing/src/browser/metrics/index.ts @@ -5,9 +5,9 @@ import { browserPerformanceTimeOrigin, htmlTreeAsString, logger, WINDOW } from ' import { IdleTransaction } from '../../idletransaction'; import { Transaction } from '../../transaction'; import { getActiveTransaction, msToSec } from '../../utils'; -import { getCLS, LayoutShift } from '../web-vitals/getCLS'; -import { getFID } from '../web-vitals/getFID'; -import { getLCP, LargestContentfulPaint } from '../web-vitals/getLCP'; +import { onCLS } from '../web-vitals/getCLS'; +import { onFID } from '../web-vitals/getFID'; +import { onLCP } from '../web-vitals/getLCP'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe'; import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; @@ -65,7 +65,7 @@ function _trackCLS(): void { // See: // https://web.dev/evolving-cls/ // https://web.dev/cls-web-tooling/ - getCLS(metric => { + onCLS(metric => { const entry = metric.entries.pop(); if (!entry) { return; @@ -79,21 +79,24 @@ function _trackCLS(): void { /** Starts tracking the Largest Contentful Paint on the current page. */ function _trackLCP(reportAllChanges: boolean): void { - getLCP(metric => { - const entry = metric.entries.pop(); - if (!entry) { - return; - } + onLCP( + metric => { + const entry = metric.entries.pop(); + if (!entry) { + return; + } - __DEBUG_BUILD__ && logger.log('[Measurements] Adding LCP'); - _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; - _lcpEntry = entry as LargestContentfulPaint; - }, reportAllChanges); + __DEBUG_BUILD__ && logger.log('[Measurements] Adding LCP'); + _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; + _lcpEntry = entry as LargestContentfulPaint; + }, + { reportAllChanges }, + ); } /** Starts tracking the First Input Delay on the current page. */ function _trackFID(): void { - getFID(metric => { + onFID(metric => { const entry = metric.entries.pop(); if (!entry) { return; diff --git a/packages/tracing/src/browser/web-vitals/getCLS.ts b/packages/tracing/src/browser/web-vitals/getCLS.ts index ef73ceb3aee0..43d9ea19d91e 100644 --- a/packages/tracing/src/browser/web-vitals/getCLS.ts +++ b/packages/tracing/src/browser/web-vitals/getCLS.ts @@ -16,72 +16,78 @@ import { bindReporter } from './lib/bindReporter'; import { initMetric } from './lib/initMetric'; -import { observe, PerformanceEntryHandler } from './lib/observe'; +import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { ReportHandler } from './types'; +import { CLSMetric, ReportCallback, ReportOpts } 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 getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => { +/** + * Calculates the [CLS](https://web.dev/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/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: ReportCallback, opts: ReportOpts = {}): void => { const metric = initMetric('CLS', 0); let report: ReturnType; let sessionValue = 0; let sessionEntries: PerformanceEntry[] = []; - const entryHandler = (entry: LayoutShift): void => { - // Only count layout shifts without recent user input. - // TODO: Figure out why entry can be undefined - if (entry && !entry.hadRecentInput) { - const firstSessionEntry = sessionEntries[0]; - const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; + // const handleEntries = (entries: Metric['entries']) => { + const handleEntries = (entries: LayoutShift[]): void => { + entries.forEach(entry => { + // 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 && - sessionEntries.length !== 0 && - entry.startTime - lastSessionEntry.startTime < 1000 && - entry.startTime - firstSessionEntry.startTime < 5000 - ) { - sessionValue += entry.value; - sessionEntries.push(entry); - } else { - sessionValue = entry.value; - sessionEntries = [entry]; - } + // 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) { + // 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; report(); } } - } + }); }; - const po = observe('layout-shift', entryHandler as PerformanceEntryHandler); + const po = observe('layout-shift', handleEntries); if (po) { - report = bindReporter(onReport, metric, reportAllChanges); + report = bindReporter(onReport, metric, opts.reportAllChanges); onHidden(() => { - po.takeRecords().map(entryHandler as PerformanceEntryHandler); + handleEntries(po.takeRecords() as CLSMetric['entries']); report(true); }); } diff --git a/packages/tracing/src/browser/web-vitals/getFID.ts b/packages/tracing/src/browser/web-vitals/getFID.ts index c7149f70aadb..982bbd4a3a01 100644 --- a/packages/tracing/src/browser/web-vitals/getFID.ts +++ b/packages/tracing/src/browser/web-vitals/getFID.ts @@ -17,29 +17,44 @@ import { bindReporter } from './lib/bindReporter'; import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; -import { observe, PerformanceEntryHandler } from './lib/observe'; +import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { PerformanceEventTiming, ReportHandler } from './types'; +import { FIDMetric, PerformanceEventTiming, ReportCallback, ReportOpts } from './types'; -export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => { +/** + * Calculates the [FID](https://web.dev/fid/) value for the current page and + * calls the `callback` function once the value is ready, along with the + * relevant `first-input` performance entry used to determine the value. The + * reported value is a `DOMHighResTimeStamp`. + * + * _**Important:** since FID is only reported after the user interacts with the + * page, it's possible that it will not be reported for some page loads._ + */ +export const onFID = (onReport: ReportCallback, opts: ReportOpts = {}): void => { const visibilityWatcher = getVisibilityWatcher(); const metric = initMetric('FID'); + // eslint-disable-next-line prefer-const let report: ReturnType; - const entryHandler = (entry: PerformanceEventTiming): void => { + const handleEntry = (entry: PerformanceEventTiming): void => { // Only report if the page wasn't hidden prior to the first input. - if (report && entry.startTime < visibilityWatcher.firstHiddenTime) { + if (entry.startTime < visibilityWatcher.firstHiddenTime) { metric.value = entry.processingStart - entry.startTime; metric.entries.push(entry); report(true); } }; - const po = observe('first-input', entryHandler as PerformanceEntryHandler); + const handleEntries = (entries: FIDMetric['entries']): void => { + (entries as PerformanceEventTiming[]).forEach(handleEntry); + }; + + const po = observe('first-input', handleEntries); + report = bindReporter(onReport, metric, opts.reportAllChanges); + if (po) { - report = bindReporter(onReport, metric, reportAllChanges); onHidden(() => { - po.takeRecords().map(entryHandler as PerformanceEntryHandler); + handleEntries(po.takeRecords() as FIDMetric['entries']); po.disconnect(); }, true); } diff --git a/packages/tracing/src/browser/web-vitals/getLCP.ts b/packages/tracing/src/browser/web-vitals/getLCP.ts index 8b00dc417746..0953af1dd3c9 100644 --- a/packages/tracing/src/browser/web-vitals/getLCP.ts +++ b/packages/tracing/src/browser/web-vitals/getLCP.ts @@ -15,55 +15,57 @@ */ import { bindReporter } from './lib/bindReporter'; +import { getActivationStart } from './lib/getActivationStart'; import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; -import { observe, PerformanceEntryHandler } from './lib/observe'; +import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; -import { ReportHandler } from './types'; - -// https://wicg.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface -export interface LargestContentfulPaint extends PerformanceEntry { - renderTime: DOMHighResTimeStamp; - loadTime: DOMHighResTimeStamp; - size: number; - id: string; - url: string; - element?: Element; - toJSON(): Record; -} +import { LCPMetric, ReportCallback, ReportOpts } from './types'; const reportedMetricIDs: Record = {}; -export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => { +/** + * Calculates the [LCP](https://web.dev/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: ReportCallback, opts: ReportOpts = {}): void => { const visibilityWatcher = getVisibilityWatcher(); const metric = initMetric('LCP'); let report: ReturnType; - const entryHandler = (entry: PerformanceEntry): void => { - // The startTime attribute returns the value of the renderTime if it is not 0, - // and the value of the loadTime otherwise. - const value = entry.startTime; + const handleEntries = (entries: LCPMetric['entries']): void => { + const lastEntry = entries[entries.length - 1] as LargestContentfulPaint; + if (lastEntry) { + // 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. + const value = lastEntry.startTime - getActivationStart(); - // 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 < visibilityWatcher.firstHiddenTime) { - metric.value = value; - metric.entries.push(entry); - } - - if (report) { - report(); + // Only report if the page wasn't hidden prior to LCP. + if (value < visibilityWatcher.firstHiddenTime) { + metric.value = value; + metric.entries = [lastEntry]; + report(); + } } }; - const po = observe('largest-contentful-paint', entryHandler); + const po = observe('largest-contentful-paint', handleEntries); if (po) { - report = bindReporter(onReport, metric, reportAllChanges); + report = bindReporter(onReport, metric, opts.reportAllChanges); const stopListening = (): void => { if (!reportedMetricIDs[metric.id]) { - po.takeRecords().map(entryHandler as PerformanceEntryHandler); + handleEntries(po.takeRecords() as LCPMetric['entries']); po.disconnect(); reportedMetricIDs[metric.id] = true; 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 4ea33bd6ad97..6e304d747c95 100644 --- a/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts +++ b/packages/tracing/src/browser/web-vitals/lib/bindReporter.ts @@ -14,25 +14,27 @@ * limitations under the License. */ -import { Metric, ReportHandler } from '../types'; +import { Metric, ReportCallback } from '../types'; export const bindReporter = ( - callback: ReportHandler, + callback: ReportCallback, metric: Metric, reportAllChanges?: boolean, ): ((forceReport?: boolean) => void) => { let prevValue: number; + let delta: number; return (forceReport?: boolean) => { if (metric.value >= 0) { if (forceReport || reportAllChanges) { - metric.delta = metric.value - (prevValue || 0); + 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 (metric.delta || prevValue === undefined) { + if (delta || prevValue === undefined) { prevValue = metric.value; + metric.delta = delta; 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 5aaf045803f8..a7972017d51c 100644 --- a/packages/tracing/src/browser/web-vitals/lib/generateUniqueID.ts +++ b/packages/tracing/src/browser/web-vitals/lib/generateUniqueID.ts @@ -20,5 +20,5 @@ * @return {string} */ export const generateUniqueID = (): string => { - return `v2-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; + return `v3-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; }; diff --git a/packages/tracing/src/browser/web-vitals/lib/getActivationStart.ts b/packages/tracing/src/browser/web-vitals/lib/getActivationStart.ts new file mode 100644 index 000000000000..84c742098aab --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/lib/getActivationStart.ts @@ -0,0 +1,22 @@ +/* + * 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 { getNavigationEntry } from './getNavigationEntry'; + +export const getActivationStart = (): number => { + const navEntry = getNavigationEntry(); + return (navEntry && navEntry.activationStart) || 0; +}; diff --git a/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts b/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts new file mode 100644 index 000000000000..03d41cf3de3d --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/lib/getNavigationEntry.ts @@ -0,0 +1,51 @@ +/* + * 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 '@sentry/utils'; + +import { NavigationTimingPolyfillEntry } from '../types'; + +const getNavigationEntryFromPerformanceTiming = (): NavigationTimingPolyfillEntry => { + // eslint-disable-next-line deprecation/deprecation + const timing = WINDOW.performance.timing; + // eslint-disable-next-line deprecation/deprecation + const type = WINDOW.performance.navigation.type; + + const navigationEntry: { [key: string]: number | string } = { + entryType: 'navigation', + startTime: 0, + type: type == 2 ? 'back_forward' : type === 1 ? 'reload' : 'navigate', + }; + + for (const key in timing) { + if (key !== 'navigationStart' && key !== 'toJSON') { + navigationEntry[key] = Math.max((timing[key as keyof PerformanceTiming] as number) - timing.navigationStart, 0); + } + } + return navigationEntry as unknown as NavigationTimingPolyfillEntry; +}; + +export const getNavigationEntry = (): PerformanceNavigationTiming | NavigationTimingPolyfillEntry | undefined => { + if (WINDOW.__WEB_VITALS_POLYFILL__) { + return ( + WINDOW.performance && + ((performance.getEntriesByType && performance.getEntriesByType('navigation')[0]) || + getNavigationEntryFromPerformanceTiming()) + ); + } else { + return WINDOW.performance && performance.getEntriesByType && performance.getEntriesByType('navigation')[0]; + } +}; diff --git a/packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts b/packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts index 371133ff6df2..d20940dc35fb 100644 --- a/packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/tracing/src/browser/web-vitals/lib/getVisibilityWatcher.ts @@ -21,7 +21,9 @@ import { onHidden } from './onHidden'; let firstHiddenTime = -1; const initHiddenTime = (): number => { - return WINDOW.document.visibilityState === 'hidden' ? 0 : Infinity; + // If the document is hidden and not prerendering, assume it was always + // hidden and the page was loaded in the background. + return WINDOW.document.visibilityState === 'hidden' && !WINDOW.document.prerendering ? 0 : Infinity; }; const trackChanges = (): void => { diff --git a/packages/tracing/src/browser/web-vitals/lib/initMetric.ts b/packages/tracing/src/browser/web-vitals/lib/initMetric.ts index 84ebed341653..c9fe2c9389be 100644 --- a/packages/tracing/src/browser/web-vitals/lib/initMetric.ts +++ b/packages/tracing/src/browser/web-vitals/lib/initMetric.ts @@ -14,15 +14,32 @@ * limitations under the License. */ +import { WINDOW } from '@sentry/utils'; + import { Metric } from '../types'; import { generateUniqueID } from './generateUniqueID'; +import { getActivationStart } from './getActivationStart'; +import { getNavigationEntry } from './getNavigationEntry'; export const initMetric = (name: Metric['name'], value?: number): Metric => { + const navEntry = getNavigationEntry(); + let navigationType: Metric['navigationType'] = 'navigate'; + + if (navEntry) { + if (WINDOW.document.prerendering || getActivationStart() > 0) { + navigationType = 'prerender'; + } else { + navigationType = navEntry.type.replace(/_/g, '-') as Metric['navigationType']; + } + } + return { name, - value: value ?? -1, + value: typeof value === 'undefined' ? -1 : value, + rating: 'good', // Will be updated if the value changes. delta: 0, entries: [], id: generateUniqueID(), + navigationType, }; }; diff --git a/packages/tracing/src/browser/web-vitals/lib/observe.ts b/packages/tracing/src/browser/web-vitals/lib/observe.ts index c7dd05ca63ae..98cb57dac2e8 100644 --- a/packages/tracing/src/browser/web-vitals/lib/observe.ts +++ b/packages/tracing/src/browser/web-vitals/lib/observe.ts @@ -14,10 +14,23 @@ * limitations under the License. */ +import { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry, PerformancePaintTiming } from '../types'; + export interface PerformanceEntryHandler { (entry: PerformanceEntry): void; } +interface PerformanceEntryMap { + event: PerformanceEventTiming[]; + paint: PerformancePaintTiming[]; + 'layout-shift': LayoutShift[]; + 'largest-contentful-paint': LargestContentfulPaint[]; + 'first-input': PerformanceEventTiming[] | FirstInputPolyfillEntry[]; + navigation: PerformanceNavigationTiming[] | NavigationTimingPolyfillEntry[]; + resource: PerformanceResourceTiming[]; + longtask: PerformanceEntry; +} + /** * Takes a performance entry type and a callback function, and creates a * `PerformanceObserver` instance that will observe the specified entry type @@ -26,18 +39,25 @@ export interface PerformanceEntryHandler { * 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: string, callback: PerformanceEntryHandler): PerformanceObserver | undefined => { +export const observe = ( + type: K, + callback: (entries: PerformanceEntryMap[K]) => void, + opts?: PerformanceObserverInit, +): 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 }); + const po = new PerformanceObserver(list => { + callback(list.getEntries() as PerformanceEntryMap[K]); + }); + po.observe( + Object.assign( + { + type, + buffered: true, + }, + opts || {}, + ) as PerformanceObserverInit, + ); return po; } } catch (e) { diff --git a/packages/tracing/src/browser/web-vitals/types.ts b/packages/tracing/src/browser/web-vitals/types.ts index a9732b3fbfbb..c78152748f97 100644 --- a/packages/tracing/src/browser/web-vitals/types.ts +++ b/packages/tracing/src/browser/web-vitals/types.ts @@ -14,36 +14,35 @@ * limitations under the License. */ -export interface Metric { - // The name of the metric (in acronym form). - name: 'CLS' | 'FCP' | 'FID' | 'LCP' | 'TTFB' | 'UpdatedCLS'; - - // The current value of the metric. - value: number; - - // 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 used in the metric value calculation. - // Note, entries will be added to the array as the value changes. - entries: (PerformanceEntry | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[]; +import { FirstInputPolyfillCallback } from './types/polyfills'; + +export * from './types/base'; +export * from './types/polyfills'; + +export * from './types/cls'; +export * from './types/fid'; +export * from './types/lcp'; + +// -------------------------------------------------------------------------- +// Web Vitals package globals +// -------------------------------------------------------------------------- + +export interface WebVitalsGlobal { + firstInputPolyfill: (onFirstInput: FirstInputPolyfillCallback) => void; + resetFirstInputPolyfill: () => void; + firstHiddenTime: number; } -export interface ReportHandler { - (metric: Metric): void; +declare global { + interface Window { + webVitals: WebVitalsGlobal; + + // Build flags: + __WEB_VITALS_POLYFILL__: boolean; + } } -// https://wicg.github.io/event-timing/#sec-performance-event-timing +export type PerformancePaintTiming = PerformanceEntry; export interface PerformanceEventTiming extends PerformanceEntry { processingStart: DOMHighResTimeStamp; processingEnd: DOMHighResTimeStamp; @@ -52,13 +51,16 @@ export interface PerformanceEventTiming extends PerformanceEntry { target?: Element; } -export type FirstInputPolyfillEntry = Omit; +// -------------------------------------------------------------------------- +// Everything below is modifications to built-in modules. +// -------------------------------------------------------------------------- -export interface FirstInputPolyfillCallback { - (entry: FirstInputPolyfillEntry): void; +interface PerformanceEntryMap { + navigation: PerformanceNavigationTiming; + resource: PerformanceResourceTiming; + paint: PerformancePaintTiming; } -// http://wicg.github.io/netinfo/#navigatornetworkinformation-interface export interface NavigatorNetworkInformation { readonly connection?: NetworkInformation; } @@ -108,17 +110,54 @@ export type NavigationTimingPolyfillEntry = Omit< | 'toJSON' >; -export interface WebVitalsGlobal { - firstInputPolyfill: (onFirstInput: FirstInputPolyfillCallback) => void; - resetFirstInputPolyfill: () => void; - firstHiddenTime: number; -} - +// Update built-in types to be more accurate. declare global { - interface Window { - webVitals: WebVitalsGlobal; + // https://wicg.github.io/nav-speculation/prerendering.html#document-prerendering + interface Document { + prerendering?: boolean; + } - // Build flags: - __WEB_VITALS_POLYFILL__: 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; + interactionId?: number; + } + + // https://wicg.github.io/layout-instability/#sec-layout-shift-attribution + interface LayoutShiftAttribution { + node?: Node; + 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 { + renderTime: DOMHighResTimeStamp; + loadTime: DOMHighResTimeStamp; + size: number; + id: string; + url: string; + element?: Element; } } diff --git a/packages/tracing/src/browser/web-vitals/types/base.ts b/packages/tracing/src/browser/web-vitals/types/base.ts new file mode 100644 index 000000000000..a5145f5bf69d --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/types/base.ts @@ -0,0 +1,107 @@ +/* + * 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 {FirstInputPolyfillEntry, NavigationTimingPolyfillEntry} from './polyfills.js'; + + +export interface Metric { + /** + * The name of the metric (in acronym form). + */ + name: 'CLS' | 'FCP' | 'FID' | '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 | LayoutShift | FirstInputPolyfillEntry | NavigationTimingPolyfillEntry)[]; + + /** + * 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'; +} + +/** + * A version of the `Metric` that is used with the attribution build. + */ +export interface MetricWithAttribution extends Metric { + /** + * An object containing potentially-helpful debugging information that + * can be sent along with the metric value for the current page visit in + * order to help identify issues happening to real-users in the field. + */ + attribution: {[key: string]: unknown}; +} + +export interface ReportCallback { + (metric: Metric): void; +} + +export interface ReportOpts { + reportAllChanges?: boolean; + durationThreshold?: number; +} + +/** + * 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/tracing/src/browser/web-vitals/types/cls.ts b/packages/tracing/src/browser/web-vitals/types/cls.ts new file mode 100644 index 000000000000..055e1f946c3d --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/types/cls.ts @@ -0,0 +1,89 @@ +/* + * 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 {LoadState, Metric, ReportCallback} from './base.js'; + + +/** + * 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 { + /** + * A selector identifying the first element (in document order) that + * shifted when the single largest layout shift contributing to the page's + * CLS score occurred. + */ + 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; +} + +/** + * A CLS-specific version of the ReportCallback function. + */ +export interface CLSReportCallback extends ReportCallback { + (metric: CLSMetric): void; +} + +/** + * A CLS-specific version of the ReportCallback function with attribution. + */ +export interface CLSReportCallbackWithAttribution extends CLSReportCallback { + (metric: CLSMetricWithAttribution): void; +} diff --git a/packages/tracing/src/browser/web-vitals/types/fid.ts b/packages/tracing/src/browser/web-vitals/types/fid.ts new file mode 100644 index 000000000000..1aa34c212a6f --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/types/fid.ts @@ -0,0 +1,82 @@ +/* + * 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 {LoadState, Metric, ReportCallback} from './base.js'; +import {FirstInputPolyfillEntry} from './polyfills.js'; + + +/** + * An FID-specific version of the Metric object. + */ +export interface FIDMetric extends Metric { + name: 'FID'; + entries: (PerformanceEventTiming | FirstInputPolyfillEntry)[]; +} + +/** + * An object containing potentially-helpful debugging information that + * can be sent along with the FID value for the current page visit in order + * to help identify issues happening to real-users in the field. + */ +export interface FIDAttribution { + /** + * A selector identifying the element that the user interacted with. This + * element will be the `target` of the `event` dispatched. + */ + eventTarget: string; + /** + * The time when the user interacted. This time will match the `timeStamp` + * value of the `event` dispatched. + */ + eventTime: number; + /** + * The `type` of the `event` dispatched from the user interaction. + */ + eventType: string; + /** + * The `PerformanceEventTiming` entry corresponding to FID (or the + * polyfill entry in browsers that don't support Event Timing). + */ + eventEntry: PerformanceEventTiming | FirstInputPolyfillEntry, + /** + * The loading state of the document at the time when the first interaction + * occurred (see `LoadState` for details). If the first interaction occurred + * while the document was loading and executing script (e.g. usually in the + * `dom-interactive` phase) it can result in long input delays. + */ + loadState: LoadState; +} + +/** + * An FID-specific version of the Metric object with attribution. + */ +export interface FIDMetricWithAttribution extends FIDMetric { + attribution: FIDAttribution; +} + +/** + * An FID-specific version of the ReportCallback function. + */ +export interface FIDReportCallback extends ReportCallback { + (metric: FIDMetric): void; +} + +/** + * An FID-specific version of the ReportCallback function with attribution. + */ +export interface FIDReportCallbackWithAttribution extends FIDReportCallback { + (metric: FIDMetricWithAttribution): void; +} diff --git a/packages/tracing/src/browser/web-vitals/types/lcp.ts b/packages/tracing/src/browser/web-vitals/types/lcp.ts new file mode 100644 index 000000000000..486f735d0cb5 --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/types/lcp.ts @@ -0,0 +1,103 @@ +/* + * 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 {Metric, ReportCallback} from './base.js'; +import {NavigationTimingPolyfillEntry} from './polyfills.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 { + /** + * The element corresponding to the largest contentful paint for the page. + */ + element?: 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/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/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/optimize-lcp/) for + * details. + */ + resourceLoadTime: number; + /** + * The delta between when the LCP resource finishes loading until the LCP + * element is fully rendered. See [Optimize + * LCP](https://web.dev/optimize-lcp/) for details. + */ + elementRenderDelay: number; + /** + * The `navigation` entry of the current page, which is useful for diagnosing + * general page load issues. + */ + navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry; + /** + * 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; +} + +/** + * An LCP-specific version of the ReportCallback function. + */ +export interface LCPReportCallback extends ReportCallback { + (metric: LCPMetric): void; +} + +/** + * An LCP-specific version of the ReportCallback function with attribution. + */ +export interface LCPReportCallbackWithAttribution extends LCPReportCallback { + (metric: LCPMetricWithAttribution): void; +} diff --git a/packages/tracing/src/browser/web-vitals/types/polyfills.ts b/packages/tracing/src/browser/web-vitals/types/polyfills.ts new file mode 100644 index 000000000000..3d4d822ad7fa --- /dev/null +++ b/packages/tracing/src/browser/web-vitals/types/polyfills.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export type FirstInputPolyfillEntry = Omit; + +export interface FirstInputPolyfillCallback { + (entry: FirstInputPolyfillEntry): void; +} + +export type NavigationTimingPolyfillEntry = Omit & { + type: PerformanceNavigationTiming['type']; +} From 2c99412127bf835c24bdaf06e34cd59c83f8fade Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 18 Oct 2022 12:15:11 +0200 Subject: [PATCH 2/8] Renames --- packages/tracing/src/browser/metrics/index.ts | 6 +++--- .../tracing/src/browser/web-vitals/{getCLS.ts => onCLS.ts} | 0 .../tracing/src/browser/web-vitals/{getFID.ts => onFID.ts} | 0 .../tracing/src/browser/web-vitals/{getLCP.ts => onLCP.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/tracing/src/browser/web-vitals/{getCLS.ts => onCLS.ts} (100%) rename packages/tracing/src/browser/web-vitals/{getFID.ts => onFID.ts} (100%) rename packages/tracing/src/browser/web-vitals/{getLCP.ts => onLCP.ts} (100%) diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts index f6e4d28e0e1f..a46fcf4f3956 100644 --- a/packages/tracing/src/browser/metrics/index.ts +++ b/packages/tracing/src/browser/metrics/index.ts @@ -5,11 +5,11 @@ import { browserPerformanceTimeOrigin, htmlTreeAsString, logger, WINDOW } from ' import { IdleTransaction } from '../../idletransaction'; import { Transaction } from '../../transaction'; import { getActiveTransaction, msToSec } from '../../utils'; -import { onCLS } from '../web-vitals/getCLS'; -import { onFID } from '../web-vitals/getFID'; -import { onLCP } from '../web-vitals/getLCP'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe'; +import { onCLS } from '../web-vitals/onCLS'; +import { onFID } from '../web-vitals/onFID'; +import { onLCP } from '../web-vitals/onLCP'; import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; import { _startChild, isMeasurementValue } from './utils'; diff --git a/packages/tracing/src/browser/web-vitals/getCLS.ts b/packages/tracing/src/browser/web-vitals/onCLS.ts similarity index 100% rename from packages/tracing/src/browser/web-vitals/getCLS.ts rename to packages/tracing/src/browser/web-vitals/onCLS.ts diff --git a/packages/tracing/src/browser/web-vitals/getFID.ts b/packages/tracing/src/browser/web-vitals/onFID.ts similarity index 100% rename from packages/tracing/src/browser/web-vitals/getFID.ts rename to packages/tracing/src/browser/web-vitals/onFID.ts diff --git a/packages/tracing/src/browser/web-vitals/getLCP.ts b/packages/tracing/src/browser/web-vitals/onLCP.ts similarity index 100% rename from packages/tracing/src/browser/web-vitals/getLCP.ts rename to packages/tracing/src/browser/web-vitals/onLCP.ts From 901e8ad78bd41208a9596ba1d5beb51c533af3e1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 18 Oct 2022 13:14:32 +0200 Subject: [PATCH 3/8] Revert "Renames" This reverts commit 2c99412127bf835c24bdaf06e34cd59c83f8fade. --- packages/tracing/src/browser/metrics/index.ts | 6 +++--- .../tracing/src/browser/web-vitals/{onCLS.ts => getCLS.ts} | 0 .../tracing/src/browser/web-vitals/{onFID.ts => getFID.ts} | 0 .../tracing/src/browser/web-vitals/{onLCP.ts => getLCP.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename packages/tracing/src/browser/web-vitals/{onCLS.ts => getCLS.ts} (100%) rename packages/tracing/src/browser/web-vitals/{onFID.ts => getFID.ts} (100%) rename packages/tracing/src/browser/web-vitals/{onLCP.ts => getLCP.ts} (100%) diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts index a46fcf4f3956..f6e4d28e0e1f 100644 --- a/packages/tracing/src/browser/metrics/index.ts +++ b/packages/tracing/src/browser/metrics/index.ts @@ -5,11 +5,11 @@ import { browserPerformanceTimeOrigin, htmlTreeAsString, logger, WINDOW } from ' import { IdleTransaction } from '../../idletransaction'; import { Transaction } from '../../transaction'; import { getActiveTransaction, msToSec } from '../../utils'; +import { onCLS } from '../web-vitals/getCLS'; +import { onFID } from '../web-vitals/getFID'; +import { onLCP } from '../web-vitals/getLCP'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe'; -import { onCLS } from '../web-vitals/onCLS'; -import { onFID } from '../web-vitals/onFID'; -import { onLCP } from '../web-vitals/onLCP'; import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; import { _startChild, isMeasurementValue } from './utils'; diff --git a/packages/tracing/src/browser/web-vitals/onCLS.ts b/packages/tracing/src/browser/web-vitals/getCLS.ts similarity index 100% rename from packages/tracing/src/browser/web-vitals/onCLS.ts rename to packages/tracing/src/browser/web-vitals/getCLS.ts diff --git a/packages/tracing/src/browser/web-vitals/onFID.ts b/packages/tracing/src/browser/web-vitals/getFID.ts similarity index 100% rename from packages/tracing/src/browser/web-vitals/onFID.ts rename to packages/tracing/src/browser/web-vitals/getFID.ts diff --git a/packages/tracing/src/browser/web-vitals/onLCP.ts b/packages/tracing/src/browser/web-vitals/getLCP.ts similarity index 100% rename from packages/tracing/src/browser/web-vitals/onLCP.ts rename to packages/tracing/src/browser/web-vitals/getLCP.ts From bb0729c767f773de16f0d747c150bf97965616a7 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 18 Oct 2022 13:32:44 +0200 Subject: [PATCH 4/8] Linting --- .../tracing/src/browser/web-vitals/types/base.ts | 7 +++---- .../tracing/src/browser/web-vitals/types/cls.ts | 5 ++--- .../tracing/src/browser/web-vitals/types/fid.ts | 7 +++---- .../tracing/src/browser/web-vitals/types/lcp.ts | 9 ++++----- .../src/browser/web-vitals/types/polyfills.ts | 15 +++++++++++---- 5 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/tracing/src/browser/web-vitals/types/base.ts b/packages/tracing/src/browser/web-vitals/types/base.ts index a5145f5bf69d..5194a8fd623b 100644 --- a/packages/tracing/src/browser/web-vitals/types/base.ts +++ b/packages/tracing/src/browser/web-vitals/types/base.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import {FirstInputPolyfillEntry, NavigationTimingPolyfillEntry} from './polyfills.js'; - +import { FirstInputPolyfillEntry, NavigationTimingPolyfillEntry } from './polyfills'; export interface Metric { /** @@ -65,7 +64,7 @@ export interface Metric { * 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'; + navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender'; } /** @@ -77,7 +76,7 @@ export interface MetricWithAttribution extends Metric { * can be sent along with the metric value for the current page visit in * order to help identify issues happening to real-users in the field. */ - attribution: {[key: string]: unknown}; + attribution: { [key: string]: unknown }; } export interface ReportCallback { diff --git a/packages/tracing/src/browser/web-vitals/types/cls.ts b/packages/tracing/src/browser/web-vitals/types/cls.ts index 055e1f946c3d..c4252dc31916 100644 --- a/packages/tracing/src/browser/web-vitals/types/cls.ts +++ b/packages/tracing/src/browser/web-vitals/types/cls.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import {LoadState, Metric, ReportCallback} from './base.js'; - +import { LoadState, Metric, ReportCallback } from './base'; /** * A CLS-specific version of the Metric object. @@ -30,7 +29,7 @@ export interface CLSMetric extends Metric { * 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 { +export interface CLSAttribution { /** * A selector identifying the first element (in document order) that * shifted when the single largest layout shift contributing to the page's diff --git a/packages/tracing/src/browser/web-vitals/types/fid.ts b/packages/tracing/src/browser/web-vitals/types/fid.ts index 1aa34c212a6f..324c7e25ff66 100644 --- a/packages/tracing/src/browser/web-vitals/types/fid.ts +++ b/packages/tracing/src/browser/web-vitals/types/fid.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import {LoadState, Metric, ReportCallback} from './base.js'; -import {FirstInputPolyfillEntry} from './polyfills.js'; - +import { LoadState, Metric, ReportCallback } from './base'; +import { FirstInputPolyfillEntry } from './polyfills'; /** * An FID-specific version of the Metric object. @@ -50,7 +49,7 @@ export interface FIDAttribution { * The `PerformanceEventTiming` entry corresponding to FID (or the * polyfill entry in browsers that don't support Event Timing). */ - eventEntry: PerformanceEventTiming | FirstInputPolyfillEntry, + eventEntry: PerformanceEventTiming | FirstInputPolyfillEntry; /** * The loading state of the document at the time when the first interaction * occurred (see `LoadState` for details). If the first interaction occurred diff --git a/packages/tracing/src/browser/web-vitals/types/lcp.ts b/packages/tracing/src/browser/web-vitals/types/lcp.ts index 486f735d0cb5..841ddca1e6de 100644 --- a/packages/tracing/src/browser/web-vitals/types/lcp.ts +++ b/packages/tracing/src/browser/web-vitals/types/lcp.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import {Metric, ReportCallback} from './base.js'; -import {NavigationTimingPolyfillEntry} from './polyfills.js'; - +import { Metric, ReportCallback } from './base'; +import { NavigationTimingPolyfillEntry } from './polyfills'; /** * An LCP-specific version of the Metric object. @@ -35,12 +34,12 @@ export interface LCPAttribution { /** * The element corresponding to the largest contentful paint for the page. */ - element?: string, + element?: 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, + 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 diff --git a/packages/tracing/src/browser/web-vitals/types/polyfills.ts b/packages/tracing/src/browser/web-vitals/types/polyfills.ts index 3d4d822ad7fa..4216d72ac732 100644 --- a/packages/tracing/src/browser/web-vitals/types/polyfills.ts +++ b/packages/tracing/src/browser/web-vitals/types/polyfills.ts @@ -20,8 +20,15 @@ export interface FirstInputPolyfillCallback { (entry: FirstInputPolyfillEntry): void; } -export type NavigationTimingPolyfillEntry = Omit & { +export type NavigationTimingPolyfillEntry = Omit< + PerformanceNavigationTiming, + | 'initiatorType' + | 'nextHopProtocol' + | 'redirectCount' + | 'transferSize' + | 'encodedBodySize' + | 'decodedBodySize' + | 'type' +> & { type: PerformanceNavigationTiming['type']; -} +}; From cb5f06628c08c6c2bf47c7958bdb18700a171be2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 18 Oct 2022 14:51:11 +0200 Subject: [PATCH 5/8] Don't fail fast for testing --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0da365f783f6..8392c98246ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -431,6 +431,7 @@ jobs: needs: [job_get_metadata, job_build] runs-on: ubuntu-latest strategy: + fail-fast: true matrix: bundle: - esm From 2d957a5f70e265811c4a649e611b8e1e997805bd Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 21 Oct 2022 21:35:57 +0200 Subject: [PATCH 6/8] Couple of minor fixes --- .github/workflows/build.yml | 1 - packages/tracing/src/browser/web-vitals/getCLS.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8392c98246ad..0da365f783f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -431,7 +431,6 @@ jobs: needs: [job_get_metadata, job_build] runs-on: ubuntu-latest strategy: - fail-fast: true matrix: bundle: - esm diff --git a/packages/tracing/src/browser/web-vitals/getCLS.ts b/packages/tracing/src/browser/web-vitals/getCLS.ts index 43d9ea19d91e..e96d2f098ccd 100644 --- a/packages/tracing/src/browser/web-vitals/getCLS.ts +++ b/packages/tracing/src/browser/web-vitals/getCLS.ts @@ -76,7 +76,9 @@ export const onCLS = (onReport: ReportCallback, opts: ReportOpts = {}): void => if (sessionValue > metric.value) { metric.value = sessionValue; metric.entries = sessionEntries; - report(); + if (report) { + report(); + } } } }); From fa42977a2a89cb5da8e72ccbab3c872d18d0a5ad Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 21 Oct 2022 22:48:13 +0200 Subject: [PATCH 7/8] Fix `longtask` bug --- packages/tracing/src/browser/metrics/index.ts | 29 ++++++++++--------- .../src/browser/web-vitals/lib/observe.ts | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/tracing/src/browser/metrics/index.ts b/packages/tracing/src/browser/metrics/index.ts index f6e4d28e0e1f..29a94908defc 100644 --- a/packages/tracing/src/browser/metrics/index.ts +++ b/packages/tracing/src/browser/metrics/index.ts @@ -9,7 +9,7 @@ import { onCLS } from '../web-vitals/getCLS'; import { onFID } from '../web-vitals/getFID'; import { onLCP } from '../web-vitals/getLCP'; import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher'; -import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe'; +import { observe } from '../web-vitals/lib/observe'; import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types'; import { _startChild, isMeasurementValue } from './utils'; @@ -42,19 +42,22 @@ export function startTrackingWebVitals(reportAllChanges: boolean = false): void * Start tracking long tasks. */ export function startTrackingLongTasks(): void { - const entryHandler: PerformanceEntryHandler = (entry: PerformanceEntry): void => { - const transaction = getActiveTransaction() as IdleTransaction | undefined; - if (!transaction) { - return; + const entryHandler = (entries: PerformanceEntry[]): void => { + for (const entry of entries) { + const transaction = getActiveTransaction() as IdleTransaction | undefined; + if (!transaction) { + return; + } + const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); + const duration = msToSec(entry.duration); + + transaction.startChild({ + description: 'Main UI thread blocked', + op: 'ui.long-task', + startTimestamp: startTime, + endTimestamp: startTime + duration, + }); } - const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); - const duration = msToSec(entry.duration); - transaction.startChild({ - description: 'Main UI thread blocked', - op: 'ui.long-task', - startTimestamp: startTime, - endTimestamp: startTime + duration, - }); }; observe('longtask', entryHandler); diff --git a/packages/tracing/src/browser/web-vitals/lib/observe.ts b/packages/tracing/src/browser/web-vitals/lib/observe.ts index 98cb57dac2e8..d3f6a0f14153 100644 --- a/packages/tracing/src/browser/web-vitals/lib/observe.ts +++ b/packages/tracing/src/browser/web-vitals/lib/observe.ts @@ -28,7 +28,7 @@ interface PerformanceEntryMap { 'first-input': PerformanceEventTiming[] | FirstInputPolyfillEntry[]; navigation: PerformanceNavigationTiming[] | NavigationTimingPolyfillEntry[]; resource: PerformanceResourceTiming[]; - longtask: PerformanceEntry; + longtask: PerformanceEntry[]; } /** From b76eb0349d56da542ff8166e231ef558d6efc035 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 21 Oct 2022 23:21:34 +0200 Subject: [PATCH 8/8] Copy 3.0.4 bug fix and update readme --- packages/tracing/src/browser/web-vitals/README.md | 7 +++++-- packages/tracing/src/browser/web-vitals/getLCP.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/tracing/src/browser/web-vitals/README.md b/packages/tracing/src/browser/web-vitals/README.md index 9270917e337d..09add37239aa 100644 --- a/packages/tracing/src/browser/web-vitals/README.md +++ b/packages/tracing/src/browser/web-vitals/README.md @@ -2,9 +2,9 @@ > 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: v2.1.0 +This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.0.4 -The commit SHA used is: [3f3338d994f182172d5b97b22a0fcce0c2846908](https://github.com/GoogleChrome/web-vitals/tree/3f3338d994f182172d5b97b22a0fcce0c2846908) +The commit SHA used is: [7f0ed0bfb03c356e348a558a3eda111b498a2a11](https://github.com/GoogleChrome/web-vitals/tree/7f0ed0bfb03c356e348a558a3eda111b498a2a11) Current vendored web vitals are: @@ -23,6 +23,9 @@ As such, logic around `BFCache` and multiple reports were removed from the libra ## CHANGELOG +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 diff --git a/packages/tracing/src/browser/web-vitals/getLCP.ts b/packages/tracing/src/browser/web-vitals/getLCP.ts index 0953af1dd3c9..286f0aa1b80b 100644 --- a/packages/tracing/src/browser/web-vitals/getLCP.ts +++ b/packages/tracing/src/browser/web-vitals/getLCP.ts @@ -47,7 +47,7 @@ export const onLCP = (onReport: ReportCallback, opts: ReportOpts = {}): void => // 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. - const value = lastEntry.startTime - getActivationStart(); + const value = Math.max(lastEntry.startTime - getActivationStart(), 0); // Only report if the page wasn't hidden prior to LCP. if (value < visibilityWatcher.firstHiddenTime) {