Skip to content

Commit

Permalink
feat: Switch web vitals library to attribution build (#919)
Browse files Browse the repository at this point in the history
  • Loading branch information
cwli24 authored Mar 26, 2024
1 parent 526a38a commit f36acbc
Show file tree
Hide file tree
Showing 26 changed files with 323 additions and 237 deletions.
19 changes: 0 additions & 19 deletions src/common/vitals/__mocks__/web-vitals.js

This file was deleted.

14 changes: 10 additions & 4 deletions src/common/vitals/cumulative-layout-shift.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { onCLS } from 'web-vitals'
import { onCLS } from 'web-vitals/attribution'
import { VITAL_NAMES } from './constants'
import { VitalMetric } from './vital-metric'
import { isBrowserScope } from '../constants/runtime'

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

if (isBrowserScope) {
onCLS(({ value, entries }) => {
if (cumulativeLayoutShift.roundingMethod(value) === cumulativeLayoutShift.current.value) return
cumulativeLayoutShift.update({ value, entries })
onCLS(({ value, attribution, id }) => {
const attrs = {
metricId: id,
largestShiftTarget: attribution.largestShiftTarget,
largestShiftTime: attribution.largestShiftTime,
largestShiftValue: attribution.largestShiftValue,
loadState: attribution.loadState
}
cumulativeLayoutShift.update({ value, attrs })
}, { reportAllChanges: true })
}
46 changes: 19 additions & 27 deletions src/common/vitals/cumulative-layout-shift.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@ afterEach(() => {
jest.clearAllMocks()
})

const clsAttribution = {
largestShiftTarget: 'element',
largestShiftTime: 12345,
largestShiftValue: 0.9712,
loadState: 'dom-content-loaded'
}
const getFreshCLSImport = async (codeToRun) => {
jest.doMock('web-vitals/attribution', () => ({
onCLS: jest.fn(cb => cb({ value: 0.123, attribution: clsAttribution, id: 'beepboop' }))
}))
const { cumulativeLayoutShift } = await import('./cumulative-layout-shift')
codeToRun(cumulativeLayoutShift)
}

describe('cls', () => {
test('reports cls', (done) => {
getFreshCLSImport(metric => {
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
metric.subscribe(({ value, attrs }) => {
expect(value).toEqual(0.123)
expect(attrs).toEqual({ ...clsAttribution, metricId: 'beepboop' })
done()
})
})
Expand All @@ -37,34 +47,16 @@ describe('cls', () => {
__esModule: true,
isBrowserScope: true
}))
let sub1, sub2
let witness = 0
getFreshCLSImport(metric => {
const remove1 = metric.subscribe(({ entries }) => {
sub1 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
})

const remove2 = metric.subscribe(({ entries }) => {
sub2 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(0.123)
witness++
})
})
})
test('reports only new values', (done) => {
jest.doMock('../constants/runtime', () => ({
__esModule: true,
isBrowserScope: true
}))
let triggered = 0
getFreshCLSImport(metric => {
metric.subscribe(({ value }) => {
triggered++
expect(value).toEqual(1)
expect(triggered).toEqual(1)
setTimeout(() => {
expect(triggered).toEqual(1)
done()
}, 1000)
expect(value).toEqual(0.123)
witness++
if (witness === 2) done()
})
})
})
Expand Down
13 changes: 9 additions & 4 deletions src/common/vitals/first-contentful-paint.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onFCP } from 'web-vitals'
import { onFCP } from 'web-vitals/attribution'
// eslint-disable-next-line camelcase
import { iOSBelow16, initiallyHidden, isBrowserScope } from '../constants/runtime'
import { VITAL_NAMES } from './constants'
Expand All @@ -15,17 +15,22 @@ if (isBrowserScope) {
const paintEntries = performance.getEntriesByType('paint')
paintEntries.forEach(entry => {
if (entry.name === 'first-contentful-paint') {
firstContentfulPaint.update({ value: Math.floor(entry.startTime), entries: paintEntries })
firstContentfulPaint.update({ value: Math.floor(entry.startTime) })
}
})
}
} catch (e) {
// ignore
}
} else {
onFCP(({ value, entries }) => {
onFCP(({ value, attribution }) => {
if (initiallyHidden || firstContentfulPaint.isValid) return
firstContentfulPaint.update({ value, entries })
const attrs = {
timeToFirstByte: attribution.timeToFirstByte,
firstByteToFCP: attribution.firstByteToFCP,
loadState: attribution.loadState
}
firstContentfulPaint.update({ value, attrs })
})
}
}
52 changes: 32 additions & 20 deletions src/common/vitals/first-contentful-paint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@ beforeEach(() => {
jest.clearAllMocks()
})

const fcpAttribution = {
timeToFirstByte: 12,
firstByteToFCP: 23,
loadState: 'dom-interactive'
}
let triggeronFCPCallback
const getFreshFCPImport = async (codeToRun) => {
jest.doMock('web-vitals/attribution', () => ({
onFCP: jest.fn(cb => { triggeronFCPCallback = cb; cb({ value: 1, attribution: fcpAttribution }) })
}))
const { firstContentfulPaint } = await import('./first-contentful-paint')
codeToRun(firstContentfulPaint)
}

describe('fcp', () => {
test('reports fcp from web-vitals', (done) => {
getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
expect(value).toEqual(1)
done()
}))
getFreshFCPImport(firstContentfulPaint => {
firstContentfulPaint.subscribe(({ value, attrs }) => {
expect(value).toEqual(1)
expect(attrs).toStrictEqual(fcpAttribution)
done()
})
})
})

test('reports fcp from paintEntries if ios<16', (done) => {
Expand Down Expand Up @@ -88,16 +100,16 @@ describe('fcp', () => {
__esModule: true,
isBrowserScope: true
}))
let sub1, sub2
let witness = 0
getFreshFCPImport(metric => {
const remove1 = metric.subscribe(({ entries }) => {
sub1 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
witness++
})

const remove2 = metric.subscribe(({ entries }) => {
sub2 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
witness++
if (witness === 2) done()
})
})
})
Expand All @@ -110,15 +122,15 @@ describe('fcp', () => {
isBrowserScope: true
}))
let triggered = 0
getFreshFCPImport(firstContentfulPaint => firstContentfulPaint.subscribe(({ value }) => {
triggered++
expect(value).toEqual(1)
expect(triggered).toEqual(1)
setTimeout(() => {
getFreshFCPImport(firstContentfulPaint => {
firstContentfulPaint.subscribe(({ value }) => {
triggered++
expect(value).toEqual(1)
expect(triggered).toEqual(1)
done()
}, 1000)
})
triggeronFCPCallback({ value: 'notequal1' })
expect(triggered).toEqual(1)
done()
})
)
})
})
17 changes: 11 additions & 6 deletions src/common/vitals/first-input-delay.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { onFID } from 'web-vitals'
import { onFID } from 'web-vitals/attribution'
import { VitalMetric } from './vital-metric'
import { VITAL_NAMES } from './constants'
import { initiallyHidden, isBrowserScope } from '../constants/runtime'

