Skip to content

Commit 4329d14

Browse files
Lms24claude
andcommitted
fix(replay): Place CLS web vital at the last layout shift
The replay CLS event used the CLS score as its timestamp, so it always landed a fraction of a millisecond after page load. Use the start time of the last layout shift instead, like the CLS span in tracing does. This also picks the time origin from when the shift happened. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 29752e0 commit 4329d14

2 files changed

Lines changed: 53 additions & 7 deletions

File tree

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,10 @@ export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry
220220
}
221221
}
222222

223-
return getWebVital(metric, 'cumulative-layout-shift', nodes, layoutShifts);
223+
// The CLS value is a score, not a time, so we place the event at the last layout shift. A CLS of 0 has no layout
224+
// shift, so it goes at the time origin.
225+
const lastEntry = metric.entries[metric.entries.length - 1];
226+
return getWebVital(metric, 'cumulative-layout-shift', nodes, layoutShifts, lastEntry?.startTime ?? 0);
224227
}
225228

226229
/**
@@ -241,11 +244,12 @@ function getWebVital(
241244
name: string,
242245
nodes: Node[] | undefined,
243246
attributions?: WebVitalData['attributions'],
247+
time = metric.value,
244248
): ReplayPerformanceEntry<WebVitalData> {
245249
const value = metric.value;
246250
const rating = metric.rating;
247251

248-
const end = getAbsoluteTime(value);
252+
const end = getAbsoluteTime(time);
249253

250254
return {
251255
type: 'web-vital',

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,14 @@ describe('Unit | util | createPerformanceEntries', () => {
143143
});
144144

145145
describe('getCumulativeLayoutShift', () => {
146-
it('works with a CLS metric', async () => {
146+
it('places the CLS event at the last layout shift', async () => {
147147
const metric = {
148-
value: 5108.299,
149-
rating: 'good' as const,
150-
entries: [],
148+
value: 0.15,
149+
rating: 'needs-improvement' as const,
150+
entries: [
151+
{ entryType: 'layout-shift', startTime: 1000, value: 0.05, sources: [], hadRecentInput: false },
152+
{ entryType: 'layout-shift', startTime: 5108.299, value: 0.1, sources: [], hadRecentInput: false },
153+
] as unknown as PerformanceEntry[],
151154
};
152155

153156
const event = getCumulativeLayoutShift(metric);
@@ -157,7 +160,46 @@ describe('Unit | util | createPerformanceEntries', () => {
157160
name: 'cumulative-layout-shift',
158161
start: 1672531205.108299,
159162
end: 1672531205.108299,
160-
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: [], attributions: [] },
163+
data: {
164+
value: 0.15,
165+
size: 0.15,
166+
rating: 'needs-improvement',
167+
nodeIds: [],
168+
attributions: [
169+
{ value: 0.05, nodeIds: undefined },
170+
{ value: 0.1, nodeIds: undefined },
171+
],
172+
},
173+
});
174+
});
175+
176+
it('uses the time origin from when the last layout shift happened', () => {
177+
const driftPointMs = 200_000;
178+
const sleepDurationMs = 3_600_000;
179+
vi.mocked(browserPerformanceTimeOrigin).mockImplementation((time = 0) =>
180+
time < driftPointMs ? TIME_ORIGIN : TIME_ORIGIN + sleepDurationMs,
181+
);
182+
183+
const event = getCumulativeLayoutShift({
184+
value: 0.1,
185+
rating: 'good',
186+
entries: [
187+
{ entryType: 'layout-shift', startTime: driftPointMs + 1000, value: 0.1, sources: [], hadRecentInput: false },
188+
] as unknown as PerformanceEntry[],
189+
});
190+
191+
expect(event.start).toBe((TIME_ORIGIN + sleepDurationMs + driftPointMs + 1000) / 1000);
192+
});
193+
194+
it('places a CLS of 0 at the time origin', async () => {
195+
const event = getCumulativeLayoutShift({ value: 0, rating: 'good', entries: [] });
196+
197+
expect(event).toEqual({
198+
type: 'web-vital',
199+
name: 'cumulative-layout-shift',
200+
start: TIME_ORIGIN / 1000,
201+
end: TIME_ORIGIN / 1000,
202+
data: { value: 0, size: 0, rating: 'good', nodeIds: [], attributions: [] },
161203
});
162204
});
163205
});

0 commit comments

Comments
 (0)