Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report scroll metrics when page is resized #2399

Merged
merged 23 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b621bac
before first scroll, report scroll metrics periodically
hamza-kadiri Aug 30, 2023
b825d9f
address code review comments
hamza-kadiri Sep 1, 2023
a1479d4
add unit test
hamza-kadiri Sep 1, 2023
c052d3c
update test
hamza-kadiri Sep 1, 2023
e4311c7
Merge remote-tracking branch 'origin/main' into hamza/report-scroll-m…
hamza-kadiri Sep 4, 2023
4523ca6
Merge remote-tracking branch 'origin/main' into hamza/report-scroll-m…
hamza-kadiri Sep 6, 2023
293a046
update scroll logic
hamza-kadiri Sep 7, 2023
e3d7d2e
round
hamza-kadiri Sep 7, 2023
1f91a98
add comment
hamza-kadiri Sep 7, 2023
49fef0a
fix error on safari
hamza-kadiri Sep 7, 2023
4279b8e
fix issues
hamza-kadiri Sep 7, 2023
61e0a7a
Merge remote-tracking branch 'origin/main' into hamza/report-scroll-m…
hamza-kadiri Sep 7, 2023
4a874c2
fix unit tests
hamza-kadiri Sep 12, 2023
c0c3496
Merge remote-tracking branch 'origin/main' into hamza/report-scroll-m…
hamza-kadiri Sep 12, 2023
bc4fea6
Empty commit
hamza-kadiri Sep 12, 2023
642eee9
Merge remote-tracking branch 'origin/main' into hamza/report-scroll-m…
hamza-kadiri Sep 13, 2023
2d46169
replace append by appendChild
hamza-kadiri Sep 13, 2023
ece6965
fix leak
hamza-kadiri Sep 13, 2023
1ac4b4d
🤞
hamza-kadiri Sep 13, 2023
6873d34
apply suggestions
hamza-kadiri Sep 13, 2023
9ddc001
update test
hamza-kadiri Sep 13, 2023
138c15a
remove support for ie
hamza-kadiri Sep 18, 2023
b6b89c4
Merge remote-tracking branch 'origin/main' into hamza/report-scroll-m…
hamza-kadiri Sep 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/rum-core/src/browser/scrollHeightObservable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Observable, setInterval, clearInterval } from '@datadog/browser-core'

export const SCROLL_HEIGHT_OBSERVABLE_INTERVAL_MS = 500

