Skip to content

Commit c2e22ab

Browse files
fix: Roll back to previous FirstInteraction implementation (#1359)
1 parent cb866e5 commit c2e22ab

File tree

12 files changed

+115
-101
lines changed

12 files changed

+115
-101
lines changed

src/common/vitals/constants.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@
55
export const VITAL_NAMES = {
66
FIRST_PAINT: 'fp',
77
FIRST_CONTENTFUL_PAINT: 'fcp',
8-
FIRST_INTERACTION: 'fi',
8+
FIRST_INPUT_DELAY: 'fi',
99
LARGEST_CONTENTFUL_PAINT: 'lcp',
1010
CUMULATIVE_LAYOUT_SHIFT: 'cls',
1111
INTERACTION_TO_NEXT_PAINT: 'inp',
1212
TIME_TO_FIRST_BYTE: 'ttfb'
1313
}
14-
15-
export const PERFORMANCE_ENTRY_TYPE = {
16-
FIRST_INPUT: 'first-input'
17-
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { onFID } from 'web-vitals/attribution'
6+
import { VitalMetric } from './vital-metric'
7+
import { VITAL_NAMES } from './constants'
8+
import { initiallyHidden, isBrowserScope } from '../constants/runtime'
9+
10+
export const firstInputDelay = new VitalMetric(VITAL_NAMES.FIRST_INPUT_DELAY)
11+
12+
if (isBrowserScope) {
13+
onFID(({ value, attribution }) => {
14+
if (initiallyHidden || firstInputDelay.isValid) return
15+
const attrs = {
16+
type: attribution.eventType,
17+
fid: Math.round(value),
18+
eventTarget: attribution.eventTarget,
19+
loadState: attribution.loadState
20+
}
21+
22+
// 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.
23+
firstInputDelay.update({
24+
value: attribution.eventTime,
25+
attrs
26+
})
27+
})
28+
}

src/common/vitals/first-interaction.js

-38
This file was deleted.

src/features/page_view_timing/aggregate/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { FEATURE_NAMES } from '../../../loaders/features/features'
1111
import { AggregateBase } from '../../utils/aggregate-base'
1212
import { cumulativeLayoutShift } from '../../../common/vitals/cumulative-layout-shift'
1313
import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
14+
import { firstInputDelay } from '../../../common/vitals/first-input-delay'
1415
import { firstPaint } from '../../../common/vitals/first-paint'
15-
import { firstInteraction } from '../../../common/vitals/first-interaction'
1616
import { interactionToNextPaint } from '../../../common/vitals/interaction-to-next-paint'
1717
import { largestContentfulPaint } from '../../../common/vitals/largest-contentful-paint'
1818
import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
@@ -37,8 +37,8 @@ export class Aggregate extends AggregateBase {
3737
this.waitForFlags(([])).then(() => {
3838
firstPaint.subscribe(this.#handleVitalMetric)
3939
firstContentfulPaint.subscribe(this.#handleVitalMetric)
40+
firstInputDelay.subscribe(this.#handleVitalMetric)
4041
largestContentfulPaint.subscribe(this.#handleVitalMetric)
41-
firstInteraction.subscribe(this.#handleVitalMetric)
4242
interactionToNextPaint.subscribe(this.#handleVitalMetric)
4343
timeToFirstByte.subscribe(({ attrs }) => {
4444
this.addTiming('load', Math.round(attrs.navigationEntry.loadEventEnd))

src/features/session_trace/aggregate/trace/storage.js

+5
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ export class TraceStorage {
136136

137137
processPVT (name, value, attrs) {
138138
this.storeTiming({ [name]: value })
139+
if (hasFID(name, attrs)) this.storeEvent({ type: 'fid', target: 'document' }, 'document', value, value + attrs.fid)
140+
141+
function hasFID (name, attrs) {
142+
return name === 'fi' && !!attrs && typeof attrs.fid === 'number'
143+
}
139144
}
140145

141146
storeTiming (timingEntry, isAbsoluteTimestamp = false) {

tests/components/page_view_timing/aggregate.test.js

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ jest.mock('web-vitals/attribution', () => ({
1313
value: 1,
1414
attribution: {}
1515
})),
16+
onFID: jest.fn(cb => cb({
17+
value: 1234,
18+
attribution: { eventTime: 5, eventType: 'pointerdown' }
19+
})),
1620
onINP: jest.fn((cb) => cb({
1721
value: 8,
1822
attribution: {}
@@ -81,6 +85,13 @@ test('LCP event with CLS attribute', () => {
8185
}
8286
})
8387

88+
test('sends expected FI attributes when available', () => {
89+
expect(timingsAggregate.events.get()[0].data.length).toBeGreaterThanOrEqual(1)
90+
const fiPayload = timingsAggregate.events.get()[0].data.find(x => x.name === 'fi')
91+
expect(fiPayload.value).toEqual(5)
92+
expect(fiPayload.attrs).toEqual(expect.objectContaining({ type: 'pointerdown', fid: 1234, cls: 0.1119, ...expectedNetworkInfo }))
93+
})
94+
8495
test('sends CLS node with right val on vis change', () => {
8596
let clsNode = timingsAggregate.events.get()[0].data.find(tn => tn.name === VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT)
8697
expect(clsNode).toBeUndefined()

tests/components/page_view_timing/index.test.js

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ jest.mock('web-vitals/attribution', () => ({
1010
value: 1,
1111
attribution: {}
1212
})),
13+
onFID: jest.fn(cb => cb({
14+
value: 1234,
15+
attribution: { eventTime: 5, eventType: 'pointerdown' }
16+
})),
1317
onINP: jest.fn((cb) => cb({
1418
value: 8,
1519
attribution: {}
@@ -83,6 +87,13 @@ describe('pvt aggregate tests', () => {
8387
}
8488
})
8589

90+
test('sends expected FI attributes when available', () => {
91+
expect(pvtAgg.events.get()[0].data.length).toBeTruthy()
92+
const fiPayload = pvtAgg.events.get()[0].data.find(x => x.name === 'fi')
93+
expect(fiPayload.value).toEqual(5)
94+
expect(fiPayload.attrs).toEqual(expect.objectContaining({ type: 'pointerdown', fid: 1234, cls: 0.1119, ...expectedNetworkInfo }))
95+
})
96+
8697
test('sends CLS node with right val on vis change', () => {
8798
let clsNode = pvtAgg.events.get()[0].data.find(tn => tn.name === VITAL_NAMES.CUMULATIVE_LAYOUT_SHIFT)
8899
expect(clsNode).toBeUndefined()

tests/components/session_trace/aggregate.test.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test('creates right nodes', async () => {
3030
document.dispatchEvent(new CustomEvent('DOMContentLoaded')) // simulate natural browser event
3131
window.dispatchEvent(new CustomEvent('load')) // load is actually ignored by Trace as it should be passed by the PVT feature, so it should not be in payload
3232
sessionTraceAggregate.events.storeXhrAgg('xhr', '[200,null,null]', { method: 'GET', status: 200 }, { rxSize: 770, duration: 99, cbTime: 0, time: 217 }) // fake ajax data
33-
sessionTraceAggregate.events.processPVT('fi', 30) // fake pvt data
33+
sessionTraceAggregate.events.processPVT('fi', 30, { fid: 8 }) // fake pvt data
3434

3535
const payload = sessionTraceAggregate.makeHarvestPayload()[0].payload
3636
let res = payload.body
@@ -58,6 +58,11 @@ test('creates right nodes', async () => {
5858
expect(pvt.o).toEqual('document')
5959
expect(pvt.s).toEqual(pvt.e) // that FI has no duration
6060
expect(pvt.t).toEqual('timing')
61+
pvt = res.filter(node => node.n === 'fid')[0]
62+
expect(pvt.o).toEqual('document')
63+
expect(pvt.s).toEqual(30) // that FID has a duration relative to FI'
64+
expect(pvt.e).toEqual(30 + 8)
65+
expect(pvt.t).toEqual('event')
6166

6267
let unknown = res.filter(n => n.o === 'unknown')
6368
expect(unknown.length).toEqual(0) // no events with unknown origin

tests/specs/pvt/timings.e2e.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { supportsCumulativeLayoutShift, supportsFirstPaint, supportsInteractionToNextPaint, supportsLargestContentfulPaint, supportsPerformanceEventTiming } from '../../../tools/browser-matcher/common-matchers.mjs'
1+
import { supportsCumulativeLayoutShift, supportsFirstInputDelay, supportsFirstPaint, supportsInteractionToNextPaint, supportsLargestContentfulPaint } from '../../../tools/browser-matcher/common-matchers.mjs'
22
import { testTimingEventsRequest } from '../../../tools/testing-server/utils/expect-tests'
33

4+
const isClickInteractionType = type => type === 'pointerdown' || type === 'mousedown' || type === 'click'
45
const loadersToTest = ['rum', 'spa']
56

67
describe('pvt timings tests', () => {
@@ -71,7 +72,7 @@ describe('pvt timings tests', () => {
7172

7273
describe('interaction related timings', () => {
7374
loadersToTest.forEach(loader => {
74-
it(`FI, INP & LCP for ${loader} agent`, async () => {
75+
it(`FI, FID, INP & LCP for ${loader} agent`, async () => {
7576
const start = Date.now()
7677
await browser.url(
7778
await browser.testHandle.assetURL('basic-click-tracking.html', { loader })
@@ -84,17 +85,20 @@ describe('pvt timings tests', () => {
8485
.then(async () => browser.url(await browser.testHandle.assetURL('/')))
8586
])
8687

87-
if (browserMatch(supportsPerformanceEventTiming)) {
88+
if (browserMatch(supportsFirstInputDelay)) {
8889
// FID is replaced by subscribing to 'first-input'
8990
const fi = timingsHarvests.find(harvest => harvest.request.body.find(t => t.name === 'fi'))
9091
?.request.body.find(timing => timing.name === 'fi')
9192
expect(fi.value).toBeGreaterThanOrEqual(0)
9293
expect(fi.value).toBeLessThan(Date.now() - start)
9394

94-
const isClickInteractionType = type => type === 'pointerdown' || type === 'mousedown' || type === 'click'
9595
const fiType = fi.attributes.find(attr => attr.key === 'type')
9696
expect(isClickInteractionType(fiType.value)).toEqual(true)
9797
expect(fiType.type).toEqual('stringAttribute')
98+
99+
const fid = fi.attributes.find(attr => attr.key === 'fid')
100+
expect(fid.value).toBeGreaterThanOrEqual(0)
101+
expect(fid.type).toEqual('doubleAttribute')
98102
}
99103

100104
if (browserMatch(supportsLargestContentfulPaint)) {
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,34 @@
1-
import { PERFORMANCE_ENTRY_TYPE } from '../../../../src/common/vitals/constants'
2-
31
beforeEach(() => {
42
jest.resetModules()
53
jest.resetAllMocks()
64
jest.clearAllMocks()
7-
8-
const mockPerformanceObserver = jest.fn(cb => ({
9-
// Note: this is an imperfect mock, as observer.disconnect() is not functional
10-
observe: () => {
11-
const callCb = () => {
12-
cb({
13-
getEntries: () => [{
14-
name: PERFORMANCE_ENTRY_TYPE.FIRST_INPUT,
15-
startTime: 1
16-
}]
17-
})
18-
setTimeout(callCb, 250)
19-
}
20-
setTimeout(callCb, 250)
21-
},
22-
disconnect: jest.fn()
23-
}))
24-
global.PerformanceObserver = mockPerformanceObserver
25-
global.PerformanceObserver.supportedEntryTypes = [PERFORMANCE_ENTRY_TYPE.FIRST_INPUT]
265
})
276

28-
const getFreshImport = async (codeToRun) => {
29-
const { firstInteraction } = await import('../../../../src/common/vitals/first-interaction')
30-
codeToRun(firstInteraction)
7+
const fidAttribution = {
8+
eventTarget: 'html>body',
9+
eventType: 'pointerdown',
10+
eventTime: 1,
11+
loadState: 'loading'
12+
}
13+
let triggeronFIDCallback
14+
const getFreshFIDImport = async (codeToRun) => {
15+
jest.doMock('web-vitals/attribution', () => ({
16+
onFID: jest.fn(cb => { triggeronFIDCallback = cb; cb({ value: 100, attribution: fidAttribution }) })
17+
}))
18+
const { firstInputDelay } = await import('../../../../src/common/vitals/first-input-delay')
19+
codeToRun(firstInputDelay)
3120
}
3221

33-
describe('fi (first interaction)', () => {
34-
test('reports fi', (done) => {
35-
getFreshImport(metric => {
36-
metric.subscribe(({ value }) => {
37-
expect(value).toEqual(1)
38-
done()
39-
})
40-
})
22+
describe('fid', () => {
23+
test('reports fcp from web-vitals', (done) => {
24+
getFreshFIDImport(metric => metric.subscribe(({ value, attrs }) => {
25+
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)
30+
done()
31+
}))
4132
})
4233

4334
test('Does NOT report values if initiallyHidden', (done) => {
@@ -47,7 +38,7 @@ describe('fi (first interaction)', () => {
4738
isBrowserScope: true
4839
}))
4940

50-
getFreshImport(metric => {
41+
getFreshFIDImport(metric => {
5142
metric.subscribe(() => {
5243
console.log('should not have reported')
5344
expect(1).toEqual(2)
@@ -63,7 +54,7 @@ describe('fi (first interaction)', () => {
6354
isBrowserScope: false
6455
}))
6556

66-
getFreshImport(metric => {
57+
getFreshFIDImport(metric => {
6758
metric.subscribe(({ value, attrs }) => {
6859
console.log('should not have reported...')
6960
expect(1).toEqual(2)
@@ -79,7 +70,7 @@ describe('fi (first interaction)', () => {
7970
isBrowserScope: true
8071
}))
8172
let witness = 0
82-
getFreshImport(metric => {
73+
getFreshFIDImport(metric => {
8374
metric.subscribe(({ value }) => {
8475
expect(value).toEqual(1)
8576
witness++
@@ -99,14 +90,15 @@ describe('fi (first interaction)', () => {
9990
isBrowserScope: true
10091
}))
10192
let triggered = 0
102-
getFreshImport(metric => metric.subscribe(({ value }) => {
103-
triggered++
104-
expect(value).toEqual(1)
105-
expect(triggered).toEqual(1)
106-
setTimeout(() => {
93+
getFreshFIDImport(metric => {
94+
metric.subscribe(({ value }) => {
95+
triggered++
96+
expect(value).toEqual(1)
10797
expect(triggered).toEqual(1)
108-
done()
109-
}, 1000)
110-
}))
98+
})
99+
triggeronFIDCallback({ value: 'notequal1' })
100+
expect(triggered).toEqual(1)
101+
done()
102+
})
111103
})
112104
})

tests/unit/features/page_view_timing/aggregate/index.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ function testCases () {
162162
},
163163
{
164164
type: 'doubleAttribute',
165-
key: 'foo',
165+
key: 'fid',
166166
value: 12.34
167167
}
168168
]
@@ -201,7 +201,7 @@ function testCases () {
201201
},
202202
{
203203
type: 'doubleAttribute',
204-
key: 'foo',
204+
key: 'fid',
205205
value: 12.34
206206
}
207207
]
@@ -218,7 +218,7 @@ function testCases () {
218218
},
219219
{
220220
type: 'doubleAttribute',
221-
key: 'foo',
221+
key: 'fid',
222222
value: 12.34
223223
}
224224
]

0 commit comments

Comments
 (0)