Skip to content

Commit

Permalink
fixup! fixup! 🐛 [RUM-3708] slidingSessionWindow clear window after 5 …
Browse files Browse the repository at this point in the history
…seconds
  • Loading branch information
thomas-lebeau committed May 13, 2024
1 parent 43a9ad0 commit 2482068
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 171 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { resetExperimentalFeatures } from '@datadog/browser-core'
import type { Clock } from '@datadog/browser-core/test'
import { mockClock } from '@datadog/browser-core/test'
import type { TestSetupBuilder } from '../../../../test'
import { appendElement, appendText, createPerformanceEntry, setup } from '../../../../test'
import { LifeCycleEventType } from '../../lifeCycle'
import { RumPerformanceEntryType } from '../../../browser/performanceCollection'
import type { CumulativeLayoutShift } from './trackCumulativeLayoutShift'
import { slidingSessionWindow, trackCumulativeLayoutShift } from './trackCumulativeLayoutShift'
import { trackCumulativeLayoutShift } from './trackCumulativeLayoutShift'

describe('trackCumulativeLayoutShift', () => {
fdescribe('trackCumulativeLayoutShift', () => {
let setupBuilder: TestSetupBuilder
let isLayoutShiftSupported: boolean
let originalSupportedEntryTypes: PropertyDescriptor | undefined
Expand Down Expand Up @@ -184,7 +182,7 @@ describe('trackCumulativeLayoutShift', () => {
expect(clsCallback.calls.mostRecent().args[0].targetSelector).toEqual('#div-element')
})

it('should not return the target element when the element is detached from the DOM', () => {
it('should not return the target element when the element is detached from the DOM before the performance entry event is triggered', () => {
const { lifeCycle, clock } = setupBuilder.withFakeClock().build()

// first session window
Expand All @@ -201,13 +199,14 @@ describe('trackCumulativeLayoutShift', () => {
// second session window
// first shift with an element
const divElement = appendElement('<div id="div-element"></div>')
divElement.remove()

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.2,
sources: [{ node: divElement }],
}),
])
divElement.remove()
// second shift that makes this window the maximum CLS
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }),
Expand All @@ -216,119 +215,32 @@ describe('trackCumulativeLayoutShift', () => {
expect(clsCallback.calls.mostRecent().args[0].value).toEqual(0.3)
expect(clsCallback.calls.mostRecent().args[0].targetSelector).toEqual(undefined)
})
})
})

describe('slidingSessionWindow', () => {
let clock: Clock

beforeEach(() => {
clock = mockClock()
})

afterEach(() => {
clock.cleanup()
})

it('should return 0 if no layout shift happen', () => {
const window = slidingSessionWindow()

expect(window.value()).toEqual(0)
})

it('should accumulate layout shift values', () => {
const window = slidingSessionWindow()

window.update(createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }))
window.update(createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.5 }))

expect(window.value()).toEqual(0.7)
})

it('should return the element with the largest layout shift', () => {
const window = slidingSessionWindow()

const textNode = appendText('text')
const divElement = appendElement('<div id="div-element"></div>')

window.update(
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.2,
sources: [{ node: textNode }, { node: divElement }, { node: textNode }],
})
)

expect(window.largestLayoutShiftTarget()).toEqual(divElement)
})

it('should create a new session window if the gap is more than 1 second', () => {
const window = slidingSessionWindow()

window.update(createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }))

clock.tick(1001)

window.update(createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }))

expect(window.value()).toEqual(0.1)
})

it('should create a new session window if the current session window is more than 5 second', () => {
const window = slidingSessionWindow()

window.update(createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0 }))

for (let i = 0; i < 6; i += 1) {
clock.tick(999)
window.update(createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }))
} // window 1: 0.5 | window 2: 0.1

expect(window.value()).toEqual(0.1)
})

it('should return largest layout shift target element', () => {
const window = slidingSessionWindow()
const firstElement = appendElement('<div id="first-element"></div>')
const secondElement = appendElement('<div id="second-element"></div>')
const thirdElement = appendElement('<div id="third-element"></div>')

window.update(
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.2,
sources: [{ node: firstElement }],
})
)

window.update(
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.3,
sources: [{ node: secondElement }],
})
)

window.update(
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.1,
sources: [{ node: thirdElement }],
})
)

expect(window.largestLayoutShiftTarget()).toEqual(secondElement)
})

it('should not retain the largest layout shift target element after 5 seconds', () => {
const window = slidingSessionWindow()
const divElement = appendElement('<div id="div-element"></div>')
fit('should get the target element of the largest layout shift', () => {
const { lifeCycle, clock } = setupBuilder.withFakeClock().build()
const divElement = appendElement('<div id="div-element"></div>')

window.update(
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, {
value: 0.2,
sources: [{ node: divElement }],
})
)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.1 }),
])
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2, sources: [{ node: divElement }] }),
])
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }),
])

clock.tick(5001)
// second session window
clock.tick(5001)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }),
])
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createPerformanceEntry(RumPerformanceEntryType.LAYOUT_SHIFT, { value: 0.2 }),
])

