diff --git a/packages/core/src/browser/viewportObservable.spec.ts b/packages/core/src/browser/viewportObservable.spec.ts new file mode 100644 index 0000000000..145c238e02 --- /dev/null +++ b/packages/core/src/browser/viewportObservable.spec.ts @@ -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()) + }) + }) +}) diff --git a/packages/core/src/browser/viewportObservable.ts b/packages/core/src/browser/viewportObservable.ts new file mode 100644 index 0000000000..ae1fca5c3f --- /dev/null +++ b/packages/core/src/browser/viewportObservable.ts @@ -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 | undefined + +export function initViewportObservable() { + if (!viewportObservable) { + viewportObservable = createViewportObservable() + } + return viewportObservable +} + +export function createViewportObservable() { + const observable = new Observable(() => { + 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), + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5fcbd7c379..b1e783c123 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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' diff --git a/packages/rum-core/src/domain/contexts/displayContext.spec.ts b/packages/rum-core/src/domain/contexts/displayContext.spec.ts index 233a446520..0333a776b5 100644 --- a/packages/rum-core/src/domain/contexts/displayContext.spec.ts +++ b/packages/rum-core/src/domain/contexts/displayContext.spec.ts @@ -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', () => { diff --git a/packages/rum-core/src/domain/contexts/displayContext.ts b/packages/rum-core/src/domain/contexts/displayContext.ts index 6c63a274da..abf0181ba8 100644 --- a/packages/rum-core/src/domain/contexts/displayContext.ts +++ b/packages/rum-core/src/domain/contexts/displayContext.ts @@ -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 +} diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index f0c1cdb678..bb3f7a8350 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -1,5 +1,6 @@ import type { DefaultPrivacyLevel } from '@datadog/browser-core' import { + initViewportObservable, instrumentSetter, instrumentMethodAndCallOriginal, assign, @@ -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 @@ -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 { diff --git a/packages/rum/src/domain/record/record.ts b/packages/rum/src/domain/record/record.ts index 9726d556a8..40736a995b 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -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, @@ -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 @@ -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, diff --git a/packages/rum/src/domain/record/viewports.spec.ts b/packages/rum/src/domain/record/viewports.spec.ts index b933eef10f..8439cc126d 100644 --- a/packages/rum/src/domain/record/viewports.spec.ts +++ b/packages/rum/src/domain/record/viewports.spec.ts @@ -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) @@ -8,9 +8,6 @@ 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') @@ -18,37 +15,6 @@ describe('layout viewport', () => { 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() diff --git a/packages/rum/src/domain/record/viewports.ts b/packages/rum/src/domain/record/viewports.ts index ef91b5f7e3..4feb00b736 100644 --- a/packages/rum/src/domain/record/viewports.ts +++ b/packages/rum/src/domain/record/viewports.ts @@ -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) { diff --git a/rum-events-format b/rum-events-format index 8b32d51779..d09b33de6b 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 8b32d51779b83d2c7c1c3e41a68c8ff7a452594b +Subproject commit d09b33de6b9e445968c4bfe1440304e6cd839faa