Skip to content

Commit

Permalink
Address review feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
philipwalton committed Jul 14, 2022
1 parent d844692 commit 7b04ae6
Show file tree
Hide file tree
Showing 19 changed files with 233 additions and 69 deletions.
92 changes: 79 additions & 13 deletions README.md
Expand Up @@ -11,6 +11,7 @@
- [Send the results to an analytics endpoint](#send-the-results-to-an-analytics-endpoint)
- [Send the results to Google Analytics](#send-the-results-to-google-analytics)
- [Send the results to Google Tag Manager](#send-the-results-to-google-tag-manager)
- [Send attribution data](#send-attribution-data)
- [Batch multiple reports together](#batch-multiple-reports-together)
- [Bundle versions](#bundle-versions)
- [Which bundle is right for you?](#which-bundle-is-right-for-you)
Expand Down Expand Up @@ -123,6 +124,8 @@ Measuring the Web Vitals scores for your real users is a great first step toward

The "attribution" build helps you do that by including additional diagnostic information with each metric to help you identify the root cause of poor performance as well as prioritize the most important things to fix.

The "attribution" build is slightly larger than the "standard" build (by about 500 bytes, brotli'd), so while the code size is still small, it's only recommended if you're actually using these features.

To load the "attribution" build, change any `import` statements that reference `web-vitals` to `web-vitals/attribution`:

```diff
Expand All @@ -132,15 +135,15 @@ To load the "attribution" build, change any `import` statements that reference `

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.

See the [`attribution` reference](#attribution) for details on what values are added for each metric.
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.

<a name="load-web-vitals-from-a-cdn"><a>

### From a CDN

The recommended way to use the `web-vitals` package is to install it from npm and integrate it into your build process. However, if you're not using npm, it's still possible to use `web-vitals` by requesting it from a CDN that serves npm package files.

The following examples show how to load `web-vitals` from [unpkg.com](https://unpkg.com), whether your targeting just Chromium-based browsers (using the "standard" version) or additional browsers (using the "base+polyfill" version):
The following examples show how to load `web-vitals` from [unpkg.com](https://unpkg.com), whether you're targeting just Chromium-based browsers (using the "standard" version) or additional browsers (using the "base+polyfill" version):

_**Important!** users who want to load version 3 beta from the unpkg CDN should specify a version number or link to the [web-vitals@next](https://unpkg.com/web-vitals@next?module) tag._

Expand Down Expand Up @@ -335,7 +338,7 @@ function sendToGoogleAnalytics({name, delta, id}) {
// Use `sendBeacon()` if the browser supports it.
transport: 'beacon',

// OPTIONAL: any additional params or debug info here.
// OPTIONAL: any additional attribution params here.
// See: https://web.dev/debug-web-vitals-in-the-field/
// dimension1: '...',
// dimension2: '...',
Expand Down Expand Up @@ -370,7 +373,7 @@ function sendToGoogleAnalytics({name, delta, id}) {
// Use a non-interaction event to avoid affecting bounce rate.
non_interaction: true,

// OPTIONAL: any additional params or debug info here.
// OPTIONAL: any additional attribution params here.
// See: https://web.dev/debug-web-vitals-in-the-field/
// metric_rating: 'good' | 'ni' | 'poor',
// debug_info: '...',
Expand Down Expand Up @@ -420,6 +423,46 @@ The recommended way to measure Web Vitals metrics with Google Tag Manager is usi

For full installation and usage instructions, see Simo's post: [Track Core Web Vitals in GA4 with Google Tag Manager](https://www.simoahava.com/analytics/track-core-web-vitals-in-ga4-with-google-tag-manager/).

### Send attribution data

When using the [attribution build](#attribution-build), you can send additional data to help you debug _why_ the metric values are they way they are.

This example sends an additional `debug_target` param to Google Analytics, corresponding to the element most associated with each metric.

```js
import {onCLS, onFID, onLCP} from 'web-vitals/attribution';

function sendToGoogleAnalytics({name, delta, value, id, attribution}) {
const eventParams = {
// Built-in params:
value: delta, // Use `delta` so the value can be summed.
// Custom params:
metric_id: id, // Needed to aggregate events.
metric_value: value, // Optional.
metric_delta: delta, // Optional.
}

switch (name) {
case 'CLS':
eventParams.debug_target = attribution.largestShiftTarget;
case 'FID':
eventParams.debug_target = attribution.eventTarget;
case 'LCP':
eventParams.debug_target = attribution.element;
}

// Assumes the global `gtag()` function exists, see:
// https://developers.google.com/analytics/devguides/collection/ga4
gtag('event', name, eventParams);
}

onCLS(sendToGoogleAnalytics);
onFID(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
```

See [Debug Web Vitals in the field](https://web.dev/debug-web-vitals-in-the-field/) for more information and examples.

### Batch multiple reports together

Rather than reporting each individual Web Vitals metric separately, you can minimize your network usage by batching multiple metric reports together in a single network request.
Expand Down Expand Up @@ -850,15 +893,19 @@ interface FCPAttribution {
*/
timeToFirstByte: number;
/**
* The time between TTFB and the first contentful paint (FCP).
* The delta between TTFB and the first contentful paint (FCP).
*/
renderDelay: number;
firstByteToFCP: number;
/**
* The loading state of the document at the time when FCP `occurred (see
* `LoadState` for details). Ideally, documents can paint before they finish
* loading (e.g. the `loading` or `domInteractive` phases).
*/
loadState: LoadState,
/**
* The `PerformancePaintTiming` entry corresponding to FCP.
*/
fcpEntry?: PerformancePaintTiming,
/**
* The `navigation` entry of the current page, which is useful for diagnosing
* general page load issues.
Expand All @@ -885,6 +932,11 @@ interface FIDAttribution {
* The `type` of the `event` dispatched from the user interaction.
*/
eventType: string;
/**
* The `PerformanceEventTiming` entry corresponding to FID (or the
* polyfill entry in browsers that don't support Event Timing).
*/
eventEntry: PerformanceEventTiming | FirstInputPolyfillEntry,
/**
* The loading state of the document at the time when the first interaction
* occurred (see `LoadState` for details). If the first interaction occurred
Expand All @@ -900,22 +952,27 @@ interface FIDAttribution {
```ts
interface INPAttribution {
/**
* A selector identifying the element that the user interacted with. This
* element will be the `target` of the `event` dispatched.
* A selector identifying the element that the user interacted with for
* the event corresponding to INP. This element will be the `target` of the
* `event` dispatched.
*/
eventTarget?: string;
/**
* The time when the user interacted. This time will match the `timeStamp`
* value of the `event` dispatched.
* The time when the user interacted for the event corresponding to INP.
* This time will match the `timeStamp` value of the `event` dispatched.
*/
eventTime?: number;
/**
* The `type` of the `event` dispatched from the user interaction.
* The `type` of the `event` dispatched corresponding to INP.
*/
eventType?: string;
/**
* The loading state of the document at the time when the interaction
* occurred (see `LoadState` for details). If the interaction occurred
* The `PerformanceEventTiming` entry corresponding to INP.
*/
eventEntry?: PerformanceEventTiming;
/**
* The loading state of the document at the time when the even corresponding
* to INP occurred (see `LoadState` for details). If the interaction occurred
* while the document was loading and executing script (e.g. usually in the
* `domInteractive` phase) it can result in long delays.
*/
Expand Down Expand Up @@ -965,6 +1022,10 @@ interface LCPAttribution {
* for diagnosing resource load issues.
*/
lcpResourceEntry?: PerformanceResourceTiming;
/**
* The `LargestContentfulPaint` entry corresponding to LCP.
*/
lcpEntry?: LargestContentfulPaint;
}
```

Expand Down Expand Up @@ -992,6 +1053,11 @@ interface TTFBAttribution {
* processing time.
*/
requestTime: number;
/**
* The `PerformanceNavigationTiming` entry used to determine TTFB (or the
* polyfill entry in browsers that don't support Navigation Timing).
*/
navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry;
}
```

Expand Down
6 changes: 4 additions & 2 deletions src/attribution/onFCP.ts
Expand Up @@ -24,23 +24,25 @@ import {FCPMetricWithAttribution, FCPReportCallback, FCPReportCallbackWithAttrib
const attributeFCP = (metric: FCPMetricWithAttribution): void => {
if (metric.entries.length) {
const navigationEntry = getNavigationEntry();
const fcpEntry = metric.entries[metric.entries.length - 1];

if (navigationEntry) {
const activationStart = navigationEntry.activationStart || 0;
const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);

metric.attribution = {
timeToFirstByte: ttfb,
renderDelay: metric.value - ttfb,
firstByteToFCP: metric.value - ttfb,
loadState: getLoadState(metric.entries[0].startTime),
navigationEntry,
fcpEntry,
};
}
} else {
// There are no entries when restored from bfcache.
metric.attribution = {
timeToFirstByte: 0,
renderDelay: metric.value,
firstByteToFCP: metric.value,
loadState: getLoadState(getBFCacheRestoreTime()),
};
}
Expand Down
1 change: 1 addition & 0 deletions src/attribution/onFID.ts
Expand Up @@ -26,6 +26,7 @@ const attributeFID = (metric: FIDMetricWithAttribution): void => {
eventTarget: getSelector(fidEntry.target),
eventType: fidEntry.name,
eventTime: fidEntry.startTime,
eventEntry: fidEntry,
loadState: getLoadState(fidEntry.startTime),
} as FIDAttribution;
};
Expand Down
1 change: 1 addition & 0 deletions src/attribution/onINP.ts
Expand Up @@ -32,6 +32,7 @@ const attributeINP = (metric: INPMetricWithAttribution): void => {
eventTarget: getSelector(longestEntry.target),
eventType: longestEntry.name,
eventTime: longestEntry.startTime,
eventEntry: longestEntry,
loadState: getLoadState(longestEntry.startTime),
};
} else {
Expand Down
1 change: 1 addition & 0 deletions src/attribution/onLCP.ts
Expand Up @@ -56,6 +56,7 @@ const attributeLCP = (metric: LCPMetricWithAttribution): void => {
elementRenderDelay: lcpRenderTime - lcpResponseEnd,
navigationEntry,
lcpResourceEntry,
lcpEntry,
};
}
} else {
Expand Down
14 changes: 9 additions & 5 deletions src/attribution/onTTFB.ts
Expand Up @@ -20,18 +20,22 @@ import {TTFBMetricWithAttribution, TTFBReportCallback, TTFBReportCallbackWithAtt

const attributeTTFB = (metric: TTFBMetricWithAttribution): void => {
if (metric.entries.length) {
const navEntry = metric.entries[0];
const activationStart = navEntry.activationStart || 0;
const navigationEntry = metric.entries[0];
const activationStart = navigationEntry.activationStart || 0;

const dnsStart = Math.max(0, navEntry.domainLookupStart - activationStart);
const connectStart = Math.max(0, navEntry.connectStart - activationStart);
const requestStart = Math.max(0, navEntry.requestStart - activationStart);
const dnsStart = Math.max(
navigationEntry.domainLookupStart - activationStart, 0);
const connectStart = Math.max(
navigationEntry.connectStart - activationStart, 0);
const requestStart = Math.max(
navigationEntry.requestStart - activationStart, 0);

metric.attribution = {
waitingTime: dnsStart,
dnsTime: connectStart - dnsStart,
connectionTime: requestStart - connectStart,
requestTime: metric.value - requestStart,
navigationEntry: navigationEntry,
};
} else {
metric.attribution = {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/initMetric.ts
Expand Up @@ -25,7 +25,7 @@ export const initMetric = (name: Metric['name'], value?: number): Metric => {
const navEntry = getNavigationEntry();
let navigationType: Metric['navigationType'];

if (getBFCacheRestoreTime() > 0) {
if (getBFCacheRestoreTime() >= 0) {
navigationType = 'back_forward_cache';
} else if (navEntry) {
if (document.prerendering || getActivationStart() > 0) {
Expand Down
8 changes: 6 additions & 2 deletions src/types/fcp.ts
Expand Up @@ -38,15 +38,19 @@ export interface FCPMetric extends Metric {
*/
timeToFirstByte: number;
/**
* The time between TTFB and the first contentful paint (FCP).
* The delta between TTFB and the first contentful paint (FCP).
*/
renderDelay: number;
firstByteToFCP: number;
/**
* The loading state of the document at the time when FCP `occurred (see
* `LoadState` for details). Ideally, documents can paint before they finish
* loading (e.g. the `loading` or `domInteractive` phases).
*/
loadState: LoadState,
/**
* The `PerformancePaintTiming` entry corresponding to FCP.
*/
fcpEntry?: PerformancePaintTiming,
/**
* The `navigation` entry of the current page, which is useful for diagnosing
* general page load issues.
Expand Down
5 changes: 5 additions & 0 deletions src/types/fid.ts
Expand Up @@ -46,6 +46,11 @@ export interface FIDAttribution {
* The `type` of the `event` dispatched from the user interaction.
*/
eventType: string;
/**
* The `PerformanceEventTiming` entry corresponding to FID (or the
* polyfill entry in browsers that don't support Event Timing).
*/
eventEntry: PerformanceEventTiming | FirstInputPolyfillEntry,
/**
* The loading state of the document at the time when the first interaction
* occurred (see `LoadState` for details). If the first interaction occurred
Expand Down
19 changes: 12 additions & 7 deletions src/types/inp.ts
Expand Up @@ -32,22 +32,27 @@ export interface INPMetric extends Metric {
*/
export interface INPAttribution {
/**
* A selector identifying the element that the user interacted with. This
* element will be the `target` of the `event` dispatched.
* A selector identifying the element that the user interacted with for
* the event corresponding to INP. This element will be the `target` of the
* `event` dispatched.
*/
eventTarget?: string;
/**
* The time when the user interacted. This time will match the `timeStamp`
* value of the `event` dispatched.
* The time when the user interacted for the event corresponding to INP.
* This time will match the `timeStamp` value of the `event` dispatched.
*/
eventTime?: number;
/**
* The `type` of the `event` dispatched from the user interaction.
* The `type` of the `event` dispatched corresponding to INP.
*/
eventType?: string;
/**
* The loading state of the document at the time when the interaction
* occurred (see `LoadState` for details). If the interaction occurred
* The `PerformanceEventTiming` entry corresponding to INP.
*/
eventEntry?: PerformanceEventTiming;
/**
* The loading state of the document at the time when the even corresponding
* to INP occurred (see `LoadState` for details). If the interaction occurred
* while the document was loading and executing script (e.g. usually in the
* `domInteractive` phase) it can result in long delays.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/types/lcp.ts
Expand Up @@ -70,6 +70,10 @@ export interface LCPAttribution {
* for diagnosing resource load issues.
*/
lcpResourceEntry?: PerformanceResourceTiming;
/**
* The `LargestContentfulPaint` entry corresponding to LCP.
*/
lcpEntry?: LargestContentfulPaint;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/types/ttfb.ts
Expand Up @@ -52,6 +52,11 @@ export interface TTFBMetric extends Metric {
* processing time.
*/
requestTime: number;
/**
* The `PerformanceNavigationTiming` entry used to determine TTFB (or the
* polyfill entry in browsers that don't support Navigation Timing).
*/
navigationEntry?: PerformanceNavigationTiming | NavigationTimingPolyfillEntry;
}

/**
Expand Down

0 comments on commit 7b04ae6

Please sign in to comment.