Skip to content

Commit 7b429e3

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 1ff19ed commit 7b429e3

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
@@ -210,11 +210,11 @@ describe('Unit | util | createPerformanceEntries', () => {
210210
});
211211

212212
describe('getInteractionToNextPaint', () => {
213-
it('works with an INP metric', async () => {
213+
it('places the INP event at the interaction', async () => {
214214
const metric = {
215-
value: 5108.299,
215+
value: 120,
216216
rating: 'good' as const,
217-
entries: [],
217+
entries: [{ name: 'click', entryType: 'event', startTime: 5108.299, duration: 120 }] as PerformanceEntry[],
218218
};
219219

220220
const event = getInteractionToNextPaint(metric);
@@ -224,8 +224,26 @@ describe('Unit | util | createPerformanceEntries', () => {
224224
name: 'interaction-to-next-paint',
225225
start: 1672531205.108299,
226226
end: 1672531205.108299,
227-
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined, attributions: undefined },
227+
data: { value: 120, size: 120, rating: 'good', nodeIds: undefined, attributions: undefined },
228228
});
229229
});
230+
231+
it('uses the time origin from when the interaction happened', () => {
232+
const driftPointMs = 200_000;
233+
const sleepDurationMs = 3_600_000;
234+
vi.mocked(browserPerformanceTimeOrigin).mockImplementation((time = 0) =>
235+
time < driftPointMs ? TIME_ORIGIN : TIME_ORIGIN + sleepDurationMs,
236+
);
237+
238+
const event = getInteractionToNextPaint({
239+
value: 120,
240+
rating: 'good',
241+
entries: [
242+
{ name: 'click', entryType: 'event', startTime: driftPointMs + 1000, duration: 120 },
243+
] as PerformanceEntry[],
244+
});
245+
246+
expect(event.start).toBe((TIME_ORIGIN + sleepDurationMs + driftPointMs + 1000) / 1000);
247+
});
230248
});
231249
});

0 commit comments

Comments
 (0)