Skip to content

Commit

Permalink
✨ Capture previous and current rects in CLS attribution data (#3269)
Browse files Browse the repository at this point in the history
  • Loading branch information
sethfowler-datadog authored Jan 15, 2025
1 parent 4623045 commit 2ed808b
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 22 deletions.
10 changes: 7 additions & 3 deletions packages/rum-core/src/browser/performanceObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,18 @@ export interface RumPerformanceEventTiming {
name: string
}

export interface RumLayoutShiftAttribution {
node: Node | null
previousRect: DOMRectReadOnly
currentRect: DOMRectReadOnly
}

export interface RumLayoutShiftTiming {
entryType: RumPerformanceEntryType.LAYOUT_SHIFT
startTime: RelativeTime
value: number
hadRecentInput: boolean
sources?: Array<{
node?: Node
}>
sources: RumLayoutShiftAttribution[]
}

// Documentation https://developer.chrome.com/docs/web-platform/long-animation-frames#better-attribution
Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/domain/view/trackViews.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,8 @@ describe('view metrics', () => {
value: 0.1,
targetSelector: undefined,
time: clock.relative(0),
previousRect: undefined,
currentRect: undefined,
})
})

Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/domain/view/viewCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ describe('viewCollection', () => {
score: 1,
timestamp: (100 * 1e6) as ServerDuration,
target_selector: undefined,
previous_rect: undefined,
current_rect: undefined,
},
fcp: {
timestamp: (10 * 1e6) as ServerDuration,
Expand Down
2 changes: 2 additions & 0 deletions packages/rum-core/src/domain/view/viewCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ function computeViewPerformanceData(
score: cumulativeLayoutShift.value,
timestamp: toServerDuration(cumulativeLayoutShift.time),
target_selector: cumulativeLayoutShift.targetSelector,
previous_rect: cumulativeLayoutShift.previousRect,
current_rect: cumulativeLayoutShift.currentRect,
},
fcp: firstContentfulPaint && { timestamp: toServerDuration(firstContentfulPaint) },
fid: firstInput && {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ describe('trackCumulativeLayoutShift', () => {
value: 0.3,
time: 2 as RelativeTime,
targetSelector: undefined,
previousRect: undefined,
currentRect: undefined,
})
})

Expand Down Expand Up @@ -139,6 +141,8 @@ describe('trackCumulativeLayoutShift', () => {
value: 0.3,
time: 1 as RelativeTime,
targetSelector: undefined,
previousRect: undefined,
currentRect: undefined,
})
})

Expand All @@ -158,6 +162,8 @@ describe('trackCumulativeLayoutShift', () => {
value: 0.6,
time: 999 as RelativeTime,
targetSelector: undefined,
previousRect: undefined,
currentRect: undefined,
})
})

Expand Down Expand Up @@ -210,6 +216,8 @@ describe('trackCumulativeLayoutShift', () => {
value: 0.5,
time: 5002 as RelativeTime,
targetSelector: undefined,
previousRect: undefined,
currentRect: undefined,
})
})

Expand Down Expand Up @@ -237,7 +245,23 @@ describe('trackCumulativeLayoutShift', () => {

notifyPerformanceEntries([
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
sources: [{ node: textNode }, { node: divElement }, { node: textNode }],
sources: [
{
node: textNode,
previousRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
currentRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
},
{
node: divElement,
previousRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
currentRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
},
{
node: textNode,
previousRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
currentRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
},
],
}),
])

Expand Down Expand Up @@ -265,15 +289,21 @@ describe('trackCumulativeLayoutShift', () => {
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.2,
startTime: 1001 as RelativeTime,
sources: [{ node: divElement }],
sources: [
{
node: divElement,
previousRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
currentRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
},
],
}),
])

expect(clsCallback.calls.mostRecent().args[0].value).toEqual(0.2)
expect(clsCallback.calls.mostRecent().args[0].targetSelector).toEqual(undefined)
})

