Skip to content

Commit 59a985f

Browse files
committed
replay web vital breadcrumbs
1 parent 48b0a35 commit 59a985f

File tree

4 files changed

+165
-5
lines changed

4 files changed

+165
-5
lines changed

packages/replay-internal/src/coreHandlers/performanceObserver.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { addLcpInstrumentationHandler, addPerformanceInstrumentationHandler } from '@sentry-internal/browser-utils';
1+
import { addClsInstrumentationHandler, addFidInstrumentationHandler, addLcpInstrumentationHandler, addPerformanceInstrumentationHandler } from '@sentry-internal/browser-utils';
22

33
import type { ReplayContainer } from '../types';
4-
import { getLargestContentfulPaint } from '../util/createPerformanceEntries';
4+
import { getLargestContentfulPaint, getCumulativeLayoutShift, getFirstInputDelay, getInteractionToNextPaint } from '../util/createPerformanceEntries';
5+
import { addInpInstrumentationHandler } from '@sentry-internal/browser-utils/build/types/metrics/instrument';
56

67
/**
78
* Sets up a PerformanceObserver to listen to all performance entry types.
@@ -29,6 +30,15 @@ export function setupPerformanceObserver(replay: ReplayContainer): () => void {
2930
addLcpInstrumentationHandler(({ metric }) => {
3031
replay.replayPerformanceEntries.push(getLargestContentfulPaint(metric));
3132
}),
33+
addClsInstrumentationHandler(({ metric }) => {
34+
replay.replayPerformanceEntries.push(getCumulativeLayoutShift(metric));
35+
}),
36+
addFidInstrumentationHandler(({ metric }) => {
37+
replay.replayPerformanceEntries.push(getFirstInputDelay(metric));
38+
}),
39+
addInpInstrumentationHandler(({ metric }) => {
40+
replay.replayPerformanceEntries.push(getInteractionToNextPaint(metric));
41+
}),
3242
);
3343

3444
// A callback to cleanup all handlers

packages/replay-internal/src/types/performance.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,46 @@ export interface LargestContentfulPaintData {
108108
nodeId?: number;
109109
}
110110

111+
export interface CumulativeLayoutShiftData {
112+
/**
113+
* Render time (in ms) of the CLS
114+
*/
115+
value: number;
116+
size: number;
117+
/**
118+
* The recording id of the CLS node. -1 if not found
119+
*/
120+
nodeId?: number;
121+
}
122+
123+
export interface FirstInputDelayData {
124+
/**
125+
* Render time (in ms) of the FID
126+
*/
127+
value: number;
128+
size: number;
129+
/**
130+
* The recording id of the FID node. -1 if not found
131+
*/
132+
nodeId?: number;
133+
}
134+
135+
export interface InteractionToNextPaintData {
136+
/**
137+
* Render time (in ms) of the INP
138+
*/
139+
value: number;
140+
size: number;
141+
/**
142+
* The recording id of the INP node. -1 if not found
143+
*/
144+
nodeId?: number;
145+
}
146+
111147
/**
112148
* Entries that come from window.performance
113149
*/
114-
export type AllPerformanceEntryData = PaintData | NavigationData | ResourceData | LargestContentfulPaintData;
150+
export type AllPerformanceEntryData = PaintData | NavigationData | ResourceData | LargestContentfulPaintData | CumulativeLayoutShiftData | FirstInputDelayData;
115151

