From 8fa200aff08e4c79e3fa31169d1b757c6be254f0 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Wed, 1 May 2024 00:36:15 -0500 Subject: [PATCH 01/14] use union of metric types more broadly --- src/attribution/onCLS.ts | 21 ++++++--------------- src/attribution/onFCP.ts | 21 ++++++--------------- src/attribution/onFID.ts | 21 ++++++--------------- src/attribution/onINP.ts | 37 ++++++++++++++----------------------- src/attribution/onLCP.ts | 15 +++++---------- src/attribution/onTTFB.ts | 21 ++++++--------------- src/onCLS.ts | 12 +++++------- src/onFCP.ts | 14 ++++++-------- src/onFID.ts | 8 +++++--- src/onINP.ts | 14 ++++++-------- src/onLCP.ts | 14 ++++++-------- src/onTTFB.ts | 11 +++++------ src/types/base.ts | 39 ++++++++++++++++----------------------- src/types/cls.ts | 18 ++---------------- src/types/fcp.ts | 18 ++---------------- src/types/fid.ts | 18 ++---------------- src/types/inp.ts | 18 ++---------------- src/types/lcp.ts | 18 ++---------------- src/types/ttfb.ts | 18 ++---------------- 19 files changed, 104 insertions(+), 252 deletions(-) diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index 453e25b6..5fca98cb 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -17,13 +17,7 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getSelector} from '../lib/getSelector.js'; import {onCLS as unattributedOnCLS} from '../onCLS.js'; -import { - CLSReportCallback, - CLSReportCallbackWithAttribution, - CLSMetric, - CLSMetricWithAttribution, - ReportOpts, -} from '../types.js'; +import {CLSMetric, CLSMetricWithAttribution, ReportOpts} from '../types.js'; const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => { return entries.reduce((a, b) => (a && a.value > b.value ? a : b)); @@ -77,14 +71,11 @@ const attributeCLS = (metric: CLSMetric): void => { * during the same page load._ */ export const onCLS = ( - onReport: CLSReportCallbackWithAttribution, + onReport: (metric: CLSMetricWithAttribution) => void, opts?: ReportOpts, ) => { - unattributedOnCLS( - ((metric: CLSMetricWithAttribution) => { - attributeCLS(metric); - onReport(metric); - }) as CLSReportCallback, - opts, - ); + unattributedOnCLS((metric: CLSMetric) => { + attributeCLS(metric); + onReport(metric as CLSMetricWithAttribution); + }, opts); }; diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 54f23110..649f96ed 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -19,13 +19,7 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getNavigationEntry} from '../lib/getNavigationEntry.js'; import {isInvalidTimestamp} from '../lib/isInvalidTimestamp.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; -import { - FCPMetric, - FCPMetricWithAttribution, - FCPReportCallback, - FCPReportCallbackWithAttribution, - ReportOpts, -} from '../types.js'; +import {FCPMetric, FCPMetricWithAttribution, ReportOpts} from '../types.js'; const attributeFCP = (metric: FCPMetric): void => { if (metric.entries.length) { @@ -64,14 +58,11 @@ const attributeFCP = (metric: FCPMetric): void => { * value is a `DOMHighResTimeStamp`. */ export const onFCP = ( - onReport: FCPReportCallbackWithAttribution, + onReport: (metric: FCPMetricWithAttribution) => void, opts?: ReportOpts, ) => { - unattributedOnFCP( - ((metric: FCPMetricWithAttribution) => { - attributeFCP(metric); - onReport(metric); - }) as FCPReportCallback, - opts, - ); + unattributedOnFCP((metric: FCPMetric) => { + attributeFCP(metric); + onReport(metric as FCPMetricWithAttribution); + }, opts); }; diff --git a/src/attribution/onFID.ts b/src/attribution/onFID.ts index 138c92e6..a75a2577 100644 --- a/src/attribution/onFID.ts +++ b/src/attribution/onFID.ts @@ -17,13 +17,7 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getSelector} from '../lib/getSelector.js'; import {onFID as unattributedOnFID} from '../onFID.js'; -import { - FIDMetric, - FIDMetricWithAttribution, - FIDReportCallback, - FIDReportCallbackWithAttribution, - ReportOpts, -} from '../types.js'; +import {FIDMetric, FIDMetricWithAttribution, ReportOpts} from '../types.js'; const attributeFID = (metric: FIDMetric): void => { const fidEntry = metric.entries[0]; @@ -46,14 +40,11 @@ const attributeFID = (metric: FIDMetric): void => { * page, it's possible that it will not be reported for some page loads._ */ export const onFID = ( - onReport: FIDReportCallbackWithAttribution, + onReport: (metric: FIDMetricWithAttribution) => void, opts?: ReportOpts, ) => { - unattributedOnFID( - ((metric: FIDMetricWithAttribution) => { - attributeFID(metric); - onReport(metric); - }) as FIDReportCallback, - opts, - ); + unattributedOnFID((metric: FIDMetric) => { + attributeFID(metric); + onReport(metric as FIDMetricWithAttribution); + }, opts); }; diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 3b6920c2..378ad2c2 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -23,13 +23,7 @@ import { import {observe} from '../lib/observe.js'; import {whenIdle} from '../lib/whenIdle.js'; import {onINP as unattributedOnINP} from '../onINP.js'; -import { - INPMetric, - INPMetricWithAttribution, - INPReportCallback, - INPReportCallbackWithAttribution, - ReportOpts, -} from '../types.js'; +import {INPMetric, INPMetricWithAttribution, ReportOpts} from '../types.js'; interface pendingEntriesGroup { startTime: DOMHighResTimeStamp; @@ -270,25 +264,22 @@ const attributeINP = (metric: INPMetric): void => { * during the same page load._ */ export const onINP = ( - onReport: INPReportCallbackWithAttribution, + onReport: (metric: INPMetricWithAttribution) => void, opts?: ReportOpts, ) => { if (!loafObserver) { loafObserver = observe('long-animation-frame', handleLoAFEntries); } - unattributedOnINP( - ((metric: INPMetricWithAttribution) => { - // Queue attribution and reporting 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 would make this no longer necessary. - whenIdle(() => { - attributeINP(metric); - onReport(metric); - }); - }) as INPReportCallback, - opts, - ); + unattributedOnINP((metric: INPMetric) => { + // Queue attribution and reporting 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 would make this no longer necessary. + whenIdle(() => { + attributeINP(metric); + onReport(metric as INPMetricWithAttribution); + }); + }, opts); }; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 5b174368..aeba48fb 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -22,8 +22,6 @@ import { LCPAttribution, LCPMetric, LCPMetricWithAttribution, - LCPReportCallback, - LCPReportCallbackWithAttribution, ReportOpts, } from '../types.js'; @@ -105,14 +103,11 @@ const attributeLCP = (metric: LCPMetric) => { * been determined. */ export const onLCP = ( - onReport: LCPReportCallbackWithAttribution, + onReport: (metric: LCPMetricWithAttribution) => void, opts?: ReportOpts, ) => { - unattributedOnLCP( - ((metric: LCPMetricWithAttribution) => { - attributeLCP(metric); - onReport(metric); - }) as LCPReportCallback, - opts, - ); + unattributedOnLCP((metric: LCPMetric) => { + attributeLCP(metric); + onReport(metric as LCPMetricWithAttribution); + }, opts); }; diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index 7c1876e4..3f267c04 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -15,13 +15,7 @@ */ import {onTTFB as unattributedOnTTFB} from '../onTTFB.js'; -import { - TTFBMetric, - TTFBMetricWithAttribution, - TTFBReportCallback, - TTFBReportCallbackWithAttribution, - ReportOpts, -} from '../types.js'; +import {TTFBMetric, TTFBMetricWithAttribution, ReportOpts} from '../types.js'; const attributeTTFB = (metric: TTFBMetric): void => { if (metric.entries.length) { @@ -91,14 +85,11 @@ const attributeTTFB = (metric: TTFBMetric): void => { * and server processing time. */ export const onTTFB = ( - onReport: TTFBReportCallbackWithAttribution, + onReport: (metric: TTFBMetricWithAttribution) => void, opts?: ReportOpts, ) => { - unattributedOnTTFB( - ((metric: TTFBMetricWithAttribution) => { - attributeTTFB(metric); - onReport(metric); - }) as TTFBReportCallback, - opts, - ); + unattributedOnTTFB((metric: TTFBMetric) => { + attributeTTFB(metric); + onReport(metric as TTFBMetricWithAttribution); + }, opts); }; diff --git a/src/onCLS.ts b/src/onCLS.ts index ee369752..6f78458b 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -22,12 +22,7 @@ import {doubleRAF} from './lib/doubleRAF.js'; import {onHidden} from './lib/onHidden.js'; import {runOnce} from './lib/runOnce.js'; import {onFCP} from './onFCP.js'; -import { - CLSMetric, - CLSReportCallback, - MetricRatingThresholds, - ReportOpts, -} from './types.js'; +import {CLSMetric, MetricRatingThresholds, ReportOpts} from './types.js'; /** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; @@ -53,7 +48,10 @@ export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25]; * hidden. As a result, the `callback` function might be called multiple times * during the same page load._ */ -export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { +export const onCLS = ( + onReport: (metric: CLSMetric) => void, + opts?: ReportOpts, +) => { // Set defaults opts = opts || {}; diff --git a/src/onFCP.ts b/src/onFCP.ts index 60cfce66..f2a3625a 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -22,12 +22,7 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {whenActivated} from './lib/whenActivated.js'; -import { - FCPMetric, - FCPReportCallback, - MetricRatingThresholds, - ReportOpts, -} from './types.js'; +import {FCPMetric, MetricRatingThresholds, ReportOpts} from './types.js'; /** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */ export const FCPThresholds: MetricRatingThresholds = [1800, 3000]; @@ -38,7 +33,10 @@ export const FCPThresholds: MetricRatingThresholds = [1800, 3000]; * relevant `paint` performance entry used to determine the value. The reported * value is a `DOMHighResTimeStamp`. */ -export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { +export const onFCP = ( + onReport: (metric: FCPMetric) => void, + opts?: ReportOpts, +) => { // Set defaults opts = opts || {}; @@ -48,7 +46,7 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { let report: ReturnType; const handleEntries = (entries: FCPMetric['entries']) => { - (entries as PerformancePaintTiming[]).forEach((entry) => { + entries.forEach((entry) => { if (entry.name === 'first-contentful-paint') { po!.disconnect(); diff --git a/src/onFID.ts b/src/onFID.ts index e3e3c327..cde84b9d 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -28,7 +28,6 @@ import {runOnce} from './lib/runOnce.js'; import {whenActivated} from './lib/whenActivated.js'; import { FIDMetric, - FIDReportCallback, FirstInputPolyfillCallback, MetricRatingThresholds, ReportOpts, @@ -46,7 +45,10 @@ export const FIDThresholds: MetricRatingThresholds = [100, 300]; * _**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: FIDReportCallback, opts?: ReportOpts) => { +export const onFID = ( + onReport: (metric: FIDMetric) => void, + opts?: ReportOpts, +) => { // Set defaults opts = opts || {}; @@ -65,7 +67,7 @@ export const onFID = (onReport: FIDReportCallback, opts?: ReportOpts) => { }; const handleEntries = (entries: FIDMetric['entries']) => { - (entries as PerformanceEventTiming[]).forEach(handleEntry); + entries.forEach(handleEntry); }; const po = observe('first-input', handleEntries); diff --git a/src/onINP.ts b/src/onINP.ts index 01cb12b5..f8172872 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -28,12 +28,7 @@ import {onHidden} from './lib/onHidden.js'; import {initInteractionCountPolyfill} from './lib/polyfills/interactionCountPolyfill.js'; import {whenActivated} from './lib/whenActivated.js'; -import { - INPMetric, - INPReportCallback, - MetricRatingThresholds, - ReportOpts, -} from './types.js'; +import {INPMetric, MetricRatingThresholds, ReportOpts} from './types.js'; /** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */ export const INPThresholds: MetricRatingThresholds = [200, 500]; @@ -65,7 +60,10 @@ export const INPThresholds: MetricRatingThresholds = [200, 500]; * hidden. As a result, the `callback` function might be called multiple times * during the same page load._ */ -export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { +export const onINP = ( + onReport: (metric: INPMetric) => void, + opts?: ReportOpts, +) => { // Set defaults opts = opts || {}; @@ -96,7 +94,7 @@ export const onINP = (onReport: INPReportCallback, opts?: ReportOpts) => { // just one or two frames is likely not worth the insight that could be // gained. durationThreshold: opts!.durationThreshold ?? DEFAULT_DURATION_THRESHOLD, - } as PerformanceObserverInit); + }); report = bindReporter( onReport, diff --git a/src/onLCP.ts b/src/onLCP.ts index 622c3638..d88803fe 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -25,12 +25,7 @@ import {onHidden} from './lib/onHidden.js'; import {runOnce} from './lib/runOnce.js'; import {whenActivated} from './lib/whenActivated.js'; import {whenIdle} from './lib/whenIdle.js'; -import { - LCPMetric, - MetricRatingThresholds, - LCPReportCallback, - ReportOpts, -} from './types.js'; +import {LCPMetric, MetricRatingThresholds, ReportOpts} from './types.js'; /** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */ export const LCPThresholds: MetricRatingThresholds = [2500, 4000]; @@ -48,7 +43,10 @@ const reportedMetricIDs: Record = {}; * performance entry is dispatched, or once the final value of the metric has * been determined. */ -export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { +export const onLCP = ( + onReport: (metric: LCPMetric) => void, + opts?: ReportOpts, +) => { // Set defaults opts = opts || {}; @@ -58,7 +56,7 @@ export const onLCP = (onReport: LCPReportCallback, opts?: ReportOpts) => { let report: ReturnType; const handleEntries = (entries: LCPMetric['entries']) => { - const lastEntry = entries[entries.length - 1] as LargestContentfulPaint; + const lastEntry = entries[entries.length - 1]; if (lastEntry) { // Only report if the page wasn't hidden prior to LCP. if (lastEntry.startTime < visibilityWatcher.firstHiddenTime) { diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 5944bfb8..a9c19d98 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -19,11 +19,7 @@ import {initMetric} from './lib/initMetric.js'; import {isInvalidTimestamp} from './lib/isInvalidTimestamp.js'; import {onBFCacheRestore} from './lib/bfcache.js'; import {getNavigationEntry} from './lib/getNavigationEntry.js'; -import { - MetricRatingThresholds, - ReportOpts, - TTFBReportCallback, -} from './types.js'; +import {MetricRatingThresholds, ReportOpts, TTFBMetric} from './types.js'; import {getActivationStart} from './lib/getActivationStart.js'; import {whenActivated} from './lib/whenActivated.js'; @@ -60,7 +56,10 @@ const whenReady = (callback: () => void) => { * includes time spent on DNS lookup, connection negotiation, network latency, * and server processing time. */ -export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { +export const onTTFB = ( + onReport: (metric: TTFBMetric) => void, + opts?: ReportOpts, +) => { // Set defaults opts = opts || {}; diff --git a/src/types/base.ts b/src/types/base.ts index 95c0a44f..96806244 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import type {CLSMetric} from './cls.js'; -import type {FCPMetric} from './fcp.js'; -import type {FIDMetric} from './fid.js'; -import type {INPMetric} from './inp.js'; -import type {LCPMetric} from './lcp.js'; -import type {TTFBMetric} from './ttfb.js'; +import type {CLSMetric, CLSMetricWithAttribution} from './cls.js'; +import type {FCPMetric, FCPMetricWithAttribution} from './fcp.js'; +import type {FIDMetric, FIDMetricWithAttribution} from './fid.js'; +import type {INPMetric, INPMetricWithAttribution} from './inp.js'; +import type {LCPMetric, LCPMetricWithAttribution} from './lcp.js'; +import type {TTFBMetric, TTFBMetricWithAttribution} from './ttfb.js'; -export interface Metric { +export interface MetricBase { /** * The name of the metric (in acronym form). */ @@ -60,7 +60,7 @@ export interface Metric { * 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)[]; + entries: PerformanceEntry[] | LayoutShift[]; /** * The type of navigation. @@ -92,17 +92,14 @@ export type MetricType = | LCPMetric | TTFBMetric; -/** - * 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}; -} +/** The union of supported metric attribution types. */ +export type MetricWithAttribution = + | CLSMetricWithAttribution + | FCPMetricWithAttribution + | FIDMetricWithAttribution + | INPMetricWithAttribution + | LCPMetricWithAttribution + | TTFBMetricWithAttribution; /** * The thresholds of metric's "good", "needs improvement", and "poor" ratings. @@ -119,10 +116,6 @@ export interface MetricWithAttribution extends Metric { */ export type MetricRatingThresholds = [number, number]; -export interface ReportCallback { - (metric: MetricType): void; -} - export interface ReportOpts { reportAllChanges?: boolean; durationThreshold?: number; diff --git a/src/types/cls.ts b/src/types/cls.ts index c3398c5f..5df716c3 100644 --- a/src/types/cls.ts +++ b/src/types/cls.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, Metric} from './base.js'; +import type {LoadState, MetricBase} from './base.js'; /** * A CLS-specific version of the Metric object. */ -export interface CLSMetric extends Metric { +export interface CLSMetric extends MetricBase { name: 'CLS'; entries: LayoutShift[]; } @@ -72,17 +72,3 @@ export interface CLSAttribution { export interface CLSMetricWithAttribution extends CLSMetric { attribution: CLSAttribution; } - -/** - * A CLS-specific version of the ReportCallback function. - */ -export interface CLSReportCallback { - (metric: CLSMetric): void; -} - -/** - * A CLS-specific version of the ReportCallback function with attribution. - */ -export interface CLSReportCallbackWithAttribution { - (metric: CLSMetricWithAttribution): void; -} diff --git a/src/types/fcp.ts b/src/types/fcp.ts index 86d48b33..c8660ba6 100644 --- a/src/types/fcp.ts +++ b/src/types/fcp.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, Metric} from './base.js'; +import type {LoadState, MetricBase} from './base.js'; /** * An FCP-specific version of the Metric object. */ -export interface FCPMetric extends Metric { +export interface FCPMetric extends MetricBase { name: 'FCP'; entries: PerformancePaintTiming[]; } @@ -63,17 +63,3 @@ export interface FCPAttribution { export interface FCPMetricWithAttribution extends FCPMetric { attribution: FCPAttribution; } - -/** - * An FCP-specific version of the ReportCallback function. - */ -export interface FCPReportCallback { - (metric: FCPMetric): void; -} - -/** - * An FCP-specific version of the ReportCallback function with attribution. - */ -export interface FCPReportCallbackWithAttribution { - (metric: FCPMetricWithAttribution): void; -} diff --git a/src/types/fid.ts b/src/types/fid.ts index f6b71e75..bfa2a2dd 100644 --- a/src/types/fid.ts +++ b/src/types/fid.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, Metric} from './base.js'; +import type {LoadState, MetricBase} from './base.js'; /** * An FID-specific version of the Metric object. */ -export interface FIDMetric extends Metric { +export interface FIDMetric extends MetricBase { name: 'FID'; entries: PerformanceEventTiming[]; } @@ -63,17 +63,3 @@ export interface FIDAttribution { export interface FIDMetricWithAttribution extends FIDMetric { attribution: FIDAttribution; } - -/** - * An FID-specific version of the ReportCallback function. - */ -export interface FIDReportCallback { - (metric: FIDMetric): void; -} - -/** - * An FID-specific version of the ReportCallback function with attribution. - */ -export interface FIDReportCallbackWithAttribution { - (metric: FIDMetricWithAttribution): void; -} diff --git a/src/types/inp.ts b/src/types/inp.ts index 039ffb26..95136dc1 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, Metric} from './base.js'; +import type {LoadState, MetricBase} from './base.js'; /** * An INP-specific version of the Metric object. */ -export interface INPMetric extends Metric { +export interface INPMetric extends MetricBase { name: 'INP'; entries: PerformanceEventTiming[]; } @@ -122,17 +122,3 @@ export interface INPAttribution { export interface INPMetricWithAttribution extends INPMetric { attribution: INPAttribution; } - -/** - * An INP-specific version of the ReportCallback function. - */ -export interface INPReportCallback { - (metric: INPMetric): void; -} - -/** - * An INP-specific version of the ReportCallback function with attribution. - */ -export interface INPReportCallbackWithAttribution { - (metric: INPMetricWithAttribution): void; -} diff --git a/src/types/lcp.ts b/src/types/lcp.ts index e5e3b3ef..c90e0c3e 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {Metric} from './base.js'; +import type {MetricBase} from './base.js'; /** * An LCP-specific version of the Metric object. */ -export interface LCPMetric extends Metric { +export interface LCPMetric extends MetricBase { name: 'LCP'; entries: LargestContentfulPaint[]; } @@ -86,17 +86,3 @@ export interface LCPAttribution { export interface LCPMetricWithAttribution extends LCPMetric { attribution: LCPAttribution; } - -/** - * An LCP-specific version of the ReportCallback function. - */ -export interface LCPReportCallback { - (metric: LCPMetric): void; -} - -/** - * An LCP-specific version of the ReportCallback function with attribution. - */ -export interface LCPReportCallbackWithAttribution { - (metric: LCPMetricWithAttribution): void; -} diff --git a/src/types/ttfb.ts b/src/types/ttfb.ts index 2f7097c7..bdf3750b 100644 --- a/src/types/ttfb.ts +++ b/src/types/ttfb.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {Metric} from './base.js'; +import type {MetricBase} from './base.js'; /** * A TTFB-specific version of the Metric object. */ -export interface TTFBMetric extends Metric { +export interface TTFBMetric extends MetricBase { name: 'TTFB'; entries: PerformanceNavigationTiming[]; } @@ -76,17 +76,3 @@ export interface TTFBAttribution { export interface TTFBMetricWithAttribution extends TTFBMetric { attribution: TTFBAttribution; } - -/** - * A TTFB-specific version of the ReportCallback function. - */ -export interface TTFBReportCallback { - (metric: TTFBMetric): void; -} - -/** - * A TTFB-specific version of the ReportCallback function with attribution. - */ -export interface TTFBReportCallbackWithAttribution { - (metric: TTFBMetricWithAttribution): void; -} From f563c352771e632794c315d1084f517f2c12d164 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Wed, 1 May 2024 01:47:47 -0500 Subject: [PATCH 02/14] add attribution in return type --- src/attribution/onCLS.ts | 15 +++++++++------ src/attribution/onFCP.ts | 18 +++++++++++------- src/attribution/onFID.ts | 11 +++++++---- src/attribution/onINP.ts | 10 ++++++---- src/attribution/onLCP.ts | 18 +++++++++++------- src/attribution/onTTFB.ts | 15 +++++++++------ 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index 5fca98cb..b10c5a82 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -27,13 +27,15 @@ const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => { return sources.find((s) => s.node && s.node.nodeType === 1) || sources[0]; }; -const attributeCLS = (metric: CLSMetric): void => { +const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { + const metricWithAttribution = metric as CLSMetricWithAttribution; + if (metric.entries.length) { const largestEntry = getLargestLayoutShiftEntry(metric.entries); if (largestEntry && largestEntry.sources && largestEntry.sources.length) { const largestSource = getLargestLayoutShiftSource(largestEntry.sources); if (largestSource) { - (metric as CLSMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { largestShiftTarget: getSelector(largestSource.node), largestShiftTime: largestEntry.startTime, largestShiftValue: largestEntry.value, @@ -41,12 +43,13 @@ const attributeCLS = (metric: CLSMetric): void => { largestShiftEntry: largestEntry, loadState: getLoadState(largestEntry.startTime), }; - return; + return metricWithAttribution; } } } // Set an empty object if no other attribution has been set. - (metric as CLSMetricWithAttribution).attribution = {}; + metricWithAttribution.attribution = {}; + return metricWithAttribution; }; /** @@ -75,7 +78,7 @@ export const onCLS = ( opts?: ReportOpts, ) => { unattributedOnCLS((metric: CLSMetric) => { - attributeCLS(metric); - onReport(metric as CLSMetricWithAttribution); + const metricWithAttribution = attributeCLS(metric); + onReport(metricWithAttribution); }, opts); }; diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 649f96ed..16df0ec7 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -21,34 +21,38 @@ import {isInvalidTimestamp} from '../lib/isInvalidTimestamp.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; import {FCPMetric, FCPMetricWithAttribution, ReportOpts} from '../types.js'; -const attributeFCP = (metric: FCPMetric): void => { +const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { + const metricWithAttribution = metric as FCPMetricWithAttribution; + if (metric.entries.length) { const navigationEntry = getNavigationEntry(); const fcpEntry = metric.entries[metric.entries.length - 1]; if (navigationEntry) { const responseStart = navigationEntry.responseStart; - if (isInvalidTimestamp(responseStart)) return; + // TODO(bckenny): this is wrong. + if (isInvalidTimestamp(responseStart)) return metricWithAttribution; const activationStart = navigationEntry.activationStart || 0; const ttfb = Math.max(0, responseStart - activationStart); - (metric as FCPMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { timeToFirstByte: ttfb, firstByteToFCP: metric.value - ttfb, loadState: getLoadState(metric.entries[0].startTime), navigationEntry, fcpEntry, }; - return; + return metricWithAttribution; } } // Set an empty object if no other attribution has been set. - (metric as FCPMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { timeToFirstByte: 0, firstByteToFCP: metric.value, loadState: getLoadState(getBFCacheRestoreTime()), }; + return metricWithAttribution; }; /** @@ -62,7 +66,7 @@ export const onFCP = ( opts?: ReportOpts, ) => { unattributedOnFCP((metric: FCPMetric) => { - attributeFCP(metric); - onReport(metric as FCPMetricWithAttribution); + const metricWithAttribution = attributeFCP(metric); + onReport(metricWithAttribution); }, opts); }; diff --git a/src/attribution/onFID.ts b/src/attribution/onFID.ts index a75a2577..7a5143e5 100644 --- a/src/attribution/onFID.ts +++ b/src/attribution/onFID.ts @@ -19,15 +19,18 @@ import {getSelector} from '../lib/getSelector.js'; import {onFID as unattributedOnFID} from '../onFID.js'; import {FIDMetric, FIDMetricWithAttribution, ReportOpts} from '../types.js'; -const attributeFID = (metric: FIDMetric): void => { +const attributeFID = (metric: FIDMetric): FIDMetricWithAttribution => { + const metricWithAttribution = metric as FIDMetricWithAttribution; + const fidEntry = metric.entries[0]; - (metric as FIDMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { eventTarget: getSelector(fidEntry.target), eventType: fidEntry.name, eventTime: fidEntry.startTime, eventEntry: fidEntry, loadState: getLoadState(fidEntry.startTime), }; + return metricWithAttribution; }; /** @@ -44,7 +47,7 @@ export const onFID = ( opts?: ReportOpts, ) => { unattributedOnFID((metric: FIDMetric) => { - attributeFID(metric); - onReport(metric as FIDMetricWithAttribution); + const metricWithAttribution = attributeFID(metric); + onReport(metricWithAttribution); }, opts); }; diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 378ad2c2..e1983303 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -186,7 +186,7 @@ const getIntersectingLoAFs = ( return intersectingLoAFs; }; -const attributeINP = (metric: INPMetric): void => { +const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { const firstEntry = metric.entries[0]; const renderTime = entryToRenderTimeMap.get(firstEntry)!; const group = pendingEntriesGroupMap.get(renderTime)!; @@ -220,7 +220,8 @@ const attributeINP = (metric: INPMetric): void => { const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates); - (metric as INPMetricWithAttribution).attribution = { + const metricWithAttribution = metric as INPMetricWithAttribution; + metricWithAttribution.attribution = { interactionTarget: getSelector( firstEntryWithTarget && firstEntryWithTarget.target, ), @@ -234,6 +235,7 @@ const attributeINP = (metric: INPMetric): void => { presentationDelay: Math.max(nextPaintTime - processingEnd, 0), loadState: getLoadState(firstEntry.startTime), }; + return metricWithAttribution; }; /** @@ -278,8 +280,8 @@ export const onINP = ( // running in Chrome (EventTimingKeypressAndCompositionInteractionId) // 123+ that if rolled out fully would make this no longer necessary. whenIdle(() => { - attributeINP(metric); - onReport(metric as INPMetricWithAttribution); + const metricWithAttribution = attributeINP(metric); + onReport(metricWithAttribution); }); }, opts); }; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index aeba48fb..3e5b21b7 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -25,13 +25,16 @@ import { ReportOpts, } from '../types.js'; -const attributeLCP = (metric: LCPMetric) => { +const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { + const metricWithAttribution = metric as LCPMetricWithAttribution; + if (metric.entries.length) { const navigationEntry = getNavigationEntry(); if (navigationEntry) { const responseStart = navigationEntry.responseStart; - if (isInvalidTimestamp(responseStart)) return; + // TODO(bckenny): this is wrong. + if (isInvalidTimestamp(responseStart)) return metricWithAttribution; const activationStart = navigationEntry.activationStart || 0; const lcpEntry = metric.entries[metric.entries.length - 1]; @@ -78,17 +81,18 @@ const attributeLCP = (metric: LCPMetric) => { attribution.lcpResourceEntry = lcpResourceEntry; } - (metric as LCPMetricWithAttribution).attribution = attribution; - return; + metricWithAttribution.attribution = attribution; + return metricWithAttribution; } } // Set an empty object if no other attribution has been set. - (metric as LCPMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { timeToFirstByte: 0, resourceLoadDelay: 0, resourceLoadDuration: 0, elementRenderDelay: metric.value, }; + return metricWithAttribution; }; /** @@ -107,7 +111,7 @@ export const onLCP = ( opts?: ReportOpts, ) => { unattributedOnLCP((metric: LCPMetric) => { - attributeLCP(metric); - onReport(metric as LCPMetricWithAttribution); + const metricWithAttribution = attributeLCP(metric); + onReport(metricWithAttribution); }, opts); }; diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index 3f267c04..51c92a46 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -17,7 +17,9 @@ import {onTTFB as unattributedOnTTFB} from '../onTTFB.js'; import {TTFBMetric, TTFBMetricWithAttribution, ReportOpts} from '../types.js'; -const attributeTTFB = (metric: TTFBMetric): void => { +const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => { + const metricWithAttribution = metric as TTFBMetricWithAttribution; + if (metric.entries.length) { const navigationEntry = metric.entries[0]; const activationStart = navigationEntry.activationStart || 0; @@ -43,7 +45,7 @@ const attributeTTFB = (metric: TTFBMetric): void => { 0, ); - (metric as TTFBMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { waitingDuration: waitEnd, cacheDuration: dnsStart - waitEnd, // dnsEnd usually equals connectStart but use connectStart over dnsEnd @@ -57,16 +59,17 @@ const attributeTTFB = (metric: TTFBMetric): void => { requestDuration: metric.value - connectEnd, navigationEntry: navigationEntry, }; - return; + return metricWithAttribution; } // Set an empty object if no other attribution has been set. - (metric as TTFBMetricWithAttribution).attribution = { + metricWithAttribution.attribution = { waitingDuration: 0, cacheDuration: 0, dnsDuration: 0, connectionDuration: 0, requestDuration: 0, }; + return metricWithAttribution; }; /** @@ -89,7 +92,7 @@ export const onTTFB = ( opts?: ReportOpts, ) => { unattributedOnTTFB((metric: TTFBMetric) => { - attributeTTFB(metric); - onReport(metric as TTFBMetricWithAttribution); + const metricWithAttribution = attributeTTFB(metric); + onReport(metricWithAttribution); }, opts); }; From 06550521307dbe4f3a3ba4550d47cf6535a41c02 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Wed, 1 May 2024 18:09:35 -0500 Subject: [PATCH 03/14] revert Metric name --- src/types/base.ts | 4 ++-- src/types/cls.ts | 4 ++-- src/types/fcp.ts | 4 ++-- src/types/fid.ts | 4 ++-- src/types/inp.ts | 4 ++-- src/types/lcp.ts | 4 ++-- src/types/ttfb.ts | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/types/base.ts b/src/types/base.ts index 96806244..9d574e90 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -21,7 +21,7 @@ import type {INPMetric, INPMetricWithAttribution} from './inp.js'; import type {LCPMetric, LCPMetricWithAttribution} from './lcp.js'; import type {TTFBMetric, TTFBMetricWithAttribution} from './ttfb.js'; -export interface MetricBase { +export interface Metric { /** * The name of the metric (in acronym form). */ @@ -60,7 +60,7 @@ export interface MetricBase { * 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[]; + entries: PerformanceEntry[]; /** * The type of navigation. diff --git a/src/types/cls.ts b/src/types/cls.ts index 5df716c3..c79ce4c0 100644 --- a/src/types/cls.ts +++ b/src/types/cls.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, MetricBase} from './base.js'; +import type {LoadState, Metric} from './base.js'; /** * A CLS-specific version of the Metric object. */ -export interface CLSMetric extends MetricBase { +export interface CLSMetric extends Metric { name: 'CLS'; entries: LayoutShift[]; } diff --git a/src/types/fcp.ts b/src/types/fcp.ts index c8660ba6..ef599b34 100644 --- a/src/types/fcp.ts +++ b/src/types/fcp.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, MetricBase} from './base.js'; +import type {LoadState, Metric} from './base.js'; /** * An FCP-specific version of the Metric object. */ -export interface FCPMetric extends MetricBase { +export interface FCPMetric extends Metric { name: 'FCP'; entries: PerformancePaintTiming[]; } diff --git a/src/types/fid.ts b/src/types/fid.ts index bfa2a2dd..5b2dcba3 100644 --- a/src/types/fid.ts +++ b/src/types/fid.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, MetricBase} from './base.js'; +import type {LoadState, Metric} from './base.js'; /** * An FID-specific version of the Metric object. */ -export interface FIDMetric extends MetricBase { +export interface FIDMetric extends Metric { name: 'FID'; entries: PerformanceEventTiming[]; } diff --git a/src/types/inp.ts b/src/types/inp.ts index 95136dc1..d469be47 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {LoadState, MetricBase} from './base.js'; +import type {LoadState, Metric} from './base.js'; /** * An INP-specific version of the Metric object. */ -export interface INPMetric extends MetricBase { +export interface INPMetric extends Metric { name: 'INP'; entries: PerformanceEventTiming[]; } diff --git a/src/types/lcp.ts b/src/types/lcp.ts index c90e0c3e..4761fdd1 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {MetricBase} from './base.js'; +import type {Metric} from './base.js'; /** * An LCP-specific version of the Metric object. */ -export interface LCPMetric extends MetricBase { +export interface LCPMetric extends Metric { name: 'LCP'; entries: LargestContentfulPaint[]; } diff --git a/src/types/ttfb.ts b/src/types/ttfb.ts index bdf3750b..3559084d 100644 --- a/src/types/ttfb.ts +++ b/src/types/ttfb.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import type {MetricBase} from './base.js'; +import type {Metric} from './base.js'; /** * A TTFB-specific version of the Metric object. */ -export interface TTFBMetric extends MetricBase { +export interface TTFBMetric extends Metric { name: 'TTFB'; entries: PerformanceNavigationTiming[]; } From 535b34e2edee258427f64a3f005a70f09686a023 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Wed, 1 May 2024 19:14:18 -0500 Subject: [PATCH 04/14] attribution object --- src/attribution/onCLS.ts | 19 +++++++++++----- src/attribution/onFCP.ts | 48 ++++++++++++++++++++++----------------- src/attribution/onFID.ts | 15 ++++++++---- src/attribution/onINP.ts | 14 +++++++++--- src/attribution/onLCP.ts | 36 +++++++++++++---------------- src/attribution/onTTFB.ts | 31 +++++++++++++++---------- 6 files changed, 97 insertions(+), 66 deletions(-) diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index b10c5a82..abd0d0cf 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -17,7 +17,12 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getSelector} from '../lib/getSelector.js'; import {onCLS as unattributedOnCLS} from '../onCLS.js'; -import {CLSMetric, CLSMetricWithAttribution, ReportOpts} from '../types.js'; +import { + CLSAttribution, + CLSMetric, + CLSMetricWithAttribution, + ReportOpts, +} from '../types.js'; const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => { return entries.reduce((a, b) => (a && a.value > b.value ? a : b)); @@ -28,14 +33,15 @@ const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => { }; const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { - const metricWithAttribution = metric as CLSMetricWithAttribution; + // Use an empty object if no other attribution has been set. + let attribution: CLSAttribution = {}; if (metric.entries.length) { const largestEntry = getLargestLayoutShiftEntry(metric.entries); if (largestEntry && largestEntry.sources && largestEntry.sources.length) { const largestSource = getLargestLayoutShiftSource(largestEntry.sources); if (largestSource) { - metricWithAttribution.attribution = { + attribution = { largestShiftTarget: getSelector(largestSource.node), largestShiftTime: largestEntry.startTime, largestShiftValue: largestEntry.value, @@ -43,12 +49,13 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { largestShiftEntry: largestEntry, loadState: getLoadState(largestEntry.startTime), }; - return metricWithAttribution; } } } - // Set an empty object if no other attribution has been set. - metricWithAttribution.attribution = {}; + + // Cast to attribution metric so it can be populated. + const metricWithAttribution = metric as CLSMetricWithAttribution; + metricWithAttribution.attribution = attribution; return metricWithAttribution; }; diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 16df0ec7..78790169 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -19,10 +19,20 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getNavigationEntry} from '../lib/getNavigationEntry.js'; import {isInvalidTimestamp} from '../lib/isInvalidTimestamp.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; -import {FCPMetric, FCPMetricWithAttribution, ReportOpts} from '../types.js'; +import { + FCPAttribution, + FCPMetric, + FCPMetricWithAttribution, + ReportOpts, +} from '../types.js'; const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { - const metricWithAttribution = metric as FCPMetricWithAttribution; + // Use a default object if no other attribution has been set. + let attribution: FCPAttribution = { + timeToFirstByte: 0, + firstByteToFCP: metric.value, + loadState: getLoadState(getBFCacheRestoreTime()), + }; if (metric.entries.length) { const navigationEntry = getNavigationEntry(); @@ -30,28 +40,24 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { if (navigationEntry) { const responseStart = navigationEntry.responseStart; - // TODO(bckenny): this is wrong. - if (isInvalidTimestamp(responseStart)) return metricWithAttribution; - - const activationStart = navigationEntry.activationStart || 0; - const ttfb = Math.max(0, responseStart - activationStart); + if (!isInvalidTimestamp(responseStart)) { + const activationStart = navigationEntry.activationStart || 0; + const ttfb = Math.max(0, responseStart - activationStart); - metricWithAttribution.attribution = { - timeToFirstByte: ttfb, - firstByteToFCP: metric.value - ttfb, - loadState: getLoadState(metric.entries[0].startTime), - navigationEntry, - fcpEntry, - }; - return metricWithAttribution; + attribution = { + timeToFirstByte: ttfb, + firstByteToFCP: metric.value - ttfb, + loadState: getLoadState(metric.entries[0].startTime), + navigationEntry, + fcpEntry, + }; + } } } - // Set an empty object if no other attribution has been set. - metricWithAttribution.attribution = { - timeToFirstByte: 0, - firstByteToFCP: metric.value, - loadState: getLoadState(getBFCacheRestoreTime()), - }; + + // Cast to attribution metric so it can be populated. + const metricWithAttribution = metric as FCPMetricWithAttribution; + metricWithAttribution.attribution = attribution; return metricWithAttribution; }; diff --git a/src/attribution/onFID.ts b/src/attribution/onFID.ts index 7a5143e5..9dcfd6dc 100644 --- a/src/attribution/onFID.ts +++ b/src/attribution/onFID.ts @@ -17,19 +17,26 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getSelector} from '../lib/getSelector.js'; import {onFID as unattributedOnFID} from '../onFID.js'; -import {FIDMetric, FIDMetricWithAttribution, ReportOpts} from '../types.js'; +import { + FIDAttribution, + FIDMetric, + FIDMetricWithAttribution, + ReportOpts, +} from '../types.js'; const attributeFID = (metric: FIDMetric): FIDMetricWithAttribution => { - const metricWithAttribution = metric as FIDMetricWithAttribution; - const fidEntry = metric.entries[0]; - metricWithAttribution.attribution = { + const attribution: FIDAttribution = { eventTarget: getSelector(fidEntry.target), eventType: fidEntry.name, eventTime: fidEntry.startTime, eventEntry: fidEntry, loadState: getLoadState(fidEntry.startTime), }; + + // Cast to attribution metric so it can be populated. + const metricWithAttribution = metric as FIDMetricWithAttribution; + metricWithAttribution.attribution = attribution; return metricWithAttribution; }; diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index e1983303..91aba61d 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -23,7 +23,12 @@ import { import {observe} from '../lib/observe.js'; import {whenIdle} from '../lib/whenIdle.js'; import {onINP as unattributedOnINP} from '../onINP.js'; -import {INPMetric, INPMetricWithAttribution, ReportOpts} from '../types.js'; +import { + INPAttribution, + INPMetric, + INPMetricWithAttribution, + ReportOpts, +} from '../types.js'; interface pendingEntriesGroup { startTime: DOMHighResTimeStamp; @@ -220,8 +225,7 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates); - const metricWithAttribution = metric as INPMetricWithAttribution; - metricWithAttribution.attribution = { + const attribution: INPAttribution = { interactionTarget: getSelector( firstEntryWithTarget && firstEntryWithTarget.target, ), @@ -235,6 +239,10 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { presentationDelay: Math.max(nextPaintTime - processingEnd, 0), loadState: getLoadState(firstEntry.startTime), }; + + // Cast to attribution metric so it can be populated. + const metricWithAttribution = metric as INPMetricWithAttribution; + metricWithAttribution.attribution = attribution; return metricWithAttribution; }; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 3e5b21b7..cdf56596 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -26,16 +26,18 @@ import { } from '../types.js'; const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { - const metricWithAttribution = metric as LCPMetricWithAttribution; - - if (metric.entries.length) { - const navigationEntry = getNavigationEntry(); - - if (navigationEntry) { - const responseStart = navigationEntry.responseStart; - // TODO(bckenny): this is wrong. - if (isInvalidTimestamp(responseStart)) return metricWithAttribution; + // Use a default object if no other attribution has been set. + let attribution: LCPAttribution = { + timeToFirstByte: 0, + resourceLoadDelay: 0, + resourceLoadDuration: 0, + elementRenderDelay: metric.value, + }; + const navigationEntry = getNavigationEntry(); + if (metric.entries.length && navigationEntry) { + const responseStart = navigationEntry.responseStart; + if (!isInvalidTimestamp(responseStart)) { const activationStart = navigationEntry.activationStart || 0; const lcpEntry = metric.entries[metric.entries.length - 1]; const lcpResourceEntry = @@ -63,7 +65,7 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { lcpEntry.startTime - activationStart, ); - const attribution: LCPAttribution = { + attribution = { element: getSelector(lcpEntry.element), timeToFirstByte: ttfb, resourceLoadDelay: lcpRequestStart - ttfb, @@ -80,18 +82,12 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { if (lcpResourceEntry) { attribution.lcpResourceEntry = lcpResourceEntry; } - - metricWithAttribution.attribution = attribution; - return metricWithAttribution; } } - // Set an empty object if no other attribution has been set. - metricWithAttribution.attribution = { - timeToFirstByte: 0, - resourceLoadDelay: 0, - resourceLoadDuration: 0, - elementRenderDelay: metric.value, - }; + + // Cast to attribution metric so it can be populated. + const metricWithAttribution = metric as LCPMetricWithAttribution; + metricWithAttribution.attribution = attribution; return metricWithAttribution; }; diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index 51c92a46..3c1529b8 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -15,10 +15,22 @@ */ import {onTTFB as unattributedOnTTFB} from '../onTTFB.js'; -import {TTFBMetric, TTFBMetricWithAttribution, ReportOpts} from '../types.js'; +import { + TTFBMetric, + TTFBMetricWithAttribution, + ReportOpts, + TTFBAttribution, +} from '../types.js'; const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => { - const metricWithAttribution = metric as TTFBMetricWithAttribution; + // Use a default object if no other attribution has been set. + let attribution: TTFBAttribution = { + waitingDuration: 0, + cacheDuration: 0, + dnsDuration: 0, + connectionDuration: 0, + requestDuration: 0, + }; if (metric.entries.length) { const navigationEntry = metric.entries[0]; @@ -45,7 +57,7 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => { 0, ); - metricWithAttribution.attribution = { + attribution = { waitingDuration: waitEnd, cacheDuration: dnsStart - waitEnd, // dnsEnd usually equals connectStart but use connectStart over dnsEnd @@ -59,16 +71,11 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => { requestDuration: metric.value - connectEnd, navigationEntry: navigationEntry, }; - return metricWithAttribution; } - // Set an empty object if no other attribution has been set. - metricWithAttribution.attribution = { - waitingDuration: 0, - cacheDuration: 0, - dnsDuration: 0, - connectionDuration: 0, - requestDuration: 0, - }; + + // Cast to attribution metric so it can be populated. + const metricWithAttribution = metric as TTFBMetricWithAttribution; + metricWithAttribution.attribution = attribution; return metricWithAttribution; }; From b7a54f1483029f15fe5e8b5e30c7768875aa2f56 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Thu, 2 May 2024 00:06:33 -0500 Subject: [PATCH 05/14] update readme --- README.md | 159 +++++++++++++++++++++++++++--------------------------- 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index c7daf3f9..646246df 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ To load the "attribution" build, change any `import` statements that reference ` + import {onLCP, onINP, onCLS} from 'web-vitals/attribution'; ``` -Usage for each of the imported function is identical to the standard build, but when importing from the attribution build, the [`Metric`](#metric) object will contain an additional [`attribution`](#metricwithattribution) property. +Usage for each of the imported function is identical to the standard build, but when importing from the attribution build, the [metrics](#metrics) objects will contain an additional [`attribution`](#attribution) property. See [Send attribution data](#send-attribution-data) for usage examples, and the [`attribution` reference](#attribution) for details on what values are added for each metric. @@ -488,17 +488,70 @@ For guidance on how to collect and use real-user data to debug performance issue ## API -### Types: +### Types -#### `Metric` +#### Metrics + +##### [`CLSMetric`](/src/types/cls.ts#:~:text=interface%20CLSMetric) ```ts -interface Metric { - /** - * The name of the metric (in acronym form). - */ - name: 'CLS' | 'FCP' | 'FID' | 'INP' | 'LCP' | 'TTFB'; +interface CLSMetric extends Metric { + name: 'CLS'; + entries: LayoutShift[]; +} +``` + +##### [`FCPMetric`](/src/types/fcp.ts#:~:text=interface%20FCPMetric) + +```ts +interface FCPMetric extends Metric { + name: 'FCP'; + entries: PerformancePaintTiming[]; +} +``` + +##### [`FIDMetric`](/src/types/fid.ts#:~:text=interface%20FIDMetric) + +```ts +interface FIDMetric extends Metric { + name: 'FID'; + entries: PerformanceEventTiming[]; +} +``` + +##### [`INPMetric`](/src/types/inp.ts#:~:text=interface%20INPMetric) + +```ts +interface INPMetric extends Metric { + name: 'INP'; + entries: PerformanceEventTiming[]; +} +``` +##### [`LCPMetric`](/src/types/lcp.ts#:~:text=interface%20LCPMetric) + +```ts +interface LCPMetric extends Metric { + name: 'LCP'; + entries: LargestContentfulPaint[]; +} +``` + +##### [`TTFBMetric`](/src/types/ttfb.ts#:~:text=interface%20TTFBMetric) + +```ts +interface TTFBMetric extends Metric { + name: 'TTFB'; + entries: PerformanceNavigationTiming[]; +} +``` + +##### `Metric` + +All metrics share the following properties: + +```ts +interface Metric { /** * The current value of the metric. */ @@ -527,13 +580,6 @@ interface Metric { */ 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)[]; - /** * The type of navigation. * @@ -556,39 +602,6 @@ interface Metric { } ``` -Metric-specific subclasses: - -- [`CLSMetric`](/src/types/cls.ts#:~:text=interface%20CLSMetric) -- [`FCPMetric`](/src/types/fcp.ts#:~:text=interface%20FCPMetric) -- [`FIDMetric`](/src/types/fid.ts#:~:text=interface%20FIDMetric) -- [`INPMetric`](/src/types/inp.ts#:~:text=interface%20INPMetric) -- [`LCPMetric`](/src/types/lcp.ts#:~:text=interface%20LCPMetric) -- [`TTFBMetric`](/src/types/ttfb.ts#:~:text=interface%20TTFBMetric) - -#### `MetricWithAttribution` - -See the [attribution build](#attribution-build) section for details on how to use this feature. - -```ts -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}; -} -``` - -Metric-specific subclasses: - -- [`CLSMetricWithAttribution`](/src/types/cls.ts#:~:text=interface%20CLSMetricWithAttribution) -- [`FCPMetricWithAttribution`](/src/types/fcp.ts#:~:text=interface%20FCPMetricWithAttribution) -- [`FIDMetricWithAttribution`](/src/types/fid.ts#:~:text=interface%20FIDMetricWithAttribution) -- [`INPMetricWithAttribution`](/src/types/inp.ts#:~:text=interface%20INPMetricWithAttribution) -- [`LCPMetricWithAttribution`](/src/types/lcp.ts#:~:text=interface%20LCPMetricWithAttribution) -- [`TTFBMetricWithAttribution`](/src/types/ttfb.ts#:~:text=interface%20TTFBMetricWithAttribution) - #### `MetricRatingThresholds` The thresholds of metric's "good", "needs improvement", and "poor" ratings. @@ -604,28 +617,11 @@ The thresholds of metric's "good", "needs improvement", and "poor" ratings. | > [1] | "poor" | ```ts -export type MetricRatingThresholds = [number, number]; +type MetricRatingThresholds = [number, number]; ``` _See also [Rating Thresholds](#rating-thresholds)._ -#### `ReportCallback` - -```ts -interface ReportCallback { - (metric: Metric): void; -} -``` - -Metric-specific subclasses: - -- [`CLSReportCallback`](/src/types/cls.ts#:~:text=interface%20CLSReportCallback) -- [`FCPReportCallback`](/src/types/fcp.ts#:~:text=interface%20FCPReportCallback) -- [`FIDReportCallback`](/src/types/fid.ts#:~:text=interface%20FIDReportCallback) -- [`INPReportCallback`](/src/types/inp.ts#:~:text=interface%20INPReportCallback) -- [`LCPReportCallback`](/src/types/lcp.ts#:~:text=interface%20LCPReportCallback) -- [`TTFBReportCallback`](/src/types/ttfb.ts#:~:text=interface%20TTFBReportCallback) - #### `ReportOpts` ```ts @@ -667,7 +663,7 @@ type LoadState = #### `onCLS()` ```ts -type onCLS = (callback: CLSReportCallback, opts?: ReportOpts) => void; +function onCLS(callback: (metric: CLSMetric) => void, opts?: ReportOpts): void; ``` 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](https://heycam.github.io/webidl/#idl-double) (corresponding to a [layout shift score](https://web.dev/articles/cls#layout_shift_score)). @@ -679,7 +675,7 @@ _**Important:** CLS should be continually monitored for changes throughout the e #### `onFCP()` ```ts -type onFCP = (callback: FCPReportCallback, opts?: ReportOpts) => void; +function onFCP(callback: (metric: FCPMetric) => void, opts?: ReportOpts): void; ``` 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`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp). @@ -689,7 +685,7 @@ Calculates the [FCP](https://web.dev/articles/fcp) value for the current page an _Deprecated and will be removed in next major release_ ```ts -type onFID = (callback: FIDReportCallback, opts?: ReportOpts) => void; +function onFID(callback: (metric: FIDMetric) => void, opts?: ReportOpts): void; ``` Calculates the [FID](https://web.dev/articles/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`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp). @@ -699,7 +695,7 @@ _**Important:** since FID is only reported after the user interacts with the pag #### `onINP()` ```ts -type onINP = (callback: INPReportCallback, opts?: ReportOpts) => void; +function onINP(callback: (metric: INPMetric) => void, opts?: ReportOpts): void; ``` 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`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp). @@ -713,7 +709,7 @@ _**Important:** INP should be continually monitored for changes throughout the e #### `onLCP()` ```ts -type onLCP = (callback: LCPReportCallback, opts?: ReportOpts) => void; +function onLCP(callback: (metric: LCPMetric) => void, opts?: ReportOpts): void; ``` 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`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp). @@ -723,7 +719,10 @@ If the `reportAllChanges` [configuration option](#reportopts) is set to `true`, #### `onTTFB()` ```ts -type onTTFB = (callback: TTFBReportCallback, opts?: ReportOpts) => void; +function onTTFB( + callback: (metric: TTFBMetric) => void, + opts?: ReportOpts, +): void; ``` 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`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp). @@ -760,15 +759,17 @@ console.log(INPThresholds); // [ 200, 500 ] console.log(LCPThresholds); // [ 2500, 4000 ] ``` -_**Note:** It's typically not necessary (or recommended) to manually calculate metric value ratings using these thresholds. Use the [`Metric['rating']`](#metric) supplied by the [`ReportCallback`](#reportcallback) functions instead._ +_**Note:** It's typically not necessary (or recommended) to manually calculate metric value ratings using these thresholds. Use the [`Metric['rating']`](#metric) instead._ ### Attribution: The following objects contain potentially-helpful debugging information that can be sent along with the metric values for the current page visit in order to help identify issues happening to real-users in the field. +When using the attribution build, these objects are found as an `attribution` property on each metric. + See the [attribution build](#attribution-build) section for details on how to use this feature. -#### CLS `attribution`: +#### [CLS `attribution`](/src/types/cls.ts#:~:text=interface%20CLSMetricWithAttribution) ```ts interface CLSAttribution { @@ -809,7 +810,7 @@ interface CLSAttribution { } ``` -#### FCP `attribution`: +#### [FCP `attribution`](/src/types/fcp.ts#:~:text=interface%20FCPMetricWithAttribution) ```ts interface FCPAttribution { @@ -841,7 +842,7 @@ interface FCPAttribution { } ``` -#### FID `attribution`: +#### [FID `attribution`](/src/types/fid.ts#:~:text=interface%20FIDMetricWithAttribution) ```ts interface FIDAttribution { @@ -873,7 +874,7 @@ interface FIDAttribution { } ``` -#### INP `attribution`: +#### [INP `attribution`](/src/types/inp.ts#:~:text=interface%20INPMetricWithAttribution) ```ts interface INPAttribution { @@ -964,7 +965,7 @@ interface INPAttribution { } ``` -#### LCP `attribution`: +#### [LCP `attribution`](/src/types/lcp.ts#:~:text=interface%20LCPMetricWithAttribution) ```ts interface LCPAttribution { @@ -1019,7 +1020,7 @@ interface LCPAttribution { } ``` -#### TTFB `attribution`: +#### [TTFB `attribution`](/src/types/ttfb.ts#:~:text=interface%20TTFBMetricWithAttribution) ```ts export interface TTFBAttribution { From 6fa9082379a10656ec562f51f9910ca92e3b3c16 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Thu, 2 May 2024 12:29:38 -0500 Subject: [PATCH 06/14] object.assign attributions --- src/attribution/onCLS.ts | 5 ++--- src/attribution/onFCP.ts | 5 ++--- src/attribution/onFID.ts | 5 ++--- src/attribution/onINP.ts | 5 ++--- src/attribution/onLCP.ts | 5 ++--- src/attribution/onTTFB.ts | 5 ++--- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index abd0d0cf..b4b80af5 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -53,9 +53,8 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { } } - // Cast to attribution metric so it can be populated. - const metricWithAttribution = metric as CLSMetricWithAttribution; - metricWithAttribution.attribution = attribution; + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution = Object.assign(metric, {attribution}); return metricWithAttribution; }; diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 78790169..faa919f3 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -55,9 +55,8 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { } } - // Cast to attribution metric so it can be populated. - const metricWithAttribution = metric as FCPMetricWithAttribution; - metricWithAttribution.attribution = attribution; + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution = Object.assign(metric, {attribution}); return metricWithAttribution; }; diff --git a/src/attribution/onFID.ts b/src/attribution/onFID.ts index 9dcfd6dc..538adbb3 100644 --- a/src/attribution/onFID.ts +++ b/src/attribution/onFID.ts @@ -34,9 +34,8 @@ const attributeFID = (metric: FIDMetric): FIDMetricWithAttribution => { loadState: getLoadState(fidEntry.startTime), }; - // Cast to attribution metric so it can be populated. - const metricWithAttribution = metric as FIDMetricWithAttribution; - metricWithAttribution.attribution = attribution; + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution = Object.assign(metric, {attribution}); return metricWithAttribution; }; diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 91aba61d..338a50e8 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -240,9 +240,8 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { loadState: getLoadState(firstEntry.startTime), }; - // Cast to attribution metric so it can be populated. - const metricWithAttribution = metric as INPMetricWithAttribution; - metricWithAttribution.attribution = attribution; + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution = Object.assign(metric, {attribution}); return metricWithAttribution; }; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index cdf56596..a7280675 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -85,9 +85,8 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { } } - // Cast to attribution metric so it can be populated. - const metricWithAttribution = metric as LCPMetricWithAttribution; - metricWithAttribution.attribution = attribution; + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution = Object.assign(metric, {attribution}); return metricWithAttribution; }; diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index 3c1529b8..5f4487ec 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -73,9 +73,8 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => { }; } - // Cast to attribution metric so it can be populated. - const metricWithAttribution = metric as TTFBMetricWithAttribution; - metricWithAttribution.attribution = attribution; + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution = Object.assign(metric, {attribution}); return metricWithAttribution; }; From da7bd49d2ff2d02738a8fb566f8a62aef3136975 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 2 May 2024 21:06:20 -0700 Subject: [PATCH 07/14] Move isInvalidTimestamp into getNavigationEntry --- src/attribution/onFCP.ts | 22 +++++++++------------- src/attribution/onLCP.ts | 10 ++++------ src/lib/getNavigationEntry.ts | 24 ++++++++++++++++++------ src/onTTFB.ts | 16 +++++++--------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index faa919f3..a17fdef2 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -17,7 +17,6 @@ import {getBFCacheRestoreTime} from '../lib/bfcache.js'; import {getLoadState} from '../lib/getLoadState.js'; import {getNavigationEntry} from '../lib/getNavigationEntry.js'; -import {isInvalidTimestamp} from '../lib/isInvalidTimestamp.js'; import {onFCP as unattributedOnFCP} from '../onFCP.js'; import { FCPAttribution, @@ -39,19 +38,16 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { const fcpEntry = metric.entries[metric.entries.length - 1]; if (navigationEntry) { - const responseStart = navigationEntry.responseStart; - if (!isInvalidTimestamp(responseStart)) { - const activationStart = navigationEntry.activationStart || 0; - const ttfb = Math.max(0, responseStart - activationStart); + const activationStart = navigationEntry.activationStart || 0; + const ttfb = Math.max(0, navigationEntry.responseStart - activationStart); - attribution = { - timeToFirstByte: ttfb, - firstByteToFCP: metric.value - ttfb, - loadState: getLoadState(metric.entries[0].startTime), - navigationEntry, - fcpEntry, - }; - } + attribution = { + timeToFirstByte: ttfb, + firstByteToFCP: metric.value - ttfb, + loadState: getLoadState(metric.entries[0].startTime), + navigationEntry, + fcpEntry, + }; } } diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index a7280675..db732e0f 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -16,7 +16,6 @@ import {getNavigationEntry} from '../lib/getNavigationEntry.js'; import {getSelector} from '../lib/getSelector.js'; -import {isInvalidTimestamp} from '../lib/isInvalidTimestamp.js'; import {onLCP as unattributedOnLCP} from '../onLCP.js'; import { LCPAttribution, @@ -34,10 +33,9 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { elementRenderDelay: metric.value, }; - const navigationEntry = getNavigationEntry(); - if (metric.entries.length && navigationEntry) { - const responseStart = navigationEntry.responseStart; - if (!isInvalidTimestamp(responseStart)) { + if (metric.entries.length) { + const navigationEntry = getNavigationEntry(); + if (navigationEntry) { const activationStart = navigationEntry.activationStart || 0; const lcpEntry = metric.entries[metric.entries.length - 1]; const lcpResourceEntry = @@ -46,7 +44,7 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { .getEntriesByType('resource') .filter((e) => e.name === lcpEntry.url)[0]; - const ttfb = Math.max(0, responseStart - activationStart); + const ttfb = Math.max(0, navigationEntry.responseStart - activationStart); const lcpRequestStart = Math.max( ttfb, diff --git a/src/lib/getNavigationEntry.ts b/src/lib/getNavigationEntry.ts index e575c584..19d18cf5 100644 --- a/src/lib/getNavigationEntry.ts +++ b/src/lib/getNavigationEntry.ts @@ -14,12 +14,24 @@ * limitations under the License. */ -export const getNavigationEntry = (): - | PerformanceNavigationTiming - | undefined => { - return ( +export const getNavigationEntry = (): PerformanceNavigationTiming | void => { + const navigationEntry = self.performance && performance.getEntriesByType && - performance.getEntriesByType('navigation')[0] - ); + performance.getEntriesByType('navigation')[0]; + + // Check to ensure the `responseStart` property is present and valid. + // In some cases no value is reported by the browser (for + // privacy/security reasons), and in other cases (bugs) the value is + // negative or is larger than the current page time. Ignore these cases: + // https://github.com/GoogleChrome/web-vitals/issues/137 + // https://github.com/GoogleChrome/web-vitals/issues/162 + // https://github.com/GoogleChrome/web-vitals/issues/275 + if ( + navigationEntry && + navigationEntry.responseStart > 0 && + navigationEntry.responseStart < performance.now() + ) { + return navigationEntry; + } }; diff --git a/src/onTTFB.ts b/src/onTTFB.ts index a9c19d98..8167a568 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -16,7 +16,6 @@ import {bindReporter} from './lib/bindReporter.js'; import {initMetric} from './lib/initMetric.js'; -import {isInvalidTimestamp} from './lib/isInvalidTimestamp.js'; import {onBFCacheRestore} from './lib/bfcache.js'; import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {MetricRatingThresholds, ReportOpts, TTFBMetric} from './types.js'; @@ -72,20 +71,19 @@ export const onTTFB = ( ); whenReady(() => { - const navEntry = getNavigationEntry(); - - if (navEntry) { - const responseStart = navEntry.responseStart; - - if (isInvalidTimestamp(responseStart)) return; + 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(responseStart - getActivationStart(), 0); + metric.value = Math.max( + navigationEntry.responseStart - getActivationStart(), + 0, + ); - metric.entries = [navEntry]; + metric.entries = [navigationEntry]; report(true); // Only report TTFB after bfcache restores if a `navigation` entry From 06d9217db4dfd042322609235d591f5481e88cf6 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 2 May 2024 21:17:32 -0700 Subject: [PATCH 08/14] Explicitly type attribution return values --- src/attribution/onCLS.ts | 5 ++++- src/attribution/onFCP.ts | 5 ++++- src/attribution/onFID.ts | 5 ++++- src/attribution/onINP.ts | 5 ++++- src/attribution/onLCP.ts | 5 ++++- src/attribution/onTTFB.ts | 5 ++++- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index b4b80af5..52e2bee4 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -54,7 +54,10 @@ const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => { } // Use Object.assign to set property to keep tsc happy. - const metricWithAttribution = Object.assign(metric, {attribution}); + const metricWithAttribution: CLSMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); return metricWithAttribution; }; diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index a17fdef2..079bf91a 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -52,7 +52,10 @@ const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => { } // Use Object.assign to set property to keep tsc happy. - const metricWithAttribution = Object.assign(metric, {attribution}); + const metricWithAttribution: FCPMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); return metricWithAttribution; }; diff --git a/src/attribution/onFID.ts b/src/attribution/onFID.ts index 538adbb3..ed561429 100644 --- a/src/attribution/onFID.ts +++ b/src/attribution/onFID.ts @@ -35,7 +35,10 @@ const attributeFID = (metric: FIDMetric): FIDMetricWithAttribution => { }; // Use Object.assign to set property to keep tsc happy. - const metricWithAttribution = Object.assign(metric, {attribution}); + const metricWithAttribution: FIDMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); return metricWithAttribution; }; diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 338a50e8..3c103fe2 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -241,7 +241,10 @@ const attributeINP = (metric: INPMetric): INPMetricWithAttribution => { }; // Use Object.assign to set property to keep tsc happy. - const metricWithAttribution = Object.assign(metric, {attribution}); + const metricWithAttribution: INPMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); return metricWithAttribution; }; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index db732e0f..285f319a 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -84,7 +84,10 @@ const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { } // Use Object.assign to set property to keep tsc happy. - const metricWithAttribution = Object.assign(metric, {attribution}); + const metricWithAttribution: LCPMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); return metricWithAttribution; }; diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index 5f4487ec..1cfd74bc 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -74,7 +74,10 @@ const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => { } // Use Object.assign to set property to keep tsc happy. - const metricWithAttribution = Object.assign(metric, {attribution}); + const metricWithAttribution: TTFBMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); return metricWithAttribution; }; From 1844113461fd6a6cfaace2b2a4f9ddf0ed2f16c4 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 2 May 2024 21:22:42 -0700 Subject: [PATCH 09/14] Remove missed file --- src/lib/isInvalidTimestamp.ts | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/lib/isInvalidTimestamp.ts diff --git a/src/lib/isInvalidTimestamp.ts b/src/lib/isInvalidTimestamp.ts deleted file mode 100644 index 4311cf71..00000000 --- a/src/lib/isInvalidTimestamp.ts +++ /dev/null @@ -1,25 +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. - */ - -export const isInvalidTimestamp = (timestamp: DOMHighResTimeStamp) => { - // In some cases no value is reported by the browser (for - // privacy/security reasons), and in other cases (bugs) the value is - // negative or is larger than the current page time. Ignore these cases: - // https://github.com/GoogleChrome/web-vitals/issues/137 - // https://github.com/GoogleChrome/web-vitals/issues/162 - // https://github.com/GoogleChrome/web-vitals/issues/275 - return timestamp <= 0 || timestamp > performance.now(); -}; From f3e826147d90de15d32826b1eb09a8d54f660134 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 2 May 2024 21:51:40 -0700 Subject: [PATCH 10/14] Update README --- README.md | 126 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 646246df..b75cbc20 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,75 @@ For guidance on how to collect and use real-user data to debug performance issue ### Types -#### Metrics +#### `Metric` + +All metrics types inherit from the following base interface: + +```ts +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[]; + + /** + * 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'; +} +``` + +Metric-specific subclasses: ##### [`CLSMetric`](/src/types/cls.ts#:~:text=interface%20CLSMetric) @@ -546,62 +614,6 @@ interface TTFBMetric extends Metric { } ``` -##### `Metric` - -All metrics share the following properties: - -```ts -interface Metric { - /** - * 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; - - /** - * 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'; -} -``` - #### `MetricRatingThresholds` The thresholds of metric's "good", "needs improvement", and "poor" ratings. From 1d15a33251ef8c270d6c123bb015e4fc33d355be Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 3 May 2024 06:51:38 -0700 Subject: [PATCH 11/14] Apply suggestions from code review Co-authored-by: Barry Pollard --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b75cbc20..35c58b04 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,7 @@ For guidance on how to collect and use real-user data to debug performance issue ### Types -#### `Metric` +#### [`Metric`](/src/types/base.ts#:~:text=interface%20Metric) All metrics types inherit from the following base interface: @@ -781,7 +781,7 @@ When using the attribution build, these objects are found as an `attribution` pr See the [attribution build](#attribution-build) section for details on how to use this feature. -#### [CLS `attribution`](/src/types/cls.ts#:~:text=interface%20CLSMetricWithAttribution) +#### [CLS `attribution`](/src/types/cls.ts#:~:text=interface%20CLSAttribution) ```ts interface CLSAttribution { @@ -822,7 +822,7 @@ interface CLSAttribution { } ``` -#### [FCP `attribution`](/src/types/fcp.ts#:~:text=interface%20FCPMetricWithAttribution) +#### [FCP `attribution`](/src/types/fcp.ts#:~:text=interface%20FCPAttribution) ```ts interface FCPAttribution { @@ -854,7 +854,7 @@ interface FCPAttribution { } ``` -#### [FID `attribution`](/src/types/fid.ts#:~:text=interface%20FIDMetricWithAttribution) +#### [FID `attribution`](/src/types/fid.ts#:~:text=interface%20FIDAttribution) ```ts interface FIDAttribution { @@ -886,7 +886,7 @@ interface FIDAttribution { } ``` -#### [INP `attribution`](/src/types/inp.ts#:~:text=interface%20INPMetricWithAttribution) +#### [INP `attribution`](/src/types/inp.ts#:~:text=interface%20INPAttribution) ```ts interface INPAttribution { @@ -977,7 +977,7 @@ interface INPAttribution { } ``` -#### [LCP `attribution`](/src/types/lcp.ts#:~:text=interface%20LCPMetricWithAttribution) +#### [LCP `attribution`](/src/types/lcp.ts#:~:text=interface%20LCPAttribution) ```ts interface LCPAttribution { @@ -1032,7 +1032,7 @@ interface LCPAttribution { } ``` -#### [TTFB `attribution`](/src/types/ttfb.ts#:~:text=interface%20TTFBMetricWithAttribution) +#### [TTFB `attribution`](/src/types/ttfb.ts#:~:text=interface%20TTFBAttribution) ```ts export interface TTFBAttribution { From f22e94de5b2baa7ed937de434406559ea840b41b Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 3 May 2024 07:30:39 -0700 Subject: [PATCH 12/14] Remove src files links in README --- README.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 35c58b04..d733f2a7 100644 --- a/README.md +++ b/README.md @@ -488,9 +488,9 @@ For guidance on how to collect and use real-user data to debug performance issue ## API -### Types +### Types: -#### [`Metric`](/src/types/base.ts#:~:text=interface%20Metric) +#### `Metric` All metrics types inherit from the following base interface: @@ -560,7 +560,7 @@ interface Metric { Metric-specific subclasses: -##### [`CLSMetric`](/src/types/cls.ts#:~:text=interface%20CLSMetric) +##### `CLSMetric` ```ts interface CLSMetric extends Metric { @@ -569,7 +569,7 @@ interface CLSMetric extends Metric { } ``` -##### [`FCPMetric`](/src/types/fcp.ts#:~:text=interface%20FCPMetric) +##### `FCPMetric` ```ts interface FCPMetric extends Metric { @@ -578,7 +578,7 @@ interface FCPMetric extends Metric { } ``` -##### [`FIDMetric`](/src/types/fid.ts#:~:text=interface%20FIDMetric) +##### `FIDMetric` ```ts interface FIDMetric extends Metric { @@ -587,7 +587,7 @@ interface FIDMetric extends Metric { } ``` -##### [`INPMetric`](/src/types/inp.ts#:~:text=interface%20INPMetric) +##### `INPMetric` ```ts interface INPMetric extends Metric { @@ -596,7 +596,7 @@ interface INPMetric extends Metric { } ``` -##### [`LCPMetric`](/src/types/lcp.ts#:~:text=interface%20LCPMetric) +##### `LCPMetric` ```ts interface LCPMetric extends Metric { @@ -605,7 +605,7 @@ interface LCPMetric extends Metric { } ``` -##### [`TTFBMetric`](/src/types/ttfb.ts#:~:text=interface%20TTFBMetric) +##### `TTFBMetric` ```ts interface TTFBMetric extends Metric { @@ -781,7 +781,7 @@ When using the attribution build, these objects are found as an `attribution` pr See the [attribution build](#attribution-build) section for details on how to use this feature. -#### [CLS `attribution`](/src/types/cls.ts#:~:text=interface%20CLSAttribution) +#### `CLSAttribution` ```ts interface CLSAttribution { @@ -822,7 +822,7 @@ interface CLSAttribution { } ``` -#### [FCP `attribution`](/src/types/fcp.ts#:~:text=interface%20FCPAttribution) +#### `FCPAttribution` ```ts interface FCPAttribution { @@ -854,7 +854,7 @@ interface FCPAttribution { } ``` -#### [FID `attribution`](/src/types/fid.ts#:~:text=interface%20FIDAttribution) +#### `FIDAttribution` ```ts interface FIDAttribution { @@ -886,7 +886,7 @@ interface FIDAttribution { } ``` -#### [INP `attribution`](/src/types/inp.ts#:~:text=interface%20INPAttribution) +#### `INPAttribution` ```ts interface INPAttribution { @@ -977,7 +977,7 @@ interface INPAttribution { } ``` -#### [LCP `attribution`](/src/types/lcp.ts#:~:text=interface%20LCPAttribution) +#### `LCPAttribution` ```ts interface LCPAttribution { @@ -1032,7 +1032,7 @@ interface LCPAttribution { } ``` -#### [TTFB `attribution`](/src/types/ttfb.ts#:~:text=interface%20TTFBAttribution) +#### `TTFBAttribution` ```ts export interface TTFBAttribution { From 2c7f648dc59ac8233aeac4818fe988aa48be9f27 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 3 May 2024 09:20:31 -0700 Subject: [PATCH 13/14] Fix README error --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a75d54c..8b839d11 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ To load the "attribution" build, change any `import` statements that reference ` + import {onLCP, onINP, onCLS} from 'web-vitals/attribution'; ``` -Usage for each of the imported function is identical to the standard build, but when importing from the attribution build, the [metrics](#metrics) objects will contain an additional [`attribution`](#attribution) property. +Usage for each of the imported function is identical to the standard build, but when importing from the attribution build, the [metric](#metric) objects will contain an additional [`attribution`](#attribution) property. See [Send attribution data](#send-attribution-data) for usage examples, and the [`attribution` reference](#attribution) for details on what values are added for each metric. @@ -580,6 +580,8 @@ interface FCPMetric extends Metric { ##### `FIDMetric` +_This interface is deprecated and will be removed in next major release_ + ```ts interface FIDMetric extends Metric { name: 'FID'; @@ -694,7 +696,7 @@ Calculates the [FCP](https://web.dev/articles/fcp) value for the current page an #### `onFID()` -_Deprecated and will be removed in next major release_ +_This function is deprecated and will be removed in next major release_ ```ts function onFID(callback: (metric: FIDMetric) => void, opts?: ReportOpts): void; From c80d9f3a7e6db031f5b088385174a60a7d1f5858 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 3 May 2024 09:27:37 -0700 Subject: [PATCH 14/14] Add one more note to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8b839d11..2119358c 100644 --- a/README.md +++ b/README.md @@ -858,6 +858,8 @@ interface FCPAttribution { #### `FIDAttribution` +_This interface is deprecated and will be removed in next major release_ + ```ts interface FIDAttribution { /**