export const firstInputDelay = new VitalMetric(VITAL_NAMES.FIRST_INPUT_DELAY)

if (isBrowserScope) {
onFID(({ value, entries }) => {
if (initiallyHidden || firstInputDelay.isValid || entries.length === 0) return
onFID(({ value, attribution }) => {
if (initiallyHidden || firstInputDelay.isValid) return
const attrs = {
type: attribution.eventType,
fid: Math.round(value),
eventTarget: attribution.eventTarget,
loadState: attribution.loadState
}

// 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.
firstInputDelay.update({
value: entries[0].startTime,
entries,
attrs: { type: entries[0].name, fid: Math.round(value) }
value: attribution.eventTime,
attrs
})
})
}
48 changes: 31 additions & 17 deletions src/common/vitals/first-input-delay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@ beforeEach(() => {
jest.clearAllMocks()
})

const fidAttribution = {
eventTarget: 'html>body',
eventType: 'pointerdown',
eventTime: 1,
loadState: 'loading'
}
let triggeronFIDCallback
const getFreshFIDImport = async (codeToRun) => {
jest.doMock('web-vitals/attribution', () => ({
onFID: jest.fn(cb => { triggeronFIDCallback = cb; cb({ value: 100, attribution: fidAttribution }) })
}))
const { firstInputDelay } = await import('./first-input-delay')
codeToRun(firstInputDelay)
}

describe('fid', () => {
test('reports fcp from web-vitals', (done) => {
getFreshFIDImport(metric => metric.subscribe(({ value }) => {
getFreshFIDImport(metric => metric.subscribe(({ value, attrs }) => {
expect(value).toEqual(1)
expect(attrs.type).toEqual(fidAttribution.eventType)
expect(attrs.fid).toEqual(100)
expect(attrs.eventTarget).toEqual(fidAttribution.eventTarget)
expect(attrs.loadState).toEqual(fidAttribution.loadState)
done()
}))
})
Expand Down Expand Up @@ -53,16 +67,16 @@ describe('fid', () => {
__esModule: true,
isBrowserScope: true
}))
let sub1, sub2
let witness = 0
getFreshFIDImport(metric => {
const remove1 = metric.subscribe(({ entries }) => {
sub1 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
witness++
})

const remove2 = metric.subscribe(({ entries }) => {
sub2 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
witness++
if (witness === 2) done()
})
})
})
Expand All @@ -74,15 +88,15 @@ describe('fid', () => {
isBrowserScope: true
}))
let triggered = 0
getFreshFIDImport(metric => metric.subscribe(({ value }) => {
triggered++
expect(value).toEqual(1)
expect(triggered).toEqual(1)
setTimeout(() => {
getFreshFIDImport(metric => {
metric.subscribe(({ value }) => {
triggered++
expect(value).toEqual(1)
expect(triggered).toEqual(1)
done()
}, 1000)
})
triggeronFIDCallback({ value: 'notequal1' })
expect(triggered).toEqual(1)
done()
})
)
})
})
2 changes: 1 addition & 1 deletion src/common/vitals/first-paint.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ if (isBrowserScope) {
observer.disconnect()

/* Initial hidden state and pre-rendering not yet considered for first paint. See web-vitals onFCP for example. */
firstPaint.update({ value: entry.startTime, entries })
firstPaint.update({ value: entry.startTime })
}
})
}
Expand Down
16 changes: 8 additions & 8 deletions src/common/vitals/first-paint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,16 @@ describe('fp', () => {
__esModule: true,
isBrowserScope: true
}))
let sub1, sub2
let witness = 0
getFreshFPImport(metric => {
const remove1 = metric.subscribe(({ entries }) => {
sub1 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
witness++
})

const remove2 = metric.subscribe(({ entries }) => {
sub2 ??= entries[0].id
if (sub1 === sub2) { remove1(); remove2(); done() }
metric.subscribe(({ value }) => {
expect(value).toEqual(1)
witness++
if (witness === 2) done()
})
})
})
Expand Down
13 changes: 10 additions & 3 deletions src/common/vitals/interaction-to-next-paint.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onINP } from 'web-vitals'
import { onINP } from 'web-vitals/attribution'
import { VitalMetric } from './vital-metric'
import { VITAL_NAMES } from './constants'
import { isBrowserScope } from '../constants/runtime'
Expand All @@ -7,7 +7,14 @@ export const interactionToNextPaint = new VitalMetric(VITAL_NAMES.INTERACTION_TO

if (isBrowserScope) {
/* Interaction-to-Next-Paint */
onINP(({ value, entries, id }) => {
interactionToNextPaint.update({ value, entries, attrs: { metricId: id } })
onINP(({ value, attribution, id }) => {
const attrs = {
metricId: id,
eventTarget: attribution.eventTarget,
eventType: attribution.eventType,
eventTime: attribution.eventTime,
loadState: attribution.loadState
}
interactionToNextPaint.update({ value, attrs })
})
}
Loading

0 comments on commit f36acbc

Please sign in to comment.