From 6c2c8fbe50d7d1f49bfa2f9176987f365e973789 Mon Sep 17 00:00:00 2001 From: Ethan Gardner Date: Fri, 29 Nov 2024 10:40:50 -0500 Subject: [PATCH] add adr and ttfb collection --- documents/adr/001-web-vitals.md | 17 +++++ package-lock.json | 63 ---------------- src/format-event-data.ts | 18 ++++- src/index.ts | 3 +- src/send-to-analytics.ts | 4 +- src/web-vitals.ts | 4 +- test/unit/format-event-data.test.ts | 111 ++++++++++++++++++++++++++++ 7 files changed, 151 insertions(+), 69 deletions(-) create mode 100644 documents/adr/001-web-vitals.md diff --git a/documents/adr/001-web-vitals.md b/documents/adr/001-web-vitals.md new file mode 100644 index 0000000..18d4436 --- /dev/null +++ b/documents/adr/001-web-vitals.md @@ -0,0 +1,17 @@ +# 001-web-vitals + +## Status + +Approved + +## Context + +There is good adoption of synthetic performance testing tools, particularly Google Lighthouse and [Page Speed Insights](https://pagespeed.web.dev). There are not as many widely deployed tools to anonymously collect field measurements of page speed performance from real users in a way that adequately captures their experience when using federal web properties. + +## Decision + +Google Analytics through the Digital Analytics Program within the federal government already has good adoption within the federal government. The `web-vitals.js` library from Google was selected as a way to instrument a website or application with performance collection. This library was chosen for its stability, as well as its completeness, as it is the basis of many commercial offerings (e.g. DataDog, Sentry) and its wide deployment through the Chrome Devtools Performance Panel. + +## Consequences + +This proposes an optional add-on for teams to be able to add and collect metrics on what a real user experiences as it relates to the universal performance indicators known as Core Web Vitals. diff --git a/package-lock.json b/package-lock.json index e1f69d7..273bfc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@vitest/coverage-v8": "^2.1.5", "@web/dev-server": "^0.4.6", "@web/test-runner": "^0.18.2", - "@web/test-runner-playwright": "^0.11.0", "concurrently": "^8.2.2", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -2748,21 +2747,6 @@ "node": ">=18.0.0" } }, - "node_modules/@web/test-runner-playwright": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-0.11.0.tgz", - "integrity": "sha512-s+f43DSAcssKYVOD9SuzueUcctJdHzq1by45gAnSCKa9FQcaTbuYe8CzmxA21g+NcL5+ayo4z+MA9PO4H+PssQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@web/test-runner-core": "^0.13.0", - "@web/test-runner-coverage-v8": "^0.8.0", - "playwright": "^1.22.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@web/test-runner/node_modules/@web/config-loader": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.3.2.tgz", @@ -8493,53 +8477,6 @@ "node": ">= 6" } }, - "node_modules/playwright": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", - "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.49.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", - "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", diff --git a/src/format-event-data.ts b/src/format-event-data.ts index d8dffa2..22f6f9f 100644 --- a/src/format-event-data.ts +++ b/src/format-event-data.ts @@ -3,14 +3,16 @@ import type { FCPAttribution, INPAttribution, LCPAttribution, + TTFBAttribution, } from 'web-vitals'; -export type WebVitalsName = 'CLS' | 'FCP' | 'INP' | 'LCP'; +export type WebVitalsName = 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB'; export type WebVitalsAttribution = | CLSAttribution | FCPAttribution | INPAttribution - | LCPAttribution; + | LCPAttribution + | TTFBAttribution; export const formatEventData = ( name: WebVitalsName, @@ -69,6 +71,18 @@ export const formatEventData = ( debug_target: (attribution as LCPAttribution).element || '(not set)', }; } + if (name === 'TTFB') { + return { + debug_waiting_duration: (attribution as TTFBAttribution) + .waitingDuration, + debug_dns_duration: (attribution as TTFBAttribution).dnsDuration, + debug_connection_duration: (attribution as TTFBAttribution) + .connectionDuration, + debug_cache_duration: (attribution as TTFBAttribution).cacheDuration, + debug_request_duration: (attribution as TTFBAttribution) + .requestDuration, + }; + } } // Return default/empty params in case there is no attribution. return { diff --git a/src/index.ts b/src/index.ts index 48e86e4..3dbb6b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { onCLS, onINP, onFCP, onLCP } from './web-vitals.js'; +import { onCLS, onINP, onFCP, onLCP, onTTFB } from './web-vitals.js'; import { sendToAnalytics } from './send-to-analytics.js'; const initWebVitalsEvents = () => { @@ -6,6 +6,7 @@ const initWebVitalsEvents = () => { onFCP(sendToAnalytics); onINP(sendToAnalytics); onLCP(sendToAnalytics); + onTTFB(sendToAnalytics); }; if (typeof window !== 'undefined' && 'gas4' in window) { diff --git a/src/send-to-analytics.ts b/src/send-to-analytics.ts index d7f3601..762a276 100644 --- a/src/send-to-analytics.ts +++ b/src/send-to-analytics.ts @@ -3,6 +3,7 @@ import type { FCPMetricWithAttribution, INPMetricWithAttribution, LCPMetricWithAttribution, + TTFBMetricWithAttribution, } from 'web-vitals'; import { formatEventData, type WebVitalsName } from './format-event-data.js'; @@ -11,7 +12,8 @@ export type WebVitalsWithAttribution = | CLSMetricWithAttribution | FCPMetricWithAttribution | INPMetricWithAttribution - | LCPMetricWithAttribution; + | LCPMetricWithAttribution + | TTFBMetricWithAttribution; declare const gas4: any; diff --git a/src/web-vitals.ts b/src/web-vitals.ts index 0365733..dba4a00 100644 --- a/src/web-vitals.ts +++ b/src/web-vitals.ts @@ -1,3 +1,3 @@ -import { onCLS, onINP, onFCP, onLCP } from 'web-vitals/attribution'; +import { onCLS, onINP, onFCP, onLCP, onTTFB } from 'web-vitals/attribution'; -export { onCLS, onINP, onFCP, onLCP }; +export { onCLS, onINP, onFCP, onLCP, onTTFB }; diff --git a/test/unit/format-event-data.test.ts b/test/unit/format-event-data.test.ts index bf56b96..9d636a0 100644 --- a/test/unit/format-event-data.test.ts +++ b/test/unit/format-event-data.test.ts @@ -84,6 +84,27 @@ describe('formatEventData', () => { }); }); + it('should format TTFB data correctly', () => { + const attribution = { + waitingDuration: 0, + cacheDuration: 0, + dnsDuration: 0, + connectionDuration: 2015, + requestDuration: 47, + }; + + // @ts-ignore + const result = formatEventData('TTFB', attribution); + + expect(result).toEqual({ + debug_cache_duration: attribution.cacheDuration, + debug_connection_duration: attribution.connectionDuration, + debug_dns_duration: attribution.dnsDuration, + debug_request_duration: attribution.requestDuration, + debug_waiting_duration: attribution.waitingDuration, + }); + }); + it('should return default/empty params if no attribution data is provided', () => { // @ts-ignore const result = formatEventData('unknown', null); @@ -453,3 +474,93 @@ describe('formatEventData', () => { // } // } // } + +// { +// "name": "TTFB", +// "value": 2062, +// "rating": "poor", +// "delta": 2062, +// "entries": [ +// { +// "name": "http://localhost:8000/demo/", +// "entryType": "navigation", +// "startTime": 0, +// "duration": 4181, +// "initiatorType": "navigation", +// "nextHopProtocol": "http/1.1", +// "workerStart": 0, +// "redirectStart": 0, +// "redirectEnd": 0, +// "fetchStart": -21, +// "domainLookupStart": -21, +// "domainLookupEnd": -21, +// "connectStart": -21, +// "connectEnd": 2015, +// "secureConnectionStart": 0, +// "requestStart": 2016, +// "responseStart": 2062, +// "responseEnd": 2062, +// "transferSize": 4277, +// "encodedBodySize": 3977, +// "decodedBodySize": 3977, +// "responseStatus": 200, +// "contentType": "text/html", +// "serverTiming": [], +// "unloadEventStart": 0, +// "unloadEventEnd": 0, +// "domInteractive": 2126, +// "domContentLoadedEventStart": 2199, +// "domContentLoadedEventEnd": 2200, +// "domComplete": 4180, +// "loadEventStart": 4180, +// "loadEventEnd": 4181, +// "type": "navigate", +// "redirectCount": 0 +// } +// ], +// "id": "v4-1732826806920-8943990242578", +// "navigationType": "navigate", +// "attribution": { +// "waitingDuration": 0, +// "cacheDuration": 0, +// "dnsDuration": 0, +// "connectionDuration": 2015, +// "requestDuration": 47, +// "navigationEntry": { +// "name": "http://localhost:8000/demo/", +// "entryType": "navigation", +// "startTime": 0, +// "duration": 4181, +// "initiatorType": "navigation", +// "nextHopProtocol": "http/1.1", +// "workerStart": 0, +// "redirectStart": 0, +// "redirectEnd": 0, +// "fetchStart": -21, +// "domainLookupStart": -21, +// "domainLookupEnd": -21, +// "connectStart": -21, +// "connectEnd": 2015, +// "secureConnectionStart": 0, +// "requestStart": 2016, +// "responseStart": 2062, +// "responseEnd": 2062, +// "transferSize": 4277, +// "encodedBodySize": 3977, +// "decodedBodySize": 3977, +// "responseStatus": 200, +// "contentType": "text/html", +// "serverTiming": [], +// "unloadEventStart": 0, +// "unloadEventEnd": 0, +// "domInteractive": 2126, +// "domContentLoadedEventStart": 2199, +// "domContentLoadedEventEnd": 2200, +// "domComplete": 4180, +// "loadEventStart": 4180, +// "loadEventEnd": 4181, +// "type": "navigate", +// "redirectCount": 0 +// } +// } +// }