Skip to content

Commit

Permalink
Collect CLS target selector
Browse files Browse the repository at this point in the history
  • Loading branch information
amortemousque committed Sep 6, 2023
1 parent 73b215a commit b842e5e
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ describe('viewCollection', () => {
count: 10,
},
cumulative_layout_shift: 1,
cumulative_layout_shift_target_selector: undefined,
custom_timings: {
bar: (20 * 1e6) as ServerDuration,
foo: (10 * 1e6) as ServerDuration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function processViewUpdate(
count: view.eventCounts.frustrationCount,
},
cumulative_layout_shift: view.commonViewMetrics.cumulativeLayoutShift,
cumulative_layout_shift_target_selector: view.commonViewMetrics.cumulativeLayoutShiftTargetSelector,
first_byte: toServerDuration(view.initialViewMetrics.firstByte),
dom_complete: toServerDuration(view.initialViewMetrics.domComplete),
dom_content_loaded: toServerDuration(view.initialViewMetrics.domContentLoaded),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { trackInteractionToNextPaint } from './trackInteractionToNextPaint'
export interface CommonViewMetrics {
loadingTime?: Duration
cumulativeLayoutShift?: number
cumulativeLayoutShiftTargetSelector?: string
interactionToNextPaint?: Duration
interactionToNextPaintTargetSelector?: string
scroll?: ScrollMetrics
Expand Down Expand Up @@ -65,10 +66,12 @@ export function trackCommonViewMetrics(
if (isLayoutShiftSupported()) {
commonViewMetrics.cumulativeLayoutShift = 0
;({ stop: stopCLSTracking } = trackCumulativeLayoutShift(
configuration,
lifeCycle,
webVitalTelemetryDebug,
(cumulativeLayoutShift) => {
(cumulativeLayoutShift, cumulativeLayoutShiftTargetSelector) => {
commonViewMetrics.cumulativeLayoutShift = cumulativeLayoutShift
commonViewMetrics.cumulativeLayoutShiftTargetSelector = cumulativeLayoutShiftTargetSelector
scheduleViewUpdate()
}
))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
import { relativeNow } from '@datadog/browser-core'
import {
ExperimentalFeature,
addExperimentalFeatures,
assign,
relativeNow,
resetExperimentalFeatures,
} from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../../../../test'
import { setup } from '../../../../../test'
import type { LifeCycle } from '../../../lifeCycle'
import { LifeCycleEventType } from '../../../lifeCycle'
import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews'
import type { ViewTest } from '../setupViewTest.specHelper'
import { setupViewTest } from '../setupViewTest.specHelper'
import type { RumLayoutShiftTiming } from '../../../../browser/performanceCollection'

describe('trackCumulativeLayoutShift', () => {
let setupBuilder: TestSetupBuilder
let viewTest: ViewTest
let isLayoutShiftSupported: boolean
let originalSupportedEntryTypes: PropertyDescriptor | undefined

function newLayoutShift(lifeCycle: LifeCycle, { value = 0.1, hadRecentInput = false }) {
function newLayoutShift(lifeCycle: LifeCycle, overrides: Partial<RumLayoutShiftTiming>) {
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
{
entryType: 'layout-shift',
startTime: relativeNow(),
hadRecentInput,
value,
},
assign(
{
entryType: 'layout-shift',
startTime: relativeNow(),
hadRecentInput: false,
value: 0.1,
},
overrides
),
])
}

beforeEach(() => {
if (!('PerformanceObserver' in window) || !('supportedEntryTypes' in PerformanceObserver)) {
pending('No PerformanceObserver support')
}

setupBuilder = setup()
.withFakeLocation('/foo')
.beforeBuild((buildContext) => {
Expand Down Expand Up @@ -150,4 +161,46 @@ describe('trackCumulativeLayoutShift', () => {
expect(getViewUpdateCount()).toEqual(3)
expect(getViewUpdate(2).commonViewMetrics.cumulativeLayoutShift).toBe(0.5)
})

describe('cls target element', () => {
let sandbox: HTMLDivElement

beforeEach(() => {
sandbox = document.createElement('div')
document.body.appendChild(sandbox)
})

afterEach(() => {
resetExperimentalFeatures()
sandbox.remove()
})

it('should return the first target element selector amongst all the shifted nodes when FF enabled', () => {
addExperimentalFeatures([ExperimentalFeature.WEB_VITALS_ATTRIBUTION])
const { lifeCycle } = setupBuilder.build()
const { getViewUpdate, getViewUpdateCount } = viewTest

const textNode = sandbox.appendChild(document.createTextNode(''))
const divElement = sandbox.appendChild(document.createElement('div'))
divElement.setAttribute('id', 'div-element')

newLayoutShift(lifeCycle, { sources: [{ node: textNode }, { node: divElement }, { node: textNode }] })

expect(getViewUpdateCount()).toEqual(1)
expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShiftTargetSelector).toBe('#div-element')
})

it('should not return the target element selector when FF disabled', () => {
const { lifeCycle } = setupBuilder.build()
const { getViewUpdate, getViewUpdateCount } = viewTest

const divElement = sandbox.appendChild(document.createElement('div'))
divElement.setAttribute('id', 'div-element')

newLayoutShift(lifeCycle, { sources: [{ node: divElement }] })

expect(getViewUpdateCount()).toEqual(1)
expect(getViewUpdate(0).commonViewMetrics.cumulativeLayoutShiftTargetSelector).toBe(undefined)
})
})
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { round, type RelativeTime, find, ONE_SECOND } from '@datadog/browser-core'
import {
round,
type RelativeTime,
find,
ONE_SECOND,
isExperimentalFeatureEnabled,
ExperimentalFeature,
} from '@datadog/browser-core'
import { isElementNode } from '../../../../browser/htmlDomUtils'
import type { LifeCycle } from '../../../lifeCycle'
import { LifeCycleEventType } from '../../../lifeCycle'
import { supportPerformanceTimingEvent, type RumLayoutShiftTiming } from '../../../../browser/performanceCollection'
import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug'
import { getSelectorFromElement } from '../../../getSelectorFromElement'
import type { RumConfiguration } from '../../../configuration'

/**
* Track the cumulative layout shifts (CLS).
Expand All @@ -21,10 +31,12 @@ import type { WebVitalTelemetryDebug } from '../startWebVitalTelemetryDebug'
* https://web.dev/evolving-cls/
* Reference implementation: https://github.com/GoogleChrome/web-vitals/blob/master/src/getCLS.ts
*/

export function trackCumulativeLayoutShift(
configuration: RumConfiguration,
lifeCycle: LifeCycle,
webVitalTelemetryDebug: WebVitalTelemetryDebug,
callback: (layoutShift: number) => void
callback: (cumulativeLayoutShift: number, cumulativeLayoutShiftTargetSelector?: string) => void
) {
let maxClsValue = 0

Expand All @@ -39,17 +51,23 @@ export function trackCumulativeLayoutShift(
if (window.value() > maxClsValue) {
maxClsValue = window.value()
const cls = round(maxClsValue, 4)
const clsTarget = window.largestLayoutShiftTarget()
let cslTargetSelector

if (isExperimentalFeatureEnabled(ExperimentalFeature.WEB_VITALS_ATTRIBUTION) && clsTarget) {
cslTargetSelector = getSelectorFromElement(clsTarget, configuration.actionNameAttribute)
}

callback(cls, cslTargetSelector)

if (!clsAttributionCollected) {
clsAttributionCollected = true
webVitalTelemetryDebug.addWebVitalTelemetryDebug(
'CLS',
window.largestLayoutShiftNode(),
window.largestLayoutShiftTarget(),
window.largestLayoutShiftTime()
)
}

callback(cls)
}
}
}
Expand All @@ -66,7 +84,7 @@ function slidingSessionWindow() {
let endTime: RelativeTime

let largestLayoutShift = 0
let largestLayoutShiftNode: Node | undefined
let largestLayoutShiftTarget: HTMLElement | undefined
let largestLayoutShiftTime: RelativeTime

return {
Expand All @@ -79,7 +97,7 @@ function slidingSessionWindow() {
startTime = endTime = entry.startTime
value = entry.value
largestLayoutShift = 0
largestLayoutShiftNode = undefined
largestLayoutShiftTarget = undefined
} else {
value += entry.value
endTime = entry.startTime
Expand All @@ -90,15 +108,17 @@ function slidingSessionWindow() {
largestLayoutShiftTime = entry.startTime

if (entry.sources?.length) {
const largestLayoutShiftSource = find(entry.sources, (s) => s.node?.nodeType === 1) || entry.sources[0]
largestLayoutShiftNode = largestLayoutShiftSource.node
largestLayoutShiftTarget = find(
entry.sources,
(s): s is { node: HTMLElement } => !!s.node && isElementNode(s.node)
)?.node
} else {
largestLayoutShiftNode = undefined
largestLayoutShiftTarget = undefined
}
}
},
value: () => value,
largestLayoutShiftNode: () => largestLayoutShiftNode,
largestLayoutShiftTarget: () => largestLayoutShiftTarget,
largestLayoutShiftTime: () => largestLayoutShiftTime,
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/rum-core/src/rawRumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export interface RawRumViewEvent {
interaction_to_next_paint?: ServerDuration
interaction_to_next_paint_target_selector?: string
cumulative_layout_shift?: number
cumulative_layout_shift_target_selector?: string
custom_timings?: {
[key: string]: ServerDuration
}
Expand Down

0 comments on commit b842e5e

Please sign in to comment.