Skip to content

Commit

Permalink
πŸ‘Œ Create a viewportObservable
Browse files Browse the repository at this point in the history
Mutualize recorder and rum code to avoid reflow as much a possible
  • Loading branch information
amortemousque committed Jun 13, 2022
1 parent d353050 commit 40f03ee
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 85 deletions.
54 changes: 54 additions & 0 deletions packages/core/src/browser/viewportObservable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Clock, createNewEvent } from '../../test/specHelper'
import { mockClock } from '../../test/specHelper'
import type { Subscription } from '../tools/observable'
import type { ViewportDimension } from './viewportObservable'
import { getViewportDimension, initViewportObservable } from './viewportObservable'

describe('viewportObservable', () => {
let viewportSubscription: Subscription
let viewportDimension: ViewportDimension
let clock: Clock

beforeEach(() => {
viewportSubscription = initViewportObservable().subscribe((dimension) => {
viewportDimension = dimension
})
clock = mockClock()
})

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

const addVerticalScrollBar = () => {
document.body.style.setProperty('margin-bottom', '5000px')
}
const addHorizontalScrollBar = () => {
document.body.style.setProperty('margin-right', '5000px')
}

it('should track viewport resize', () => {
window.dispatchEvent(createNewEvent('resize'))
clock.tick(200)

expect(viewportDimension).toEqual({ width: jasmine.any(Number), height: jasmine.any(Number) })
})

describe('get layout width and height has similar native behaviour', () => {
// innerWidth includes the thickness of the sidebar while `visualViewport.width` and clientWidth exclude it
it('without scrollbars', () => {
expect(getViewportDimension()).toEqual({ width: window.innerWidth, height: window.innerHeight })
})

it('with scrollbars', () => {
addHorizontalScrollBar()
addVerticalScrollBar()
expect([
// Some devices don't follow specification of including scrollbars
{ width: window.innerWidth, height: window.innerHeight },
{ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight },
]).toContain(getViewportDimension())
})
})
})
48 changes: 48 additions & 0 deletions packages/core/src/browser/viewportObservable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { monitor } from '../tools/monitor'
import { Observable } from '../tools/observable'
import { throttle, addEventListener, DOM_EVENT } from '../tools/utils'

export interface ViewportDimension {
height: number
width: number
}

let viewportObservable: Observable<ViewportDimension> | undefined

export function initViewportObservable() {
if (!viewportObservable) {
viewportObservable = createViewportObservable()
}
return viewportObservable
}

export function createViewportObservable() {
const observable = new Observable<ViewportDimension>(() => {
const { throttled: updateDimension } = throttle(
monitor(() => {
observable.notify(getViewportDimension())
}),
200
)

return addEventListener(window, DOM_EVENT.RESIZE, updateDimension, { capture: true, passive: true }).stop
})

return observable
}

