Skip to content

Commit f36acbc

Browse files
authored
feat: Switch web vitals library to attribution build (#919)
1 parent 526a38a commit f36acbc

26 files changed

+323
-237
lines changed

src/common/vitals/__mocks__/web-vitals.js

-19
This file was deleted.
+10-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import { onCLS } from 'web-vitals'
1+
import { onCLS } from 'web-vitals/attribution'
22
import { VITAL_NAMES } from './constants'
33
import { VitalMetric } from './vital-metric'
44
import { isBrowserScope } from '../constants/runtime'
55

66
export const cumulativeLayoutShift = new VitalMetric(VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT, (x) => x)
77

88
if (isBrowserScope) {
9-
onCLS(({ value, entries }) => {
10-
if (cumulativeLayoutShift.roundingMethod(value) === cumulativeLayoutShift.current.value) return
11-
cumulativeLayoutShift.update({ value, entries })
9+
onCLS(({ value, attribution, id }) => {
10+
const attrs = {
11+
metricId: id,
12+
largestShiftTarget: attribution.largestShiftTarget,
13+
largestShiftTime: attribution.largestShiftTime,
14+
largestShiftValue: attribution.largestShiftValue,
15+
loadState: attribution.loadState
16+
}
17+
cumulativeLayoutShift.update({ value, attrs })
1218
}, { reportAllChanges: true })
1319
}

src/common/vitals/cumulative-layout-shift.test.js

+19-27
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@ afterEach(() => {
44
jest.clearAllMocks()
55
})
66

7+
const clsAttribution = {
8+
largestShiftTarget: 'element',
9+
largestShiftTime: 12345,
10+
largestShiftValue: 0.9712,
11+
loadState: 'dom-content-loaded'
12+
}
713
const getFreshCLSImport = async (codeToRun) => {
14+
jest.doMock('web-vitals/attribution', () => ({
15+
onCLS: jest.fn(cb => cb({ value: 0.123, attribution: clsAttribution, id: 'beepboop' }))
16+
}))
817
const { cumulativeLayoutShift } = await import('./cumulative-layout-shift')
918
codeToRun(cumulativeLayoutShift)
1019
}
1120

1221
describe('cls', () => {
1322
test('reports cls', (done) => {
1423
getFreshCLSImport(metric => {
15-
metric.subscribe(({ value }) => {
16-
expect(value).toEqual(1)
24+
metric.subscribe(({ value, attrs }) => {
25+
expect(value).toEqual(0.123)
26+
expect(attrs).toEqual({ ...clsAttribution, metricId: 'beepboop' })
1727
done()
1828
})
1929
})
@@ -37,34 +47,16 @@ describe('cls', () => {
3747
__esModule: true,
3848
isBrowserScope: true
3949
}))
40-
let sub1, sub2
50+
let witness = 0
4151
getFreshCLSImport(metric => {
42-
const remove1 = metric.subscribe(({ entries }) => {
43-
sub1 ??= entries[0].id
44-
if (sub1 === sub2) { remove1(); remove2(); done() }
45-
})
46-
47-
const remove2 = metric.subscribe(({ entries }) => {
48-
sub2 ??= entries[0].id
49-
if (sub1 === sub2) { remove1(); remove2(); done() }
52+
metric.subscribe(({ value }) => {
53+
expect(value).toEqual(0.123)
54+
witness++
5055
})
51-
})
52-
})
53-
test('reports only new values', (done) => {
54-
jest.doMock('../constants/runtime', () => ({
55-
__esModule: true,
56-
isBrowserScope: true
57-
}))
58-
let triggered = 0
59-
getFreshCLSImport(metric => {
6056
metric.subscribe(({ value }) => {
61-
triggered++
62-
expect(value).toEqual(1)
63-
expect(triggered).toEqual(1)
64-
setTimeout(() => {
65-
expect(triggered).toEqual(1)
66-
done()
67-
}, 1000)
57+
expect(value).toEqual(0.123)
58+
witness++
59+
if (witness === 2) done()
6860
})
6961
})
7062
})

src/common/vitals/first-contentful-paint.js

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { onFCP } from 'web-vitals'
1+
import { onFCP } from 'web-vitals/attribution'
22
// eslint-disable-next-line camelcase
33
import { iOSBelow16, initiallyHidden, isBrowserScope } from '../constants/runtime'
44
import { VITAL_NAMES } from './constants'
@@ -15,17 +15,22 @@ if (isBrowserScope) {
1515
const paintEntries = performance.getEntriesByType('paint')
1616
paintEntries.forEach(entry => {
1717
if (entry.name === 'first-contentful-paint') {
18-
firstContentfulPaint.update({ value: Math.floor(entry.startTime), entries: paintEntries })
18+
firstContentfulPaint.update({ value: Math.floor(entry.startTime) })
1919
}
2020
})
2121
}
2222
} catch (e) {
2323
// ignore
2424
}
2525
} else {
26-
onFCP(({ value, entries }) => {
26+
onFCP(({ value, attribution }) => {
2727
if (initiallyHidden || firstContentfulPaint.isValid) return
28-
firstContentfulPaint.update({ value, entries })
28+
const attrs = {
29+
timeToFirstByte: attribution.timeToFirstByte,
30+
firstByteToFCP: attribution.firstByteToFCP,
31+
loadState: attribution.loadState
32+
}
33+
firstContentfulPaint.update({ value, attrs })
2934
})
3035
}
3136
}

src/common/vitals/first-contentful-paint.test.js

+32-20
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@ beforeEach(() => {
44
jest.clearAllMocks()
55
})
66

7+
const fcpAttribution = {
8+
timeToFirstByte: 12,
9+
firstByteToFCP: 23,
10+
loadState: 'dom-interactive'
11+
}
12+
let triggeronFCPCallback
713
const getFreshFCPImport = async (codeToRun) => {
14+
jest.doMock('web-vitals/attribution', () => ({
15+
onFCP: jest.fn(cb => { triggeronFCPCallback = cb; cb({ value: 1, attribution: fcpAttribution }) })
16+
}))
817
const { firstContentfulPaint } = await import('./first-contentful-paint')
918
codeToRun(firstContentfulPaint)
1019
}
1120

1221
describe('fcp', () => {
1322
test('reports fcp from web-vitals', (done) => {
14-
getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
15-
expect(value).toEqual(1)
16-
done()
17-
}))
23+
getFreshFCPImport(firstContentfulPaint => {
24+
firstContentfulPaint.subscribe(({ value, attrs }) => {
25+
expect(value).toEqual(1)
26+
expect(attrs).toStrictEqual(fcpAttribution)
27+
done()
28+
})
29+
})
1830
})
1931

2032
test('reports fcp from paintEntries if ios<16', (done) => {
@@ -88,16 +100,16 @@ describe('fcp', () => {
88100
__esModule: true,
89101
isBrowserScope: true
90102
}))
91-
let sub1, sub2
103+
let witness = 0
92104
getFreshFCPImport(metric => {
93-
const remove1 = metric.subscribe(({ entries }) => {
94-
sub1 ??= entries[0].id
95-
if (sub1 === sub2) { remove1(); remove2(); done() }
105+
metric.subscribe(({ value }) => {
106+
expect(value).toEqual(1)
107+
witness++
96108
})
97-
98-
const remove2 = metric.subscribe(({ entries }) => {
99-
sub2 ??= entries[0].id
100-
if (sub1 === sub2) { remove1(); remove2(); done() }
109+
metric.subscribe(({ value }) => {
110+
expect(value).toEqual(1)
111+
witness++
112+
if (witness === 2) done()
101113
})
102114
})
103115
})
@@ -110,15 +122,15 @@ describe('fcp', () => {
110122
isBrowserScope: true
111123
}))
112124
let triggered = 0
113-
getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
114-
triggered++
115-
expect(value).toEqual(1)
116-
expect(triggered).toEqual(1)
117-
setTimeout(() => {
125+
getFreshFCPImport(firstContentfulPaint => {
126+
firstContentfulPaint.subscribe(({ value }) => {
127+
triggered++
128+
expect(value).toEqual(1)
118129
expect(triggered).toEqual(1)
119-
done()
120-
}, 1000)
130+
})
131+
triggeronFCPCallback({ value: 'notequal1' })
132+
expect(triggered).toEqual(1)
133+
done()
121134
})
122-
)
123135
})
124136
})
+11-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
import { onFID } from 'web-vitals'
1+
import { onFID } from 'web-vitals/attribution'
22
import { VitalMetric } from './vital-metric'
33
import { VITAL_NAMES } from './constants'
44
import { initiallyHidden, isBrowserScope } from '../constants/runtime'
55

66
export const firstInputDelay = new VitalMetric(VITAL_NAMES.FIRST_INPUT_DELAY)
77

88
if (isBrowserScope) {
9-
onFID(({ value, entries }) => {
10-
if (initiallyHidden || firstInputDelay.isValid || entries.length === 0) return
9+
onFID(({ value, attribution }) => {
10+
if (initiallyHidden || firstInputDelay.isValid) return
11+
const attrs = {
12+
type: attribution.eventType,
13+
fid: Math.round(value),
14+
eventTarget: attribution.eventTarget,
15+
loadState: attribution.loadState
16+
}
1117

1218
// CWV will only report one (THE) first-input entry to us; fid isn't reported if there are no user interactions occurs before the *first* page hiding.
1319
firstInputDelay.update({
14-
value: entries[0].startTime,
15-
entries,
16-
attrs: { type: entries[0].name, fid: Math.round(value) }
20+
value: attribution.eventTime,
21+
attrs
1722
})
1823
})
1924
}

src/common/vitals/first-input-delay.test.js

+31-17
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,29 @@ beforeEach(() => {
44
jest.clearAllMocks()
55
})
66

7+
const fidAttribution = {
8+
eventTarget: 'html>body',
9+
eventType: 'pointerdown',
10+
eventTime: 1,
11+
loadState: 'loading'
12+
}
13+
let triggeronFIDCallback
714
const getFreshFIDImport = async (codeToRun) => {
15+
jest.doMock('web-vitals/attribution', () => ({
16+
onFID: jest.fn(cb => { triggeronFIDCallback = cb; cb({ value: 100, attribution: fidAttribution }) })
17+
}))
818
const { firstInputDelay } = await import('./first-input-delay')
919
codeToRun(firstInputDelay)
1020
}
1121

1222
describe('fid', () => {
1323
test('reports fcp from web-vitals', (done) => {
14-
getFreshFIDImport(metric => metric.subscribe(({ value }) => {
24+
getFreshFIDImport(metric => metric.subscribe(({ value, attrs }) => {
1525
expect(value).toEqual(1)
26+
expect(attrs.type).toEqual(fidAttribution.eventType)
27+
expect(attrs.fid).toEqual(100)
28+
expect(attrs.eventTarget).toEqual(fidAttribution.eventTarget)
29+
expect(attrs.loadState).toEqual(fidAttribution.loadState)
1630
done()
1731
}))
1832
})
@@ -53,16 +67,16 @@ describe('fid', () => {
5367
__esModule: true,
5468
isBrowserScope: true
5569
}))
56-
let sub1, sub2
70+
let witness = 0
5771
getFreshFIDImport(metric => {
58-
const remove1 = metric.subscribe(({ entries }) => {
59-
sub1 ??= entries[0].id
60-
if (sub1 === sub2) { remove1(); remove2(); done() }
72+
metric.subscribe(({ value }) => {
73+
expect(value).toEqual(1)
74+
witness++
6175
})
62-
63-
const remove2 = metric.subscribe(({ entries }) => {
64-
sub2 ??= entries[0].id
65-
if (sub1 === sub2) { remove1(); remove2(); done() }
76+
metric.subscribe(({ value }) => {
77+
expect(value).toEqual(1)
78+
witness++
79+
if (witness === 2) done()
6680
})
6781
})
6882
})
@@ -74,15 +88,15 @@ describe('fid', () => {
7488
isBrowserScope: true
7589
}))
7690
let triggered = 0
77-
getFreshFIDImport(metric => metric.subscribe(({ value }) => {
78-
triggered++
79-
expect(value).toEqual(1)
80-
expect(triggered).toEqual(1)
81-
setTimeout(() => {
91+
getFreshFIDImport(metric => {
92+
metric.subscribe(({ value }) => {
93+
triggered++
94+
expect(value).toEqual(1)
8295
expect(triggered).toEqual(1)
83-
done()
84-
}, 1000)
96+
})
97+
triggeronFIDCallback({ value: 'notequal1' })
98+
expect(triggered).toEqual(1)
99+
done()
85100
})
86-
)
87101
})
88102
})

src/common/vitals/first-paint.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ if (isBrowserScope) {
1111
observer.disconnect()
1212

1313
/* Initial hidden state and pre-rendering not yet considered for first paint. See web-vitals onFCP for example. */
14-
firstPaint.update({ value: entry.startTime, entries })
14+
firstPaint.update({ value: entry.startTime })
1515
}
1616
})
1717
}

src/common/vitals/first-paint.test.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,16 @@ describe('fp', () => {
9292
__esModule: true,
9393
isBrowserScope: true
9494
}))
95-
let sub1, sub2
95+
let witness = 0
9696
getFreshFPImport(metric => {
97-
const remove1 = metric.subscribe(({ entries }) => {
98-
sub1 ??= entries[0].id
99-
if (sub1 === sub2) { remove1(); remove2(); done() }
97+
metric.subscribe(({ value }) => {
98+
expect(value).toEqual(1)
99+
witness++
100100
})
101-
102-
const remove2 = metric.subscribe(({ entries }) => {
103-
sub2 ??= entries[0].id
104-
if (sub1 === sub2) { remove1(); remove2(); done() }
101+
metric.subscribe(({ value }) => {
102+
expect(value).toEqual(1)
103+
witness++
104+
if (witness === 2) done()
105105
})
106106
})
107107
})
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { onINP } from 'web-vitals'
1+
import { onINP } from 'web-vitals/attribution'
22
import { VitalMetric } from './vital-metric'
33
import { VITAL_NAMES } from './constants'
44
import { isBrowserScope } from '../constants/runtime'
@@ -7,7 +7,14 @@ export const interactionToNextPaint = new VitalMetric(VITAL_NAMES.INTERACTION_TO
77

88
if (isBrowserScope) {
99
/* Interaction-to-Next-Paint */
10-
onINP(({ value, entries, id }) => {
11-
interactionToNextPaint.update({ value, entries, attrs: { metricId: id } })
10+
onINP(({ value, attribution, id }) => {
11+
const attrs = {
12+
metricId: id,
13+
eventTarget: attribution.eventTarget,
14+
eventType: attribution.eventType,
15+
eventTime: attribution.eventTime,
16+
loadState: attribution.loadState
17+
}
18+
interactionToNextPaint.update({ value, attrs })
1219
})
1320
}

0 commit comments

Comments
 (0)