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)
+ })
})