116152
export interface MemoryData {
117153
memory: {

packages/replay-internal/src/types/replayFrame.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import type { Breadcrumb } from '@sentry/types';
33
import type {
44
HistoryData,
55
LargestContentfulPaintData,
6+
CumulativeLayoutShiftData,
7+
FirstInputDelayData,
8+
InteractionToNextPaintData,
69
MemoryData,
710
NavigationData,
811
NetworkRequestData,
@@ -167,6 +170,21 @@ interface ReplayLargestContentfulPaintFrame extends ReplayBaseSpanFrame {
167170
op: 'largest-contentful-paint';
168171
}
169172

173+
interface ReplayCumulativeLayoutShiftFrame extends ReplayBaseSpanFrame {
174+
data: CumulativeLayoutShiftData;
175+
op: 'cumulative-layout-shift';
176+
}
177+
178+
interface ReplayFirstInputDelayFrame extends ReplayBaseSpanFrame {
179+
data: FirstInputDelayData;
180+
op: 'first-input-delay';
181+
}
182+
183+
interface ReplayInteractionToNextPaintFrame extends ReplayBaseSpanFrame {
184+
data: InteractionToNextPaintData;
185+
op: 'interaction-to-next-paint';
186+
}
187+
170188
interface ReplayMemoryFrame extends ReplayBaseSpanFrame {
171189
data: MemoryData;
172190
op: 'memory';
@@ -197,6 +215,9 @@ export type ReplaySpanFrame =
197215
| ReplayHistoryFrame
198216
| ReplayRequestFrame
199217
| ReplayLargestContentfulPaintFrame
218+
| ReplayCumulativeLayoutShiftFrame
219+
| ReplayFirstInputDelayFrame
220+
| ReplayInteractionToNextPaintFrame
200221
| ReplayMemoryFrame
201222
| ReplayNavigationFrame
202223
| ReplayPaintFrame

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

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import type {
77
AllPerformanceEntryData,
88
ExperimentalPerformanceResourceTiming,
99
LargestContentfulPaintData,
10+
CumulativeLayoutShiftData,
11+
FirstInputDelayData,
12+
InteractionToNextPaintData,
1013
NavigationData,
1114
PaintData,
1215
ReplayPerformanceEntry,
@@ -141,7 +144,7 @@ function createResourceEntry(
141144
}
142145

143146
/**
144-
* Add a LCP event to the replay based on an LCP metric.
147+
* Add a LCP event to the replay based on a LCP metric.
145148
*/
146149
export function getLargestContentfulPaint(metric: {
147150
value: number;
@@ -156,7 +159,7 @@ export function getLargestContentfulPaint(metric: {
156159
const end = getAbsoluteTime(value);
157160

158161
const data: ReplayPerformanceEntry<LargestContentfulPaintData> = {
159-
type: 'largest-contentful-paint',
162+
type: 'web-vital',
160163
name: 'largest-contentful-paint',
161164
start: end,
162165
end,
@@ -169,3 +172,93 @@ export function getLargestContentfulPaint(metric: {
169172

170173
return data;
171174
}
175+
176+
/**
177+
* Add a CLS event to the replay based on a CLS metric.
178+
*/
179+
export function getCumulativeLayoutShift(metric: {
180+
value: number;
181+
entries: PerformanceEntry[];
182+
}): ReplayPerformanceEntry<CumulativeLayoutShiftData> {
183+
const entries = metric.entries;
184+
const lastEntry = entries[entries.length - 1] as (PerformanceEntry & { element?: Element }) | undefined;
185+
const element = lastEntry ? lastEntry.element : undefined;
186+
187+
const value = metric.value;
188+
189+
const end = getAbsoluteTime(value);
190+
191+
const data: ReplayPerformanceEntry<CumulativeLayoutShiftData> = {
192+
type: 'web-vital',
193+
name: 'cumulative-layout-shift',
194+
start: end,
195+
end,
196+
data: {
197+
value,
198+
size: value,
199+
nodeId: element ? record.mirror.getId(element) : undefined,
200+
},
201+
};
202+
203+
return data;
204+
}
205+
206+
/**
207+
* Add a FID event to the replay based on a FID metric.
208+
*/
209+
export function getFirstInputDelay(metric: {
210+
value: number;
211+
entries: PerformanceEntry[];
212+
}): ReplayPerformanceEntry<FirstInputDelayData> {
213+
const entries = metric.entries;
214+
const lastEntry = entries[entries.length - 1] as (PerformanceEntry & { element?: Element }) | undefined;
215+
const element = lastEntry ? lastEntry.element : undefined;
216+
217+
const value = metric.value;
218+
219+
const end = getAbsoluteTime(value);
220+
221+
const data: ReplayPerformanceEntry<FirstInputDelayData> = {
222+
type: 'web-vital',
223+
name: 'first-input-delay',
224+
start: end,
225+
end,
226+
data: {
227+
value,
228+
size: value,
229+
nodeId: element ? record.mirror.getId(element) : undefined,
230+
},
231+
};
232+
233+
return data;
234+
}
235+
236+
/**
237+
* Add an INP event to the replay based on an INP metric.
238+
*/
239+
export function getInteractionToNextPaint(metric: {
240+
value: number;
241+
entries: PerformanceEntry[];
242+
}): ReplayPerformanceEntry<InteractionToNextPaintData> {
243+
const entries = metric.entries;
244+
const lastEntry = entries[entries.length - 1] as (PerformanceEntry & { element?: Element }) | undefined;
245+
const element = lastEntry ? lastEntry.element : undefined;
246+
247+
const value = metric.value;
248+
249+
const end = getAbsoluteTime(value);
250+
251+
const data: ReplayPerformanceEntry<InteractionToNextPaintData> = {
252+
type: 'web-vital',
253+
name: 'interaction-to-next-paint',
254+
start: end,
255+
end,
256+
data: {
257+
value,
258+
size: value,
259+
nodeId: element ? record.mirror.getId(element) : undefined,
260+
},
261+
};
262+
263+
return data;
264+
}

0 commit comments

Comments
 (0)