expect(window.largestLayoutShiftTarget()).toBeUndefined()
expect(clsCallback).toHaveBeenCalledTimes(4)
expect(clsCallback.calls.mostRecent().args[0]).toEqual({ value: 0.5, targetSelector: '#div-element' })
})
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { round, find, ONE_SECOND, noop, setTimeout, clearTimeout } from '@datadog/browser-core'
import type { RelativeTime, TimeoutId } from '@datadog/browser-core'
import { round, find, ONE_SECOND, noop } from '@datadog/browser-core'
import type { RelativeTime } from '@datadog/browser-core'
import { isElementNode } from '../../../browser/htmlDomUtils'
import type { LifeCycle } from '../../lifeCycle'
import { LifeCycleEventType } from '../../lifeCycle'
Expand Down Expand Up @@ -42,6 +42,7 @@ export function trackCumulativeLayoutShift(
}

let maxClsValue = 0
let maxClsTargetSelector: string | undefined

// if no layout shift happen the value should be reported as 0
callback({
Expand All @@ -52,25 +53,21 @@ export function trackCumulativeLayoutShift(
const { unsubscribe: stop } = lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType === RumPerformanceEntryType.LAYOUT_SHIFT && !entry.hadRecentInput) {
window.update(entry)

if (window.value() > maxClsValue) {
maxClsValue = window.value()
const cls = round(maxClsValue, 4)
const clsTarget = window.largestLayoutShiftTarget()
let clsTargetSelector

if (
clsTarget &&
// Check if the CLS target have been removed from the DOM between the time we collect the target reference and when we compute the selector
clsTarget.isConnected
) {
clsTargetSelector = getSelectorFromElement(clsTarget, configuration.actionNameAttribute)
}
const { cumulatedValue, isMaxValue } = window.update(entry)

if (isMaxValue) {
const maxClsTarget = getTargetSelctorFromSource(entry.sources)
maxClsTargetSelector = maxClsTarget?.isConnected
? getSelectorFromElement(maxClsTarget, configuration.actionNameAttribute)
: undefined
}

if (cumulatedValue > maxClsValue) {
maxClsValue = cumulatedValue

callback({
value: cls,
targetSelector: clsTargetSelector,
value: round(maxClsValue, 4),
targetSelector: maxClsTargetSelector,
})
}
}
Expand All @@ -82,60 +79,53 @@ export function trackCumulativeLayoutShift(
}
}

const MAX_WINDOW_LENGTH = 5 * ONE_SECOND
function getTargetSelctorFromSource(sources?: Array<{ node?: Node }>) {
if (!sources) {
return
}

return find(sources, (source): source is { node: HTMLElement } => !!source.node && isElementNode(source.node))?.node
}

const MAX_WINDOW_DURATION = 5 * ONE_SECOND
const MAX_UPDATE_GAP = ONE_SECOND

export function slidingSessionWindow() {
let value = 0
function slidingSessionWindow() {
let cumulatedValue = 0
let startTime: RelativeTime
let endTime: RelativeTime

let largestLayoutShift = 0
let largestLayoutShiftTarget: HTMLElement | undefined
let largestLayoutShiftTime: RelativeTime
let timeoutId: TimeoutId

function resetWindow(entry: RumLayoutShiftTiming) {
startTime = endTime = entry.startTime
value = entry.value
largestLayoutShift = 0
largestLayoutShiftTarget = undefined
largestLayoutShiftTime = entry.startTime
}
let maxValue = 0

return {
update: (entry: RumLayoutShiftTiming) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => resetWindow(entry), MAX_WINDOW_LENGTH)

const shouldCreateNewWindow =
startTime === undefined ||
entry.startTime - endTime >= MAX_UPDATE_GAP ||
entry.startTime - startTime >= MAX_WINDOW_LENGTH
entry.startTime - startTime >= MAX_WINDOW_DURATION

let isMaxValue: boolean

if (shouldCreateNewWindow) {
resetWindow(entry)
startTime = endTime = entry.startTime
maxValue = cumulatedValue = entry.value
isMaxValue = true
} else {
value += entry.value
cumulatedValue += entry.value
endTime = entry.startTime
isMaxValue = false
}

if (entry.value > largestLayoutShift) {
largestLayoutShift = entry.value
largestLayoutShiftTime = entry.startTime
if (entry.sources?.length) {
largestLayoutShiftTarget = find(
entry.sources,
(s): s is { node: HTMLElement } => !!s.node && isElementNode(s.node)
)?.node
} else {
largestLayoutShiftTarget = undefined
}
if (entry.value > maxValue) {
maxValue = entry.value
isMaxValue = true
}

return {
cumulatedValue,
isMaxValue,
}
},
value: () => value,
largestLayoutShiftTarget: () => largestLayoutShiftTarget,
largestLayoutShiftTime: () => largestLayoutShiftTime,
value: () => cumulatedValue,
}
}

Expand Down

0 comments on commit 2482068

Please sign in to comment.