it('should get the target element and time of the largest layout shift', () => {
it('should get the target element, time, and rects of the largest layout shift', () => {
startCLSTracking()
const divElement = appendElement('<div id="div-element"></div>')

Expand All @@ -285,7 +315,13 @@ describe('trackCumulativeLayoutShift', () => {
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.2,
startTime: 1 as RelativeTime,
sources: [{ node: divElement }],
sources: [
{
node: divElement,
previousRect: DOMRectReadOnly.fromRect({ x: 0, y: 0, width: 10, height: 10 }),
currentRect: DOMRectReadOnly.fromRect({ x: 50, y: 50, width: 10, height: 10 }),
},
],
}),
])
notifyPerformanceEntries([
Expand All @@ -311,6 +347,8 @@ describe('trackCumulativeLayoutShift', () => {
value: 0.5,
time: 1 as RelativeTime,
targetSelector: '#div-element',
previousRect: { x: 0, y: 0, width: 10, height: 10 },
currentRect: { x: 50, y: 50, width: 10, height: 10 },
})
})
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import { round, ONE_SECOND, noop, elapsed } from '@datadog/browser-core'
import type { Duration, RelativeTime, WeakRef, WeakRefConstructor } from '@datadog/browser-core'
import { isElementNode } from '../../../browser/htmlDomUtils'
import type { RumLayoutShiftTiming } from '../../../browser/performanceObservable'
import type { RumLayoutShiftAttribution, RumLayoutShiftTiming } from '../../../browser/performanceObservable'
import {
supportPerformanceTimingEvent,
RumPerformanceEntryType,
createPerformanceObservable,
} from '../../../browser/performanceObservable'
import { getSelectorFromElement } from '../../getSelectorFromElement'
import type { RumConfiguration } from '../../configuration'
import type { RumRect } from '../../../rumEvent.types'

declare const WeakRef: WeakRefConstructor

export interface CumulativeLayoutShift {
value: number
targetSelector?: string
time?: Duration
previousRect?: RumRect
currentRect?: RumRect
}

declare const WeakRef: WeakRefConstructor
interface LayoutShiftInstance {
target: WeakRef<Element> | undefined
time: Duration
previousRect: DOMRectReadOnly | undefined
currentRect: DOMRectReadOnly | undefined
}

/**
* Track the cumulative layout shifts (CLS).
Expand Down Expand Up @@ -47,8 +57,7 @@ export function trackCumulativeLayoutShift(
}

let maxClsValue = 0
let maxClsTarget: WeakRef<HTMLElement> | undefined
let maxClsStartTime: Duration | undefined
let biggestShift: LayoutShiftInstance | undefined

// if no layout shift happen the value should be reported as 0
callback({
Expand All @@ -68,19 +77,25 @@ export function trackCumulativeLayoutShift(
const { cumulatedValue, isMaxValue } = window.update(entry)

if (isMaxValue) {
const target = getTargetFromSource(entry.sources)
maxClsTarget = target ? new WeakRef(target) : undefined
maxClsStartTime = elapsed(viewStart, entry.startTime)
const attribution = getFirstElementAttribution(entry.sources)
biggestShift = {
target: attribution?.node ? new WeakRef(attribution.node) : undefined,
time: elapsed(viewStart, entry.startTime),
previousRect: attribution?.previousRect,
currentRect: attribution?.currentRect,
}
}

if (cumulatedValue > maxClsValue) {
maxClsValue = cumulatedValue
const target = maxClsTarget?.deref()
const target = biggestShift?.target?.deref()

callback({
value: round(maxClsValue, 4),
targetSelector: target && getSelectorFromElement(target, configuration.actionNameAttribute),
time: maxClsStartTime,
time: biggestShift?.time,
previousRect: biggestShift?.previousRect ? asRumRect(biggestShift.previousRect) : undefined,
currentRect: biggestShift?.currentRect ? asRumRect(biggestShift.currentRect) : undefined,
})
}
}
Expand All @@ -93,12 +108,16 @@ export function trackCumulativeLayoutShift(
}
}

function getTargetFromSource(sources?: Array<{ node?: Node }>) {
if (!sources) {
return
}
function getFirstElementAttribution(
sources: RumLayoutShiftAttribution[]
): (RumLayoutShiftAttribution & { node: Element }) | undefined {
return sources.find(
(source): source is RumLayoutShiftAttribution & { node: Element } => !!source.node && isElementNode(source.node)
)
}

return sources.find((source): source is { node: HTMLElement } => !!source.node && isElementNode(source.node))?.node
function asRumRect({ x, y, width, height }: DOMRectReadOnly): RumRect {
return { x, y, width, height }
}

export const MAX_WINDOW_DURATION = 5 * ONE_SECOND
Expand Down
9 changes: 9 additions & 0 deletions packages/rum-core/src/rawRumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ export interface ViewPerformanceData {
score: number
timestamp?: ServerDuration
target_selector?: string
previous_rect?: RumRect
current_rect?: RumRect
}
fcp?: {
timestamp: number
Expand All @@ -178,6 +180,13 @@ export interface ViewPerformanceData {
}
}

export interface RumRect {
x: number
y: number
width: number
height: number
}

export type PageStateServerEntry = { state: PageState; start: ServerDuration }

export const enum ViewLoadingType {
Expand Down
7 changes: 6 additions & 1 deletion packages/rum-core/test/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { Context, Duration, RelativeTime, ServerDuration, TimeStamp } from '@datadog/browser-core'
import { combine, ErrorHandling, ErrorSource, generateUUID, relativeNow, ResourceType } from '@datadog/browser-core'
import { RumPerformanceEntryType, type EntryTypeToReturnType } from '../src/browser/performanceObservable'
import {
type RumLayoutShiftAttribution,
RumPerformanceEntryType,
type EntryTypeToReturnType,
} from '../src/browser/performanceObservable'
import type { RawRumEvent } from '../src/rawRumEvent.types'
import { VitalType, ActionType, RumEventType, ViewLoadingType, RumLongTaskEntryType } from '../src/rawRumEvent.types'

Expand Down Expand Up @@ -152,6 +156,7 @@ export function createPerformanceEntry<T extends RumPerformanceEntryType>(
startTime: relativeNow(),
hadRecentInput: false,
value: 0.1,
sources: [] as RumLayoutShiftAttribution[],
...overrides,
} as EntryTypeToReturnType[T]
case RumPerformanceEntryType.PAINT:
Expand Down

0 comments on commit 2ed808b

Please sign in to comment.