// excludes the width and height of any rendered classic scrollbar that is fixed to the visual viewport
export function getViewportDimension(): ViewportDimension {
const visual = window.visualViewport
if (visual) {
return {
width: Number(visual.width * visual.scale),
height: Number(visual.height * visual.scale),
}
}

return {
width: Number(window.innerWidth || 0),
height: Number(window.innerHeight || 0),
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export { Context, ContextArray, ContextValue } from './tools/context'
export { areCookiesAuthorized, getCookie, setCookie, deleteCookie, COOKIE_ACCESS_DELAY } from './browser/cookie'
export { initXhrObservable, XhrCompleteContext, XhrStartContext } from './browser/xhrObservable'
export { initFetchObservable, FetchCompleteContext, FetchStartContext, FetchContext } from './browser/fetchObservable'
export { initViewportObservable, getViewportDimension } from './browser/viewportObservable'
export { initConsoleObservable, ConsoleLog } from './domain/console/consoleObservable'
export { BoundedBuffer } from './tools/boundedBuffer'
export { catchUserErrors } from './tools/catchUserErrors'
Expand Down
3 changes: 2 additions & 1 deletion packages/rum-core/src/domain/contexts/displayContext.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { resetExperimentalFeatures, updateExperimentalFeatures } from '@datadog/browser-core'
import { getDisplayContext } from './displayContext'
import { getDisplayContext, resetDisplayContext } from './displayContext'

describe('displayContext', () => {
afterEach(() => {
resetExperimentalFeatures()
resetDisplayContext()
})

it('should return current display context when ff enabled', () => {
Expand Down
22 changes: 17 additions & 5 deletions packages/rum-core/src/domain/contexts/displayContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { isExperimentalFeatureEnabled } from '@datadog/browser-core'
import { isExperimentalFeatureEnabled, initViewportObservable, getViewportDimension } from '@datadog/browser-core'

let viewport: { width: number; height: number } | undefined
let stopListeners: (() => void) | undefined

export function getDisplayContext() {
if (!isExperimentalFeatureEnabled('clickmap')) return

if (!viewport) {
viewport = getViewportDimension()
stopListeners = initViewportObservable().subscribe((viewportDimension) => {
viewport = viewportDimension
}).unsubscribe
}

return {
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
viewport,
}
}

export function resetDisplayContext() {
if (stopListeners) stopListeners()
viewport = undefined
}
23 changes: 3 additions & 20 deletions packages/rum/src/domain/record/observer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DefaultPrivacyLevel } from '@datadog/browser-core'
import {
initViewportObservable,
instrumentSetter,
instrumentMethodAndCallOriginal,
assign,
Expand Down Expand Up @@ -30,14 +31,7 @@ import { forEach, isTouchEvent } from './utils'
import type { MutationController } from './mutationObserver'
import { startMutationObserver } from './mutationObserver'

import {
getVisualViewport,
getWindowHeight,
getWindowWidth,
getScrollX,
getScrollY,
convertMouseEventToLayoutCoordinates,
} from './viewports'
import { getVisualViewport, getScrollX, getScrollY, convertMouseEventToLayoutCoordinates } from './viewports'

const MOUSE_MOVE_OBSERVER_THRESHOLD = 50
const SCROLL_OBSERVER_THRESHOLD = 100
Expand Down Expand Up @@ -221,18 +215,7 @@ function initScrollObserver(cb: ScrollCallback, defaultPrivacyLevel: DefaultPriv
}

function initViewportResizeObserver(cb: ViewportResizeCallback): ListenerHandler {
const { throttled: updateDimension } = throttle(
monitor(() => {
const height = getWindowHeight()
const width = getWindowWidth()
cb({
height: Number(height),
width: Number(width),
})
}),
200
)
return addEventListener(window, DOM_EVENT.RESIZE, updateDimension, { capture: true, passive: true }).stop
return initViewportObservable().subscribe(cb).unsubscribe
}

export function initInputObserver(cb: InputCallback, defaultPrivacyLevel: DefaultPrivacyLevel): ListenerHandler {
Expand Down
10 changes: 5 additions & 5 deletions packages/rum/src/domain/record/record.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assign, timeStampNow } from '@datadog/browser-core'
import { assign, getViewportDimension, timeStampNow } from '@datadog/browser-core'
import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core'
import type {
IncrementalSnapshotRecord,
Expand All @@ -18,7 +18,7 @@ import { serializeDocument } from './serialize'
import { initObservers } from './observer'

import { MutationController } from './mutationObserver'
import { getVisualViewport, getScrollX, getScrollY, getWindowHeight, getWindowWidth } from './viewports'
import { getVisualViewport, getScrollX, getScrollY } from './viewports'

export interface RecordOptions {
emit?: (record: Record) => void
Expand All @@ -42,12 +42,12 @@ export function record(options: RecordOptions): RecordAPI {

const takeFullSnapshot = (timestamp = timeStampNow()) => {
mutationController.flush() // process any pending mutation before taking a full snapshot

const { width, height } = getViewportDimension()
emit({
data: {
height: getWindowHeight(),
height,
href: window.location.href,
width: getWindowWidth(),
width,
},
type: RecordType.Meta,
timestamp,
Expand Down
36 changes: 1 addition & 35 deletions packages/rum/src/domain/record/viewports.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getScrollX, getScrollY, getWindowHeight, getWindowWidth } from './viewports'
import { getScrollX, getScrollY } from './viewports'

function isMobileSafari12() {
return /iPhone OS 12.* like Mac OS.* Version\/12.* Mobile.*Safari/.test(navigator.userAgent)
Expand All @@ -8,47 +8,13 @@ describe('layout viewport', () => {
const addVerticalScrollBar = () => {
document.body.style.setProperty('margin-bottom', '5000px')
}
const addHorizontalScrollBar = () => {
document.body.style.setProperty('margin-right', '5000px')
}

afterEach(() => {
document.body.style.removeProperty('margin-bottom')
document.body.style.removeProperty('margin-right')
window.scrollTo(0, 0)
})

describe('get window width has similar native behaviour', () => {
// innerWidth includes the thickness of the sidebar while `visualViewport.width` and clientWidth exclude it
it('without scrollbars', () => {
expect(getWindowWidth()).toBe(window.innerWidth)
})

it('with scrollbars', () => {
addHorizontalScrollBar()
expect([
// Some devices don't follow specification of including scrollbars
window.innerWidth,
document.documentElement.clientWidth,
]).toContain(getWindowWidth())
})
})

describe('get window height has similar native behaviour', () => {
// innerHeight includes the thickness of the sidebar while `visualViewport.height` and clientHeight exclude it
it('without scrollbars', () => {
expect(getWindowHeight()).toBe(window.innerHeight)
})
it('with scrollbars', () => {
addVerticalScrollBar()
expect([
// Some devices don't follow specification of including scrollbars
window.innerHeight,
document.documentElement.clientHeight,
]).toContain(getWindowHeight())
})
})

describe('getScrollX/Y', () => {
it('normalized scroll matches initial behaviour', () => {
addVerticalScrollBar()
Expand Down
18 changes: 0 additions & 18 deletions packages/rum/src/domain/record/viewports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,6 @@ export const getVisualViewport = (): VisualViewportRecord['data'] => {
}
}

// excludes the width of any rendered classic scrollbar that is fixed to the visual viewport
export function getWindowWidth(): number {
const visual = window.visualViewport
if (visual) {
return visual.width * visual.scale
}
return window.innerWidth || 0
}

// excludes the height of any rendered classic scrollbar that is fixed to the visual viewport
export function getWindowHeight(): number {
const visual = window.visualViewport
if (visual) {
return visual.height * visual.scale
}
return window.innerHeight || 0
}

export function getScrollX() {
const visual = window.visualViewport
if (visual) {
Expand Down

0 comments on commit 40f03ee

Please sign in to comment.