export function createScrollHeightObservable() {
const observable = new Observable<number>(() => {
const setIntervalId = setInterval(() => {
observable.notify(Math.round((document.scrollingElement || document.documentElement).scrollHeight))
hamza-kadiri marked this conversation as resolved.
Show resolved Hide resolved
}, SCROLL_HEIGHT_OBSERVABLE_INTERVAL_MS)

return () => clearInterval(setIntervalId)
})

return observable
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ClocksState, Duration, Observable } from '@datadog/browser-core'
import { noop } from '@datadog/browser-core'
import { createScrollHeightObservable } from '../../../../browser/scrollHeightObservable'
import type { ViewLoadingType } from '../../../../rawRumEvent.types'
import type { RumConfiguration } from '../../../configuration'
import type { LifeCycle } from '../../../lifeCycle'
Expand Down Expand Up @@ -36,17 +37,6 @@ export function trackCommonViewMetrics(
viewStart,
(newLoadingTime) => {
commonViewMetrics.loadingTime = newLoadingTime

// We compute scroll metrics at loading time to ensure we have scroll data when loading the view initially
// This is to ensure that we have the depth data even if the user didn't scroll or if the view is not scrollable.
const { scrollHeight, scrollDepth, scrollTop } = computeScrollValues()

commonViewMetrics.scroll = {
maxDepth: scrollDepth,
maxDepthScrollHeight: scrollHeight,
maxDepthTime: newLoadingTime,
maxDepthScrollTop: scrollTop,
}
scheduleViewUpdate()
}
)
Expand All @@ -57,6 +47,7 @@ export function trackCommonViewMetrics(
(newScrollMetrics) => {
commonViewMetrics.scroll = newScrollMetrics
},
createScrollHeightObservable(),
computeScrollValues
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import type { RelativeTime, TimeStamp, Duration } from '@datadog/browser-core'
import { DOM_EVENT } from '@datadog/browser-core'
import { Observable, DOM_EVENT } from '@datadog/browser-core'
import type { Clock } from '@datadog/browser-core/test'
import { createNewEvent, mockClock } from '@datadog/browser-core/test'
import type { TestSetupBuilder } from '../../../../../test'
import { setup } from '../../../../../test'
import { PAGE_ACTIVITY_END_DELAY, PAGE_ACTIVITY_VALIDATION_DELAY } from '../../../waitPageActivityEnd'
import type { RumConfiguration } from '../../../configuration'
import { THROTTLE_VIEW_UPDATE_PERIOD } from '../trackViews'
import type { ViewTest } from '../setupViewTest.specHelper'
import { setupViewTest } from '../setupViewTest.specHelper'
import { THROTTLE_SCROLL_DURATION, type ScrollMetrics, trackScrollMetrics } from './trackScrollMetrics'

const BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY = (PAGE_ACTIVITY_VALIDATION_DELAY * 0.8) as Duration

const AFTER_PAGE_ACTIVITY_END_DELAY = PAGE_ACTIVITY_END_DELAY * 1.1

describe('trackScrollMetrics', () => {
let setupBuilder: TestSetupBuilder
let viewTest: ViewTest
Expand All @@ -37,14 +31,22 @@ describe('trackScrollMetrics', () => {
let stopTrackScrollMetrics: () => void
let clock: Clock
let configuration: RumConfiguration
const scrollHeightObservable: Observable<number> = new Observable()

const getMetrics = jasmine.createSpy('getMetrics')

const newScroll = (scrollParams: { scrollHeight: number; scrollDepth: number; scrollTop: number }) => {
const mockGetMetrics = (scrollParams: { scrollHeight: number; scrollDepth: number; scrollTop: number }) => {
getMetrics.and.returnValue(scrollParams)
}

window.dispatchEvent(createNewEvent(DOM_EVENT.SCROLL))
const emitObservableValue = (scrollParams: { scrollHeight: number; scrollDepth: number; scrollTop: number }) => {
mockGetMetrics({ ...scrollParams, scrollHeight: 0 })
scrollHeightObservable.notify(scrollParams.scrollHeight)
}

const newScroll = (scrollParams: { scrollHeight: number; scrollDepth: number; scrollTop: number }) => {
mockGetMetrics(scrollParams)
window.dispatchEvent(createNewEvent(DOM_EVENT.SCROLL))
clock.tick(THROTTLE_SCROLL_DURATION)
}

Expand All @@ -55,6 +57,7 @@ describe('trackScrollMetrics', () => {
configuration,
{ relative: 0 as RelativeTime, timeStamp: 0 as TimeStamp },
(s) => (scrollMetrics = s),
scrollHeightObservable,
getMetrics
).stop
})
Expand All @@ -65,9 +68,31 @@ describe('trackScrollMetrics', () => {
clock.cleanup()
})

it('should update scroll metrics when scrolling the first time', () => {
newScroll({ scrollHeight: 1000, scrollDepth: 500, scrollTop: 100 })
it('should initially be undefined', () => {
expect(scrollMetrics).toBeUndefined()
})

it('should update scroll metrics before scrolling', () => {
emitObservableValue({
scrollHeight: 99,
scrollDepth: 99,
scrollTop: 99,
})
expect(scrollMetrics).toEqual({
maxDepthScrollHeight: 99,
maxDepth: 99,
maxDepthScrollTop: 99,
maxDepthTime: 0 as Duration,
})
})

it('should update scroll metrics when scrolling the first time, ignoring subsequent values emitted by the observable', () => {
newScroll({ scrollHeight: 1000, scrollDepth: 500, scrollTop: 100 })
emitObservableValue({
scrollHeight: 9999,
scrollDepth: 9999,
scrollTop: 9999,
})
expect(scrollMetrics).toEqual({
maxDepthScrollHeight: 1000,
maxDepth: 500,
Expand All @@ -76,11 +101,16 @@ describe('trackScrollMetrics', () => {
})
})

it('should update scroll metrics when scroll depth has increased', () => {
it('should update scroll metrics when scroll depth has increased, still ignoring values emitted by the observable', () => {
newScroll({ scrollHeight: 1000, scrollDepth: 500, scrollTop: 100 })

newScroll({ scrollHeight: 1000, scrollDepth: 600, scrollTop: 200 })

emitObservableValue({
scrollHeight: 9999,
scrollDepth: 9999,
scrollTop: 9999,
})

expect(scrollMetrics).toEqual({
maxDepthScrollHeight: 1000,
maxDepth: 600,
Expand All @@ -91,9 +121,14 @@ describe('trackScrollMetrics', () => {

it('should NOT update scroll metrics when scroll depth has not increased', () => {
newScroll({ scrollHeight: 1000, scrollDepth: 600, scrollTop: 200 })

newScroll({ scrollHeight: 1000, scrollDepth: 450, scrollTop: 50 })

emitObservableValue({
scrollHeight: 9999,
scrollDepth: 9999,
scrollTop: 9999,
})

expect(scrollMetrics).toEqual({
maxDepthScrollHeight: 1000,
maxDepth: 600,
Expand All @@ -102,41 +137,4 @@ describe('trackScrollMetrics', () => {
})
})
})

describe('on load', () => {
beforeEach(() => {
setupBuilder.withFakeClock()
})

it('should have an undefined loading time and empty scroll metrics if there is no activity on a route change', () => {
const { clock } = setupBuilder.build()
const { getViewUpdate, getViewUpdateCount, startView } = viewTest

startView()
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

expect(getViewUpdateCount()).toEqual(3)
expect(getViewUpdate(2).commonViewMetrics.loadingTime).toBeUndefined()
expect(getViewUpdate(2).commonViewMetrics.scroll).toEqual(undefined)
})

it('should have a loading time equal to the activity time and scroll metrics if there is a unique activity on a route change', () => {
const { domMutationObservable, clock } = setupBuilder.build()
const { getViewUpdate, startView } = viewTest

startView()
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()
clock.tick(AFTER_PAGE_ACTIVITY_END_DELAY)
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

expect(getViewUpdate(3).commonViewMetrics.loadingTime).toEqual(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
expect(getViewUpdate(3).commonViewMetrics.scroll).toEqual({
maxDepthScrollHeight: jasmine.any(Number),
maxDepth: jasmine.any(Number),
maxDepthTime: jasmine.any(Number),
maxDepthScrollTop: jasmine.any(Number),
})
})
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClocksState, Duration } from '@datadog/browser-core'
import type { ClocksState, Duration, Observable } from '@datadog/browser-core'
import { ONE_SECOND, elapsed, relativeNow, throttle, addEventListener, DOM_EVENT } from '@datadog/browser-core'
import type { RumConfiguration } from '../../../configuration'
import { getScrollY } from '../../../../browser/scroll'
Expand All @@ -18,11 +18,28 @@ export function trackScrollMetrics(
configuration: RumConfiguration,
viewStart: ClocksState,
callback: (scrollMetrics: ScrollMetrics) => void,
scrollHeightObservable: Observable<number>,
getScrollValues = computeScrollValues
) {
let maxDepth = 0

const subscription = scrollHeightObservable.subscribe((scrollHeight) => {
const { scrollDepth, scrollTop } = getScrollValues()

const now = relativeNow()
const maxDepthTime = elapsed(viewStart.relative, now)
maxDepth = scrollDepth
callback({
maxDepth,
maxDepthScrollHeight: scrollHeight,
maxDepthTime,
maxDepthScrollTop: scrollTop,
})
hamza-kadiri marked this conversation as resolved.
Show resolved Hide resolved
})

const handleScrollEvent = throttle(
() => {
subscription.unsubscribe()
const { scrollHeight, scrollDepth, scrollTop } = getScrollValues()

if (scrollDepth > maxDepth) {
Expand All @@ -48,6 +65,7 @@ export function trackScrollMetrics(
return {
stop: () => {
handleScrollEvent.cancel()
subscription.unsubscribe()
stop()
},
}
Expand Down