From fd8b0c3a59089c080fdb3ed9297a9e73867f4963 Mon Sep 17 00:00:00 2001 From: Brendan Kenny Date: Fri, 3 May 2024 11:36:17 -0500 Subject: [PATCH] Small type clean up for v4 (#471) * use union of metric types more broadly * add attribution in return type * revert Metric name * attribution object * update readme * object.assign attributions * Move isInvalidTimestamp into getNavigationEntry * Explicitly type attribution return values * Remove missed file * Update README * Apply suggestions from code review Co-authored-by: Barry Pollard * Remove src files links in README * Fix README error * Add one more note to README --------- Co-authored-by: Philip Walton Co-authored-by: Philip Walton Co-authored-by: Barry Pollard --- README.md | 129 +++++++++++++++++++--------------- src/attribution/onCLS.ts | 33 +++++---- src/attribution/onFCP.ts | 47 ++++++------- src/attribution/onFID.ts | 27 +++---- src/attribution/onINP.ts | 43 ++++++------ src/attribution/onLCP.ts | 51 ++++++-------- src/attribution/onTTFB.ts | 45 ++++++------ src/lib/getNavigationEntry.ts | 24 +++++-- src/lib/isInvalidTimestamp.ts | 25 ------- src/onCLS.ts | 12 ++-- src/onFCP.ts | 14 ++-- src/onFID.ts | 8 ++- src/onINP.ts | 14 ++-- src/onLCP.ts | 12 ++-- src/onTTFB.ts | 27 ++++--- src/types/base.ts | 37 ++++------ src/types/cls.ts | 14 ---- src/types/fcp.ts | 14 ---- src/types/fid.ts | 14 ---- src/types/inp.ts | 14 ---- src/types/lcp.ts | 14 ---- src/types/ttfb.ts | 14 ---- 22 files changed, 271 insertions(+), 361 deletions(-) delete mode 100644 src/lib/isInvalidTimestamp.ts diff --git a/README.md b/README.md index a585321e..2119358c 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 [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. @@ -492,6 +492,8 @@ For guidance on how to collect and use real-user data to debug performance issue #### `Metric` +All metrics types inherit from the following base interface: + ```ts interface Metric { /** @@ -532,7 +534,7 @@ 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[]; /** * The type of navigation. @@ -558,36 +560,61 @@ 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) +##### `CLSMetric` -#### `MetricWithAttribution` +```ts +interface CLSMetric extends Metric { + name: 'CLS'; + entries: LayoutShift[]; +} +``` -See the [attribution build](#attribution-build) section for details on how to use this feature. +##### `FCPMetric` ```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}; +interface FCPMetric extends Metric { + name: 'FCP'; + entries: PerformancePaintTiming[]; } ``` -Metric-specific subclasses: +##### `FIDMetric` -- [`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) +_This interface is deprecated and will be removed in next major release_ + +```ts +interface FIDMetric extends Metric { + name: 'FID'; + entries: PerformanceEventTiming[]; +} +``` + +##### `INPMetric` + +```ts +interface INPMetric extends Metric { + name: 'INP'; + entries: PerformanceEventTiming[]; +} +``` + +##### `LCPMetric` + +```ts +interface LCPMetric extends Metric { + name: 'LCP'; + entries: LargestContentfulPaint[]; +} +``` + +##### `TTFBMetric` + +```ts +interface TTFBMetric extends Metric { + name: 'TTFB'; + entries: PerformanceNavigationTiming[]; +} +``` #### `MetricRatingThresholds` @@ -604,28 +631,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 +677,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,17 +689,17 @@ _**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). #### `onFID()` -_Deprecated and will be removed in next major release_ +_This function is 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 +709,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 +723,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 +733,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 +773,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`: +#### `CLSAttribution` ```ts interface CLSAttribution { @@ -809,7 +824,7 @@ interface CLSAttribution { } ``` -#### FCP `attribution`: +#### `FCPAttribution` ```ts interface FCPAttribution { @@ -841,7 +856,9 @@ interface FCPAttribution { } ``` -#### FID `attribution`: +#### `FIDAttribution` + +_This interface is deprecated and will be removed in next major release_ ```ts interface FIDAttribution { @@ -873,7 +890,7 @@ interface FIDAttribution { } ``` -#### INP `attribution`: +#### `INPAttribution` ```ts interface INPAttribution { @@ -964,7 +981,7 @@ interface INPAttribution { } ``` -#### LCP `attribution`: +#### `LCPAttribution` ```ts interface LCPAttribution { @@ -1019,7 +1036,7 @@ interface LCPAttribution { } ``` -#### TTFB `attribution`: +#### `TTFBAttribution` ```ts export interface TTFBAttribution { diff --git a/src/attribution/onCLS.ts b/src/attribution/onCLS.ts index 453e25b6..52e2bee4 100644 --- a/src/attribution/onCLS.ts +++ b/src/attribution/onCLS.ts @@ -18,8 +18,7 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getSelector} from '../lib/getSelector.js'; import {onCLS as unattributedOnCLS} from '../onCLS.js'; import { - CLSReportCallback, - CLSReportCallbackWithAttribution, + CLSAttribution, CLSMetric, CLSMetricWithAttribution, ReportOpts, @@ -33,13 +32,16 @@ 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 => { + // 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) { - (metric as CLSMetricWithAttribution).attribution = { + attribution = { largestShiftTarget: getSelector(largestSource.node), largestShiftTime: largestEntry.startTime, largestShiftValue: largestEntry.value, @@ -47,12 +49,16 @@ const attributeCLS = (metric: CLSMetric): void => { largestShiftEntry: largestEntry, loadState: getLoadState(largestEntry.startTime), }; - return; } } } - // Set an empty object if no other attribution has been set. - (metric as CLSMetricWithAttribution).attribution = {}; + + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution: CLSMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); + return metricWithAttribution; }; /** @@ -77,14 +83,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) => { + const metricWithAttribution = attributeCLS(metric); + onReport(metricWithAttribution); + }, opts); }; diff --git a/src/attribution/onFCP.ts b/src/attribution/onFCP.ts index 54f23110..079bf91a 100644 --- a/src/attribution/onFCP.ts +++ b/src/attribution/onFCP.ts @@ -17,44 +17,46 @@ 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, FCPMetric, FCPMetricWithAttribution, - FCPReportCallback, - FCPReportCallbackWithAttribution, ReportOpts, } from '../types.js'; -const attributeFCP = (metric: FCPMetric): void => { +const attributeFCP = (metric: FCPMetric): 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(); const fcpEntry = metric.entries[metric.entries.length - 1]; if (navigationEntry) { - const responseStart = navigationEntry.responseStart; - if (isInvalidTimestamp(responseStart)) return; - const activationStart = navigationEntry.activationStart || 0; - const ttfb = Math.max(0, responseStart - activationStart); + const ttfb = Math.max(0, navigationEntry.responseStart - activationStart); - (metric as FCPMetricWithAttribution).attribution = { + attribution = { timeToFirstByte: ttfb, firstByteToFCP: metric.value - ttfb, loadState: getLoadState(metric.entries[0].startTime), navigationEntry, fcpEntry, }; - return; } } - // Set an empty object if no other attribution has been set. - (metric as FCPMetricWithAttribution).attribution = { - timeToFirstByte: 0, - firstByteToFCP: metric.value, - loadState: getLoadState(getBFCacheRestoreTime()), - }; + + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution: FCPMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); + return metricWithAttribution; }; /** @@ -64,14 +66,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) => { + const metricWithAttribution = attributeFCP(metric); + onReport(metricWithAttribution); + }, opts); }; diff --git a/src/attribution/onFID.ts b/src/attribution/onFID.ts index 138c92e6..ed561429 100644 --- a/src/attribution/onFID.ts +++ b/src/attribution/onFID.ts @@ -18,22 +18,28 @@ import {getLoadState} from '../lib/getLoadState.js'; import {getSelector} from '../lib/getSelector.js'; import {onFID as unattributedOnFID} from '../onFID.js'; import { + FIDAttribution, FIDMetric, FIDMetricWithAttribution, - FIDReportCallback, - FIDReportCallbackWithAttribution, ReportOpts, } from '../types.js'; -const attributeFID = (metric: FIDMetric): void => { +const attributeFID = (metric: FIDMetric): FIDMetricWithAttribution => { const fidEntry = metric.entries[0]; - (metric as FIDMetricWithAttribution).attribution = { + const attribution: FIDAttribution = { eventTarget: getSelector(fidEntry.target), eventType: fidEntry.name, eventTime: fidEntry.startTime, eventEntry: fidEntry, loadState: getLoadState(fidEntry.startTime), }; + + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution: FIDMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); + return metricWithAttribution; }; /** @@ -46,14 +52,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) => { + const metricWithAttribution = attributeFID(metric); + onReport(metricWithAttribution); + }, opts); }; diff --git a/src/attribution/onINP.ts b/src/attribution/onINP.ts index 3b6920c2..3c103fe2 100644 --- a/src/attribution/onINP.ts +++ b/src/attribution/onINP.ts @@ -24,10 +24,9 @@ import {observe} from '../lib/observe.js'; import {whenIdle} from '../lib/whenIdle.js'; import {onINP as unattributedOnINP} from '../onINP.js'; import { + INPAttribution, INPMetric, INPMetricWithAttribution, - INPReportCallback, - INPReportCallbackWithAttribution, ReportOpts, } from '../types.js'; @@ -192,7 +191,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)!; @@ -226,7 +225,7 @@ const attributeINP = (metric: INPMetric): void => { const nextPaintTime = Math.max.apply(Math, nextPaintTimeCandidates); - (metric as INPMetricWithAttribution).attribution = { + const attribution: INPAttribution = { interactionTarget: getSelector( firstEntryWithTarget && firstEntryWithTarget.target, ), @@ -240,6 +239,13 @@ const attributeINP = (metric: INPMetric): void => { presentationDelay: Math.max(nextPaintTime - processingEnd, 0), loadState: getLoadState(firstEntry.startTime), }; + + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution: INPMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); + return metricWithAttribution; }; /** @@ -270,25 +276,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(() => { + const metricWithAttribution = attributeINP(metric); + onReport(metricWithAttribution); + }); + }, opts); }; diff --git a/src/attribution/onLCP.ts b/src/attribution/onLCP.ts index 5b174368..285f319a 100644 --- a/src/attribution/onLCP.ts +++ b/src/attribution/onLCP.ts @@ -16,25 +16,26 @@ 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, LCPMetric, LCPMetricWithAttribution, - LCPReportCallback, - LCPReportCallbackWithAttribution, ReportOpts, } from '../types.js'; -const attributeLCP = (metric: LCPMetric) => { +const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => { + // Use a default object if no other attribution has been set. + let attribution: LCPAttribution = { + timeToFirstByte: 0, + resourceLoadDelay: 0, + resourceLoadDuration: 0, + elementRenderDelay: metric.value, + }; + if (metric.entries.length) { const navigationEntry = getNavigationEntry(); - if (navigationEntry) { - const responseStart = navigationEntry.responseStart; - if (isInvalidTimestamp(responseStart)) return; - const activationStart = navigationEntry.activationStart || 0; const lcpEntry = metric.entries[metric.entries.length - 1]; const lcpResourceEntry = @@ -43,7 +44,7 @@ const attributeLCP = (metric: LCPMetric) => { .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, @@ -62,7 +63,7 @@ const attributeLCP = (metric: LCPMetric) => { lcpEntry.startTime - activationStart, ); - const attribution: LCPAttribution = { + attribution = { element: getSelector(lcpEntry.element), timeToFirstByte: ttfb, resourceLoadDelay: lcpRequestStart - ttfb, @@ -79,18 +80,15 @@ const attributeLCP = (metric: LCPMetric) => { if (lcpResourceEntry) { attribution.lcpResourceEntry = lcpResourceEntry; } - - (metric as LCPMetricWithAttribution).attribution = attribution; - return; } } - // Set an empty object if no other attribution has been set. - (metric as LCPMetricWithAttribution).attribution = { - timeToFirstByte: 0, - resourceLoadDelay: 0, - resourceLoadDuration: 0, - elementRenderDelay: metric.value, - }; + + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution: LCPMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); + return metricWithAttribution; }; /** @@ -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) => { + const metricWithAttribution = attributeLCP(metric); + onReport(metricWithAttribution); + }, opts); }; diff --git a/src/attribution/onTTFB.ts b/src/attribution/onTTFB.ts index 7c1876e4..1cfd74bc 100644 --- a/src/attribution/onTTFB.ts +++ b/src/attribution/onTTFB.ts @@ -18,12 +18,20 @@ import {onTTFB as unattributedOnTTFB} from '../onTTFB.js'; import { TTFBMetric, TTFBMetricWithAttribution, - TTFBReportCallback, - TTFBReportCallbackWithAttribution, ReportOpts, + TTFBAttribution, } from '../types.js'; -const attributeTTFB = (metric: TTFBMetric): void => { +const attributeTTFB = (metric: TTFBMetric): 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]; const activationStart = navigationEntry.activationStart || 0; @@ -49,7 +57,7 @@ const attributeTTFB = (metric: TTFBMetric): void => { 0, ); - (metric as TTFBMetricWithAttribution).attribution = { + attribution = { waitingDuration: waitEnd, cacheDuration: dnsStart - waitEnd, // dnsEnd usually equals connectStart but use connectStart over dnsEnd @@ -63,16 +71,14 @@ const attributeTTFB = (metric: TTFBMetric): void => { requestDuration: metric.value - connectEnd, navigationEntry: navigationEntry, }; - return; } - // Set an empty object if no other attribution has been set. - (metric as TTFBMetricWithAttribution).attribution = { - waitingDuration: 0, - cacheDuration: 0, - dnsDuration: 0, - connectionDuration: 0, - requestDuration: 0, - }; + + // Use Object.assign to set property to keep tsc happy. + const metricWithAttribution: TTFBMetricWithAttribution = Object.assign( + metric, + {attribution}, + ); + return metricWithAttribution; }; /** @@ -91,14 +97,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) => { + const metricWithAttribution = attributeTTFB(metric); + onReport(metricWithAttribution); + }, opts); }; 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/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(); -}; 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 02352209..9dbd79af 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 || {}; diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 5944bfb8..8167a568 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -16,14 +16,9 @@ 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, - 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 +55,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 || {}; @@ -73,20 +71,19 @@ export const onTTFB = (onReport: TTFBReportCallback, opts?: ReportOpts) => { ); 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 diff --git a/src/types/base.ts b/src/types/base.ts index 95c0a44f..9d574e90 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -14,12 +14,12 @@ * 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 { /** @@ -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[]; /** * 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..c79ce4c0 100644 --- a/src/types/cls.ts +++ b/src/types/cls.ts @@ -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..ef599b34 100644 --- a/src/types/fcp.ts +++ b/src/types/fcp.ts @@ -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..5b2dcba3 100644 --- a/src/types/fid.ts +++ b/src/types/fid.ts @@ -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..d469be47 100644 --- a/src/types/inp.ts +++ b/src/types/inp.ts @@ -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..4761fdd1 100644 --- a/src/types/lcp.ts +++ b/src/types/lcp.ts @@ -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..3559084d 100644 --- a/src/types/ttfb.ts +++ b/src/types/ttfb.ts @@ -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; -}