diff --git a/package-lock.json b/package-lock.json index 66e302a76..bf90be845 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23469,9 +23469,10 @@ } }, "node_modules/web-vitals": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", - "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==" + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", + "license": "Apache-2.0" }, "node_modules/webidl-conversions": { "version": "7.0.0", @@ -24398,7 +24399,7 @@ }, "packages/sui-bundler": { "name": "@s-ui/bundler", - "version": "9.74.0", + "version": "9.77.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -24889,7 +24890,7 @@ }, "packages/sui-lint": { "name": "@s-ui/lint", - "version": "4.58.0", + "version": "4.59.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -25581,7 +25582,7 @@ "license": "MIT", "dependencies": { "@s-ui/react-hooks": "1", - "web-vitals": "4.2.4" + "web-vitals": "5.2.0" }, "devDependencies": { "@s-ui/test": "8", @@ -25642,7 +25643,7 @@ }, "packages/sui-segment-wrapper": { "name": "@s-ui/segment-wrapper", - "version": "4.36.0", + "version": "4.41.0", "license": "ISC", "dependencies": { "@s-ui/js": "2", diff --git a/packages/sui-react-web-vitals/package.json b/packages/sui-react-web-vitals/package.json index 537b4f274..fd4c70bdd 100644 --- a/packages/sui-react-web-vitals/package.json +++ b/packages/sui-react-web-vitals/package.json @@ -1,6 +1,6 @@ { "name": "@s-ui/react-web-vitals", - "version": "2.9.0", + "version": "3.0.0", "description": "", "type": "module", "main": "lib/index.js", @@ -17,13 +17,13 @@ "author": "", "license": "MIT", "dependencies": { - "web-vitals": "4.2.4", - "@s-ui/react-hooks": "1" + "@s-ui/react-hooks": "1", + "web-vitals": "5.2.0" }, "peerDependencies": { - "react": "16 || 17", "@s-ui/react-context": "1", - "@s-ui/react-router": "1" + "@s-ui/react-router": "1", + "react": "16 || 17" }, "devDependencies": { "@s-ui/test": "8", diff --git a/packages/sui-react-web-vitals/src/index.js b/packages/sui-react-web-vitals/src/index.js index dc03a2fe6..9ba2d2d14 100644 --- a/packages/sui-react-web-vitals/src/index.js +++ b/packages/sui-react-web-vitals/src/index.js @@ -10,7 +10,6 @@ import {useRouter} from '@s-ui/react-router' export const METRICS = { CLS: 'CLS', FCP: 'FCP', - FID: 'FID', INP: 'INP', LCP: 'LCP', TTFB: 'TTFB' @@ -44,7 +43,7 @@ const RATING = { POOR: 'poor' } -const DEFAULT_METRICS_REPORTING_ALL_CHANGES = [METRICS.CLS, METRICS.FID, METRICS.INP, METRICS.LCP] +const DEFAULT_METRICS_REPORTING_ALL_CHANGES = [METRICS.CLS, METRICS.INP, METRICS.LCP] export const DEVICE_TYPES = { DESKTOP: 'desktop', @@ -52,6 +51,34 @@ export const DEVICE_TYPES = { MOBILE: 'mobile' } +const BLINK_BROWSERS = new Set([ + 'Brave', + 'Chrome', + 'Chrome Headless', + 'Chromium', + 'Facebook', + 'MIUI Browser', + 'UCBrowser', + 'Edge' // Only Chromium-based Edge (v79+) uses Blink, but older versions are EOL +]) +const WEBKIT_BROWSERS = new Set(['Android Browser', 'GSA', 'khtml', 'Mobile Safari', 'Safari', 'webkit']) +const GECKO_BROWSERS = new Set(['Firefox', 'Mozilla']) + +/** + * getBrowserEngine determines the browser engine based on the browser name and version, with special handling for iOS and Edge. The order of checks is important to correctly identify the engine: + * - iOS browsers are always classified as WebKit due to Apple's requirements, regardless of their reported name. + * - WebKit browsers are identified by checking if their name is in the WEBKIT_BROWSERS set. + * - Gecko browsers are identified by checking if their name is in the GECKO_BROWSERS set. + * - Blink browsers are identified by checking if their name ends with 'WebView' (common for Android WebViews) or if their name is in the BLINK_BROWSERS set. + * - If none of the above conditions are met, the browser engine is classified as 'Other'. + */ +const getBrowserEngine = ({name, isIOS}) => { + if (isIOS || WEBKIT_BROWSERS.has(name)) return 'WebKit' + if (GECKO_BROWSERS.has(name)) return 'Gecko' + if (name?.endsWith('WebView') || BLINK_BROWSERS.has(name)) return 'Blink' + return 'Other' +} + export default function WebVitalsReporter({ reporter = cwv, children = null, @@ -73,6 +100,7 @@ export default function WebVitalsReporter({ useMount(() => { const {deviceMemory, connection: {effectiveType} = {}, hardwareConcurrency} = window.navigator || {} + const browserEngine = getBrowserEngine(browser || {}) const getRouteid = () => { return route?.id @@ -91,22 +119,9 @@ export default function WebVitalsReporter({ case METRICS.CLS: return attribution.largestShiftTarget case METRICS.LCP: - return attribution.element + return attribution.target default: - return attribution.eventTarget || attribution.interactionTarget - } - } - - const computeINPSubparts = entry => { - // RenderTime is an estimate because duration is rounded and may get rounded down. - // In rare cases, it can be less than processingEnd and that breaks performance.measure(). - // Let's ensure it's at least 4ms in those cases so you can barely see it. - const presentationTime = Math.max(entry.processingEnd + 4, entry.startTime + entry.duration) - - return { - [INP_SUBPARTS.ID]: Math.round(entry.processingStart - entry.startTime, 0), - [INP_SUBPARTS.PT]: Math.round(entry.processingEnd - entry.processingStart, 0), - [INP_SUBPARTS.PD]: Math.round(presentationTime - entry.processingEnd, 0) + return attribution.interactionTarget } } @@ -119,7 +134,7 @@ export default function WebVitalsReporter({ if (!isAllowed || !logger?.cwv || rating === RATING.GOOD) return - const {loadState, eventType} = attribution + const {loadState, interactionType} = attribution logger.cwv({ name: `cwv.${name.toLowerCase()}`, @@ -129,7 +144,7 @@ export default function WebVitalsReporter({ visibilityState: document.visibilityState, ...(routeid && {routeId: routeid}), ...(loadState && {loadState}), - ...(eventType && {eventType}), + ...(interactionType && {eventType: interactionType}), ...(deviceMemory && {deviceMemory}), ...(effectiveType && {effectiveType}), ...(hardwareConcurrency && {hardwareConcurrency}) @@ -181,7 +196,11 @@ export default function WebVitalsReporter({ value: type } ] - : []) + : []), + { + key: 'browserEngine', + value: browserEngine + } ] // Log the main metric @@ -197,12 +216,11 @@ export default function WebVitalsReporter({ ] }) - // Handle INP subparts - if (name === METRICS.INP && entries) { - entries.forEach(entry => { - const metrics = computeINPSubparts(entry) - - Object.keys(metrics).forEach(name => { + // Process and log Metric subparts from a metrics object + const processMetricSubparts = metrics => { + Object.keys(metrics).forEach(name => { + // Only log if we have a non-zero value + if (metrics[name] > 0) { logger.distribution({ name: 'cwv', amount: metrics[name], @@ -214,10 +232,30 @@ export default function WebVitalsReporter({ ...tags ] }) - }) + } }) } + // Handle INP subparts + if (name === METRICS.INP) { + // Helper function to create INP subpart metrics from an object + const extractINPSubparts = source => { + return { + [INP_SUBPARTS.ID]: Math.round(source.inputDelay || 0, 0), + [INP_SUBPARTS.PT]: Math.round(source.processingDuration || 0, 0), + [INP_SUBPARTS.PD]: Math.round(source.presentationDelay || 0, 0) + } + } + + if ( + attribution && + (attribution.inputDelay || attribution.processingDuration || attribution.presentationDelay) + ) { + const metrics = extractINPSubparts(attribution) + processMetricSubparts(metrics) + } + } + // Handle LCP subparts if (name === METRICS.LCP) { // Helper function to create LCP subpart metrics from an object @@ -230,26 +268,6 @@ export default function WebVitalsReporter({ } } - // Process and log LCP subparts from a metrics object - const processLCPSubparts = metrics => { - Object.keys(metrics).forEach(name => { - // Only log if we have a non-zero value - if (metrics[name] > 0) { - logger.distribution({ - name: 'cwv', - amount: metrics[name], - tags: [ - { - key: 'name', - value: name.toLowerCase() - }, - ...tags - ] - }) - } - }) - } - // First check if LCP subparts are in the attribution object if ( attribution && @@ -259,33 +277,19 @@ export default function WebVitalsReporter({ attribution.elementRenderDelay) ) { const metrics = extractLCPSubparts(attribution) - processLCPSubparts(metrics) - } - - // Then check entries as before - if (entries && entries.length > 0) { - entries.forEach(entry => { - if ( - entry && - (entry.timeToFirstByte || - entry.resourceLoadDelay || - entry.resourceLoadDuration || - entry.elementRenderDelay) - ) { - const metrics = extractLCPSubparts(entry) - processLCPSubparts(metrics) - } - }) + processMetricSubparts(metrics) } } } - metrics.forEach(metric => { - reporter[`on${metric}`](handleChange) + metrics + .filter(metric => !!metric && typeof reporter[`on${metric}`] === 'function') + .forEach(metric => { + reporter[`on${metric}`](handleChange) - if (metricsAllChanges.includes(metric)) { - reporter[`on${metric}`](handleAllChanges, {reportAllChanges: true}) - } - }) + if (metricsAllChanges.includes(metric)) { + reporter[`on${metric}`](handleAllChanges, {reportAllChanges: true}) + } + }) }) return children @@ -301,11 +305,11 @@ WebVitalsReporter.propTypes = { */ deviceType: PropTypes.oneOf(Object.values(DEVICE_TYPES)), /** - * An optional array of core web vitals. Choose between: TTFB, LCP, FID, CLS and INP. Defaults to all. + * An optional array of core web vitals. Choose between: TTFB, LCP, CLS and INP. Defaults to all. */ metrics: PropTypes.arrayOf(PropTypes.oneOf(Object.values(METRICS))), /** - * An optional array of core web vitals that will report on all changes. Choose between: TTFB, LCP, FID, CLS and INP. Defaults to LCP and INP. + * An optional array of core web vitals that will report on all changes. Choose between: TTFB, LCP, CLS and INP. Defaults to LCP and INP. */ metricsAllChanges: PropTypes.arrayOf(PropTypes.oneOf(Object.values(METRICS))), /** diff --git a/packages/sui-react-web-vitals/test/browser/indexSpec.js b/packages/sui-react-web-vitals/test/browser/indexSpec.js index 34b605dc2..c939c949c 100644 --- a/packages/sui-react-web-vitals/test/browser/indexSpec.js +++ b/packages/sui-react-web-vitals/test/browser/indexSpec.js @@ -26,13 +26,23 @@ const render = (children, options = {}) => { } describe('WebVitalsReporter', () => { + beforeEach(() => { + sinon.stub(document, 'visibilityState').value('visible') + sinon.stub(window.navigator, 'deviceMemory').value(4) + sinon.stub(window.navigator, 'connection').value({effectiveType: '3g'}) + sinon.stub(window.navigator, 'hardwareConcurrency').value(4) + }) + + afterEach(() => { + sinon.restore() + }) + it('should render content', async () => { render(

Title

) - await screen.findByRole('heading', {name: 'Title'}) }) @@ -43,12 +53,21 @@ describe('WebVitalsReporter', () => { } } const onReport = sinon.spy() - render() - await waitFor(() => [expect(onReport.calledWithMatch({name: 'TTFB', amount: 10, pathname: '/'})).to.be.true]) }) + it('should not track cwv when path is not in allowed list', async () => { + const onReport = sinon.spy() + const reporter = { + onINP: fn => { + fn({name: 'INP', value: 10, entries: [], attribution: {}}) + } + } + render() + await waitFor(() => expect(onReport.called).to.be.false) + }) + it('should track cwv using callback with route id', async () => { const reporter = { onINP: fn => { @@ -57,17 +76,15 @@ describe('WebVitalsReporter', () => { } const onReport = sinon.spy() const routeId = 'home' - render(, { routeId }) - await waitFor(() => [ expect(onReport.calledWithMatch({name: 'TTFB', amount: 10, pathname: '/', routeid: routeId})).to.be.true ]) }) - it('should track cwv using callback with browser', async () => { + it('should track cwv using callback with device type', async () => { const reporter = { onINP: fn => { fn({name: 'TTFB', value: 10}) @@ -75,7 +92,6 @@ describe('WebVitalsReporter', () => { } const onReport = sinon.spy() const deviceType = 'mobile' - render( { onReport={onReport} /> ) - await waitFor(() => [ expect(onReport.calledWithMatch({name: 'TTFB', amount: 10, pathname: '/', type: deviceType})).to.be.true ]) @@ -100,11 +115,9 @@ describe('WebVitalsReporter', () => { const onReport = sinon.spy() const deviceType = 'mobile' const browser = {deviceType} - render(, { browser }) - await waitFor(() => [ expect(onReport.calledWithMatch({name: 'TTFB', amount: 10, pathname: '/', type: deviceType})).to.be.true ]) @@ -119,9 +132,7 @@ describe('WebVitalsReporter', () => { fn({name: 'TTFB', value: 10}) } } - render(, {logger}) - await waitFor(() => [ expect( logger.distribution.calledWith({ @@ -129,7 +140,8 @@ describe('WebVitalsReporter', () => { amount: 10, tags: [ {key: 'name', value: 'ttfb'}, - {key: 'pathname', value: '/'} + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true @@ -146,9 +158,7 @@ describe('WebVitalsReporter', () => { } } const routeId = 'home' - render(, {logger, routeId}) - await waitFor(() => [ expect( logger.distribution.calledWith({ @@ -156,14 +166,15 @@ describe('WebVitalsReporter', () => { amount: 10, tags: [ {key: 'name', value: 'ttfb'}, - {key: 'routeid', value: routeId} + {key: 'routeid', value: routeId}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true ]) }) - it('should track cwv using logger distribution with browser', async () => { + it('should track cwv using logger distribution with device type', async () => { const logger = { distribution: sinon.spy() } @@ -173,11 +184,9 @@ describe('WebVitalsReporter', () => { } } const deviceType = 'mobile' - render(, { logger }) - await waitFor(() => [ expect( logger.distribution.calledWith({ @@ -186,12 +195,60 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'ttfb'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true ]) }) + ;[ + // isIOS forces WebKit regardless of browser name + ({browser: {name: 'Chrome', isIOS: true}, expectedEngine: 'WebKit'}, + // WEBKIT_BROWSERS set + {browser: {name: 'Safari'}, expectedEngine: 'WebKit'}, + {browser: {name: 'Mobile Safari'}, expectedEngine: 'WebKit'}, + {browser: {name: 'Android Browser'}, expectedEngine: 'WebKit'}, + {browser: {name: 'GSA'}, expectedEngine: 'WebKit'}, + // GECKO_BROWSERS set + {browser: {name: 'Firefox'}, expectedEngine: 'Gecko'}, + {browser: {name: 'Mozilla'}, expectedEngine: 'Gecko'}, + // BLINK_BROWSERS set + {browser: {name: 'Chrome'}, expectedEngine: 'Blink'}, + {browser: {name: 'Edge'}, expectedEngine: 'Blink'}, + {browser: {name: 'Chromium'}, expectedEngine: 'Blink'}, + // name ending in 'WebView' + {browser: {name: 'AndroidWebView'}, expectedEngine: 'Blink'}, + // unknown name → Other + {browser: {name: 'UnknownBrowser'}, expectedEngine: 'Other'}, + // no name → Other + {browser: {}, expectedEngine: 'Other'}) + ].forEach(({browser, expectedEngine}) => { + it(`should track cwv using logger distribution with browserEngine: ${expectedEngine} for browser "${ + browser.name ?? '' + }"${browser.isIOS ? ' (isIOS)' : ''}`, async () => { + const logger = {distribution: sinon.spy()} + const reporter = { + onTTFB: fn => { + fn({name: 'TTFB', value: 10, entries: [], attribution: {}}) + } + } + render(, {logger, browser}) + await waitFor(() => [ + expect( + logger.distribution.calledWith({ + name: 'cwv', + amount: 10, + tags: [ + {key: 'name', value: 'ttfb'}, + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: expectedEngine} + ] + }) + ).to.be.true + ]) + }) + }) it('should track TTFB using logger distribution with browser in context', async () => { const logger = { @@ -204,9 +261,7 @@ describe('WebVitalsReporter', () => { } const deviceType = 'mobile' const browser = {deviceType} - render(, {logger, browser}) - await waitFor(() => [ expect( logger.distribution.calledWith({ @@ -215,12 +270,14 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'ttfb'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true ]) }) + it('should track LCP using logger distribution with browser in context', async () => { const logger = { distribution: sinon.spy() @@ -244,9 +301,7 @@ describe('WebVitalsReporter', () => { } const deviceType = 'mobile' const browser = {deviceType} - render(, {logger, browser}) - await waitFor(() => [ expect( logger.distribution.calledWith({ @@ -255,7 +310,8 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'lcp'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -266,7 +322,8 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'lcp_ttfb'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -277,7 +334,8 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'lcp_rlde'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -288,7 +346,8 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'lcp_rldu'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -299,25 +358,34 @@ describe('WebVitalsReporter', () => { tags: [ {key: 'name', value: 'lcp_erde'}, {key: 'pathname', value: '/'}, - {key: 'type', value: deviceType} + {key: 'type', value: deviceType}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true ]) }) - it('should track inp using logger distribution', async () => { + + it('should track INP using logger distribution', async () => { const logger = { distribution: sinon.spy() } - const reporter = { onINP: fn => { - fn({name: 'INP', value: 104, entries: [{startTime: 0, processingStart: 50, processingEnd: 100, duration: 5}]}) + fn({ + name: 'INP', + value: 104, + entries: [], + attribution: { + inputDelay: 50, + processingDuration: 50, + presentationDelay: 4, + interactionTarget: document.body + } + }) } } - render(, {logger}) - await waitFor(() => [ expect( logger.distribution.calledWith({ @@ -325,7 +393,8 @@ describe('WebVitalsReporter', () => { amount: 104, tags: [ {key: 'name', value: 'inp'}, - {key: 'pathname', value: '/'} + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -335,7 +404,8 @@ describe('WebVitalsReporter', () => { amount: 50, tags: [ {key: 'name', value: 'id'}, - {key: 'pathname', value: '/'} + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -345,7 +415,8 @@ describe('WebVitalsReporter', () => { amount: 50, tags: [ {key: 'name', value: 'pt'}, - {key: 'pathname', value: '/'} + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true, @@ -355,50 +426,198 @@ describe('WebVitalsReporter', () => { amount: 4, tags: [ {key: 'name', value: 'pd'}, - {key: 'pathname', value: '/'} + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} + ] + }) + ).to.be.true + ]) + }) + + it('should track FCP using logger distribution', async () => { + const logger = { + distribution: sinon.spy() + } + const reporter = { + onFCP: fn => { + fn({name: 'FCP', value: 800, entries: [], attribution: {}}) + } + } + render(, {logger}) + await waitFor(() => [ + expect( + logger.distribution.calledWith({ + name: 'cwv', + amount: 800, + tags: [ + {key: 'name', value: 'fcp'}, + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} + ] + }) + ).to.be.true + ]) + }) + + it('should track CLS using logger distribution with value multiplied by 1000', async () => { + const logger = { + distribution: sinon.spy() + } + const reporter = { + onCLS: fn => { + fn({name: 'CLS', value: 0.1, entries: [], attribution: {}}) + } + } + render(, {logger}) + await waitFor(() => [ + expect( + logger.distribution.calledWith({ + name: 'cwv', + amount: 100, + tags: [ + {key: 'name', value: 'cls'}, + {key: 'pathname', value: '/'}, + {key: 'browserEngine', value: 'Other'} ] }) ).to.be.true ]) }) - it.skip('should track inp with deviceMemory, networkConnection, and hardwareConcurrency using logger cwv', async () => { - // Mocking the visibilityState - Object.defineProperty(window.document, 'visibilityState', {value: 'hidden', writable: false}) + it('should track CLS using logger with route id, load state, device memory, network connection, and hardware concurrency', async () => { + const logger = { + cwv: sinon.spy() + } + const reporter = { + onCLS: fn => { + fn({ + name: 'CLS', + value: 0.1, + entries: [], + attribution: { + loadState: 'complete', + largestShiftTarget: document.body + } + }) + } + } + const routeId = 'home' + + render(, {logger, routeId}) + await waitFor(() => [ + expect( + logger.cwv.calledWithMatch({ + name: 'cwv.cls', + amount: 100, + path: '/', + target: document.body, + visibilityState: 'visible', + routeId, + loadState: 'complete', + eventType: undefined, + deviceMemory: 4, + effectiveType: '3g', + hardwareConcurrency: 4 + }) + ).to.be.true + ]) + }) + it('should track LCP using logger with route id, load state, device memory, network connection, and hardware concurrency', async () => { const logger = { cwv: sinon.spy() } + const reporter = { + onLCP: fn => { + fn({ + name: 'LCP', + value: 1200, + attribution: { + loadState: 'complete', + target: document.body + } + }) + } + } + const routeId = 'home' + + render(, {logger, routeId}) + await waitFor(() => [ + expect( + logger.cwv.calledWithMatch({ + name: 'cwv.lcp', + amount: 1200, + path: '/', + target: document.body, + visibilityState: 'visible', + routeId, + loadState: 'complete', + eventType: undefined, + deviceMemory: 4, + effectiveType: '3g', + hardwareConcurrency: 4 + }) + ).to.be.true + ]) + }) + it('should track INP using logger with route id, interaction type, device memory, network connection, and hardware concurrency', async () => { + const logger = { + cwv: sinon.spy() + } const reporter = { onINP: fn => { fn({ - attribution: {eventType: 'body'}, - deviceMemory: 8, - effectiveType: '4g', - hardwareConcurrency: 10, name: 'INP', - rating: 'poor', - value: 304 + value: 104, + attribution: { + loadState: 'complete', + interactionType: 'pointer', + interactionTarget: document.body + } }) } } + const routeId = 'home' - render(, {logger}) + render(, {logger, routeId}) await waitFor(() => [ expect( - logger.cwv.calledOnceWithMatch({ + logger.cwv.calledWithMatch({ name: 'cwv.inp', - amount: 304, + amount: 104, path: '/', - target: undefined, - visibilityState: 'hidden', - eventType: 'body', - deviceMemory: 8, - effectiveType: '4g', - hardwareConcurrency: 10 + target: document.body, + visibilityState: 'visible', + routeId, + loadState: 'complete', + eventType: 'pointer', + deviceMemory: 4, + effectiveType: '3g', + hardwareConcurrency: 4 }) ).to.be.true ]) }) + + it('should not track cwv using logger cwv when rating is good', async () => { + const logger = { + cwv: sinon.spy() + } + const reporter = { + onINP: fn => { + fn({ + name: 'INP', + value: 50, + rating: 'good', + attribution: { + interactionType: 'pointer', + loadState: 'dom-interactive' + } + }) + } + } + render(, {logger}) + await waitFor(() => expect(logger.cwv.called).to.be.false) + }) })