Skip to content

Commit 918c011

Browse files
c298leebillyvg
andauthored
feat(replay): Add layout shift to CLS replay data (getsentry#13386)
We want to show the score for each layout shift as well as the all the nodes that contributed to the score, so we're adding a new `attributions` object to our web vitals data Relates to getsentry/sentry#69881 --------- Co-authored-by: Billy Vong <[email protected]>
1 parent 8d9fe35 commit 918c011

File tree

6 files changed

+48
-15
lines changed

6 files changed

+48
-15
lines changed

Diff for: dev-packages/browser-integration-tests/utils/replayEventTemplates.ts

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export const expectedCLSPerformanceSpan = {
141141
data: {
142142
value: expect.any(Number),
143143
nodeIds: expect.any(Array),
144+
attributions: expect.any(Array),
144145
rating: expect.any(String),
145146
size: expect.any(Number),
146147
},

Diff for: dev-packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export const ReplayRecordingData = [
219219
data: {
220220
value: expect.any(Number),
221221
size: expect.any(Number),
222-
nodeId: 16,
222+
nodeIds: [16],
223223
},
224224
},
225225
},

Diff for: dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts

+1
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export const ReplayRecordingData = [
240240
size: expect.any(Number),
241241
rating: expect.any(String),
242242
nodeIds: expect.any(Array),
243+
attributions: expect.any(Array),
243244
},
244245
},
245246
},

Diff for: packages/replay-internal/src/types/performance.ts

+4
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export interface WebVitalData {
111111
* The recording id of the web vital nodes. -1 if not found
112112
*/
113113
nodeIds?: number[];
114+
/**
115+
* The layout shifts of a CLS metric
116+
*/
117+
attributions?: { value: number; sources?: number[] }[];
114118
}
115119

116120
/**

Diff for: packages/replay-internal/src/util/createPerformanceEntries.ts

+37-10
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ export interface Metric {
4343
* The array may also be empty if the metric value was not based on any
4444
* entries (e.g. a CLS value of 0 given no layout shifts).
4545
*/
46-
entries: PerformanceEntry[] | PerformanceEventTiming[];
46+
entries: PerformanceEntry[] | LayoutShift[];
47+
}
48+
49+
interface LayoutShift extends PerformanceEntry {
50+
value: number;
51+
sources: LayoutShiftAttribution[];
52+
hadRecentInput: boolean;
4753
}
4854

4955
interface LayoutShiftAttribution {
@@ -52,6 +58,11 @@ interface LayoutShiftAttribution {
5258
currentRect: DOMRectReadOnly;
5359
}
5460

61+
interface Attribution {
62+
value: number;
63+
nodeIds?: number[];
64+
}
65+
5566
/**
5667
* Handler creater for web vitals
5768
*/
@@ -187,22 +198,32 @@ export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntr
187198
return getWebVital(metric, 'largest-contentful-paint', node);
188199
}
189200

201+
function isLayoutShift(entry: PerformanceEntry | LayoutShift): entry is LayoutShift {
202+
return (entry as LayoutShift).sources !== undefined;
203+
}
204+
190205
/**
191206
* Add a CLS event to the replay based on a CLS metric.
192207
*/
193208
export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
194-
const lastEntry = metric.entries[metric.entries.length - 1] as
195-
| (PerformanceEntry & { sources?: LayoutShiftAttribution[] })
196-
| undefined;
209+
const layoutShifts: Attribution[] = [];
197210
const nodes: Node[] = [];
198-
if (lastEntry && lastEntry.sources) {
199-
for (const source of lastEntry.sources) {
200-
if (source.node) {
201-
nodes.push(source.node);
211+
for (const entry of metric.entries) {
212+
if (isLayoutShift(entry)) {
213+
const nodeIds = [];
214+
for (const source of entry.sources) {
215+
if (source.node) {
216+
nodes.push(source.node);
217+
const nodeId = record.mirror.getId(source.node);
218+
if (nodeId) {
219+
nodeIds.push(nodeId);
220+
}
221+
}
202222
}
223+
layoutShifts.push({ value: entry.value, nodeIds });
203224
}
204225
}
205-
return getWebVital(metric, 'cumulative-layout-shift', nodes);
226+
return getWebVital(metric, 'cumulative-layout-shift', nodes, layoutShifts);
206227
}
207228

208229
/**
@@ -226,7 +247,12 @@ export function getInteractionToNextPaint(metric: Metric): ReplayPerformanceEntr
226247
/**
227248
* Add an web vital event to the replay based on the web vital metric.
228249
*/
229-
function getWebVital(metric: Metric, name: string, nodes: Node[] | undefined): ReplayPerformanceEntry<WebVitalData> {
250+
function getWebVital(
251+
metric: Metric,
252+
name: string,
253+
nodes: Node[] | undefined,
254+
attributions?: Attribution[],
255+
): ReplayPerformanceEntry<WebVitalData> {
230256
const value = metric.value;
231257
const rating = metric.rating;
232258

@@ -242,6 +268,7 @@ function getWebVital(metric: Metric, name: string, nodes: Node[] | undefined): R
242268
size: value,
243269
rating,
244270
nodeIds: nodes ? nodes.map(node => record.mirror.getId(node)) : undefined,
271+
attributions,
245272
},
246273
};
247274

Diff for: packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe('Unit | util | createPerformanceEntries', () => {
8383
name: 'largest-contentful-paint',
8484
start: 1672531205.108299,
8585
end: 1672531205.108299,
86-
data: { value: 5108.299, rating: 'good', size: 5108.299, nodeIds: undefined },
86+
data: { value: 5108.299, rating: 'good', size: 5108.299, nodeIds: undefined, attributions: undefined },
8787
});
8888
});
8989
});
@@ -103,7 +103,7 @@ describe('Unit | util | createPerformanceEntries', () => {
103103
name: 'cumulative-layout-shift',
104104
start: 1672531205.108299,
105105
end: 1672531205.108299,
106-
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: [] },
106+
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: [], attributions: [] },
107107
});
108108
});
109109
});
@@ -123,7 +123,7 @@ describe('Unit | util | createPerformanceEntries', () => {
123123
name: 'first-input-delay',
124124
start: 1672531205.108299,
125125
end: 1672531205.108299,
126-
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined },
126+
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined, attributions: undefined },
127127
});
128128
});
129129
});
@@ -143,7 +143,7 @@ describe('Unit | util | createPerformanceEntries', () => {
143143
name: 'interaction-to-next-paint',
144144
start: 1672531205.108299,
145145
end: 1672531205.108299,
146-
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined },
146+
data: { value: 5108.299, size: 5108.299, rating: 'good', nodeIds: undefined, attributions: undefined },
147147
});
148148
});
149149
});

0 commit comments

Comments
 (0)