Skip to content

Commit 53c97f7

Browse files
Lms24claude
andcommitted
fix(replay): Place INP web vital at the interaction
Like CLS, the replay INP event used the metric value as its timestamp. For INP that value is a duration, so the event landed shortly after page load. Use the start time of the interaction instead. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4329d14 commit 53c97f7

2 files changed

Lines changed: 24 additions & 5 deletions

File tree

‎packages/replay-internal/src/util/createPerformanceEntries.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ export function getInteractionToNextPaint(metric: Metric): ReplayPerformanceEntr
233233
// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- rule false positive: the cast exposes the entry's `target` field; tsc errors without it
234234
const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { target?: Node }) | undefined;
235235
const node = lastEntry?.target ? [lastEntry.target] : undefined;
236-
return getWebVital(metric, 'interaction-to-next-paint', node);
236+
// The INP value is a duration, not a time, so we place the event at the interaction.
237+
return getWebVital(metric, 'interaction-to-next-paint', node, undefined, lastEntry?.startTime ?? 0);
237238
}
238239

239240
/**

‎packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,11 @@ describe('Unit | util | createPerformanceEntries', () => {
205205
});
206206

207207
describe('getInteractionToNextPaint', () => {
208-
it('works with an INP metric', async () => {
208+
it('places the INP event at the interaction', async () => {
209209
const metric = {
210-
value: 5108.299,
210+
value: 120,
211211
rating: 'good' as const,
212-
entries: [],
212+
entries: [{ name: 'click', entryType: 'event', startTime: 5108.299, duration: 120 }] as PerformanceEntry[],
213213
};
214214

215215
const event = getInteractionToNextPaint(metric);
@@ -219,8 +219,26 @@ describe('Unit | util | createPerformanceEntries', () => {
219219
name: 'interaction-to-next-paint',
220220
start: 1672531205.108299,
221221
end: 1672531205.108299,
222-
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined, attributions: undefined },
222+
data: { value: 120, size: 120, rating: 'good', nodeIds: undefined, attributions: undefined },
223223
});
224224
});
225+
226+
it('uses the time origin from when the interaction happened', () => {
227+
const driftPointMs = 200_000;
228+
const sleepDurationMs = 3_600_000;
229+
vi.mocked(browserPerformanceTimeOrigin).mockImplementation((time = 0) =>
230+
time < driftPointMs ? TIME_ORIGIN : TIME_ORIGIN + sleepDurationMs,
231+
);
232+
233+
const event = getInteractionToNextPaint({
234+
value: 120,
235+
rating: 'good',
236+
entries: [
237+
{ name: 'click', entryType: 'event', startTime: driftPointMs + 1000, duration: 120 },
238+
] as PerformanceEntry[],
239+
});
240+
241+
expect(event.start).toBe((TIME_ORIGIN + sleepDurationMs + driftPointMs + 1000) / 1000);
242+
});
225243
});
226244
});

0 commit comments

Comments
 (0)