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

✨ [RUMF-1288] Collect viewport size #1584

Merged
merged 8 commits into from
Jun 16, 2022
Merged
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'
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
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
4 changes: 2 additions & 2 deletions packages/rum-core/src/boot/rumPublicApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import {
areCookiesAuthorized,
} from '@datadog/browser-core'
import type { LifeCycle } from '../domain/lifeCycle'
import type { ViewContexts } from '../domain/viewContexts'
import type { ViewContexts } from '../domain/contexts/viewContexts'
import type { RumSessionManager } from '../domain/rumSessionManager'
import type { CommonContext, User, ReplayStats } from '../rawRumEvent.types'
import { ActionType } from '../rawRumEvent.types'
import { willSyntheticsInjectRum } from '../domain/syntheticsContext'
import { willSyntheticsInjectRum } from '../domain/contexts/syntheticsContext'
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
import { validateAndBuildRumConfiguration } from '../domain/configuration'
import type { ViewOptions } from '../domain/rumEventsCollection/view/trackViews'
Expand Down
8 changes: 4 additions & 4 deletions packages/rum-core/src/boot/startRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { startTelemetry, canUseEventBridge, getEventBridge, startFlushFailedSend
import { createDOMMutationObservable } from '../browser/domMutationObservable'
import { startPerformanceCollection } from '../browser/performanceCollection'
import { startRumAssembly } from '../domain/assembly'
import { startForegroundContexts } from '../domain/foregroundContexts'
import { startInternalContext } from '../domain/internalContext'
import { startForegroundContexts } from '../domain/contexts/foregroundContexts'
import { startInternalContext } from '../domain/contexts/internalContext'
import { LifeCycle } from '../domain/lifeCycle'
import { startViewContexts } from '../domain/viewContexts'
import { startViewContexts } from '../domain/contexts/viewContexts'
import { startRequestCollection } from '../domain/requestCollection'
import { startActionCollection } from '../domain/rumEventsCollection/action/actionCollection'
import { startErrorCollection } from '../domain/rumEventsCollection/error/errorCollection'
Expand All @@ -18,7 +18,7 @@ import { startRumSessionManager, startRumSessionManagerStub } from '../domain/ru
import type { CommonContext } from '../rawRumEvent.types'
import { startRumBatch } from '../transport/startRumBatch'
import { startRumEventBridge } from '../transport/startRumEventBridge'
import { startUrlContexts } from '../domain/urlContexts'
import { startUrlContexts } from '../domain/contexts/urlContexts'
import type { LocationChange } from '../browser/locationChangeObservable'
import { createLocationChangeObservable } from '../browser/locationChangeObservable'
import type { RumConfiguration } from '../domain/configuration'
Expand Down
3 changes: 2 additions & 1 deletion packages/rum-core/src/domain/assembly.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
setup,
} from '../../test/specHelper'
import type { RumEventDomainContext } from '../domainContext.types'
import type { CommonContext, RawRumActionEvent, RawRumErrorEvent, RawRumEvent, ViewContext } from '../rawRumEvent.types'
import type { CommonContext, RawRumActionEvent, RawRumErrorEvent, RawRumEvent } from '../rawRumEvent.types'
import { RumEventType } from '../rawRumEvent.types'
import type { RumActionEvent, RumErrorEvent, RumEvent } from '../rumEvent.types'
import { initEventBridgeStub, deleteEventBridgeStub } from '../../../core/test/specHelper'
Expand All @@ -26,6 +26,7 @@ import type { LifeCycle, RawRumEventCollectedData } from './lifeCycle'
import { LifeCycleEventType } from './lifeCycle'
import { RumSessionPlan } from './rumSessionManager'
import type { RumConfiguration } from './configuration'
import type { ViewContext } from './contexts/viewContexts'

describe('rum assembly', () => {
let setupBuilder: TestSetupBuilder
Expand Down
10 changes: 6 additions & 4 deletions packages/rum-core/src/domain/assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ import type {
} from '../rawRumEvent.types'
import { RumEventType } from '../rawRumEvent.types'
import type { RumEvent } from '../rumEvent.types'
import { getSyntheticsContext } from './syntheticsContext'
import { getCiTestContext } from './ciTestContext'
import { getSyntheticsContext } from './contexts/syntheticsContext'
import { getCiTestContext } from './contexts/ciTestContext'
import type { LifeCycle } from './lifeCycle'
import { LifeCycleEventType } from './lifeCycle'
import type { ViewContexts } from './viewContexts'
import type { ViewContexts } from './contexts/viewContexts'
import type { RumSessionManager } from './rumSessionManager'
import { RumSessionPlan } from './rumSessionManager'
import type { UrlContexts } from './urlContexts'
import type { UrlContexts } from './contexts/urlContexts'
import type { RumConfiguration } from './configuration'
import type { ActionContexts } from './rumEventsCollection/action/actionCollection'
import { getDisplayContext } from './contexts/displayContext'

// replaced at build time
declare const __BUILD_ENV__SDK_VERSION__: string
Expand Down Expand Up @@ -121,6 +122,7 @@ export function startRumAssembly(
},
synthetics: syntheticsContext,
ci_test: ciTestContext,
display: getDisplayContext(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the display context at the assembly stage is the more straightforward way to implement it.
However, for pre-init calls, it means we won't get the display info of the time the event happens, but at the init time. I think it's good enough though

}
const actionId = actionContexts.findActionId(startTime)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mockCiVisibilityWindowValues, cleanupCiVisibilityWindowValues } from '../../test/specHelper'
import { mockCiVisibilityWindowValues, cleanupCiVisibilityWindowValues } from '../../../test/specHelper'
import { getCiTestContext } from './ciTestContext'

describe('getCiTestContext', () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/rum-core/src/domain/contexts/displayContext.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { resetExperimentalFeatures, updateExperimentalFeatures } from '@datadog/browser-core'
import { getDisplayContext, resetDisplayContext } from './displayContext'

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

it('should return current display context when ff enabled', () => {
updateExperimentalFeatures(['clickmap'])

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

it('should not return current display context when ff disabled', () => {
expect(getDisplayContext()).not.toBeDefined()
})
})
24 changes: 24 additions & 0 deletions packages/rum-core/src/domain/contexts/displayContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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,
}
}

export function resetDisplayContext() {
if (stopListeners) stopListeners()
viewport = undefined
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { RelativeTime, Duration, ServerDuration } from '@datadog/browser-core'
import { relativeNow } from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../test/specHelper'
import { setup } from '../../test/specHelper'
import type { TestSetupBuilder } from '../../../test/specHelper'
import { setup } from '../../../test/specHelper'
import type { ForegroundContexts } from './foregroundContexts'
import {
startForegroundContexts,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RelativeTime, Duration } from '@datadog/browser-core'
import { addEventListener, DOM_EVENT, elapsed, relativeNow, toServerDuration } from '@datadog/browser-core'
import type { InForegroundPeriod } from '../rawRumEvent.types'
import type { InForegroundPeriod } from '../../rawRumEvent.types'

// Arbitrary value to cap number of element mostly for backend & to save bandwidth
export const MAX_NUMBER_OF_SELECTABLE_FOREGROUND_PERIODS = 500
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { RelativeTime } from '@datadog/browser-core'
import { createRumSessionManagerMock } from '../../test/mockRumSessionManager'
import type { TestSetupBuilder } from '../../test/specHelper'
import { setup } from '../../test/specHelper'
import { createRumSessionManagerMock } from '../../../test/mockRumSessionManager'
import type { TestSetupBuilder } from '../../../test/specHelper'
import { setup } from '../../../test/specHelper'
import type { ActionContexts } from '../rumEventsCollection/action/actionCollection'
import type { RumSessionManager } from '../rumSessionManager'
import { startInternalContext } from './internalContext'
import type { ViewContexts } from './viewContexts'
import type { UrlContexts } from './urlContexts'
import type { RumSessionManager } from './rumSessionManager'
import type { ActionContexts } from './rumEventsCollection/action/actionCollection'

describe('internal context', () => {
let setupBuilder: TestSetupBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import type { RelativeTime } from '@datadog/browser-core'
import { assign } from '@datadog/browser-core'
import type { InternalContext } from '../rawRumEvent.types'
import type { ActionContexts } from '../rumEventsCollection/action/actionCollection'
import type { RumSessionManager } from '../rumSessionManager'
import type { ViewContexts } from './viewContexts'
import type { ActionContexts } from './rumEventsCollection/action/actionCollection'
import type { RumSessionManager } from './rumSessionManager'
import type { UrlContexts } from './urlContexts'

export interface InternalContext {
application_id: string
session_id: string | undefined
view?: {
id: string
url: string
referrer: string
name?: string
}
user_action?: {
id: string | string[]
}
}

/**
* Internal context keep returning v1 format
* to not break compatibility with logs data format
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mockSyntheticsWorkerValues, cleanupSyntheticsWorkerValues } from '../../test/specHelper'
import { mockSyntheticsWorkerValues, cleanupSyntheticsWorkerValues } from '../../../test/specHelper'
import { getSyntheticsContext, willSyntheticsInjectRum } from './syntheticsContext'

describe('getSyntheticsContext', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { RelativeTime } from '@datadog/browser-core'
import { relativeToClocks } from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../test/specHelper'
import { setup } from '../../test/specHelper'
import type { TestSetupBuilder } from '../../../test/specHelper'
import { setup } from '../../../test/specHelper'
import { LifeCycleEventType } from '../lifeCycle'
import type { ViewCreatedEvent, ViewEndedEvent } from '../rumEventsCollection/view/trackViews'
import type { UrlContexts } from './urlContexts'
import { startUrlContexts } from './urlContexts'
import type { ViewCreatedEvent, ViewEndedEvent } from './rumEventsCollection/view/trackViews'
import { LifeCycleEventType } from './lifeCycle'

describe('urlContexts', () => {
let setupBuilder: TestSetupBuilder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { RelativeTime, Observable } from '@datadog/browser-core'
import type { RelativeTime, Observable, Context } from '@datadog/browser-core'
import { SESSION_TIME_OUT_DELAY, relativeNow, ContextHistory } from '@datadog/browser-core'
import type { UrlContext } from '../rawRumEvent.types'
import type { LocationChange } from '../browser/locationChangeObservable'
import type { LifeCycle } from './lifeCycle'
import { LifeCycleEventType } from './lifeCycle'
import type { LocationChange } from '../../browser/locationChangeObservable'
import type { LifeCycle } from '../lifeCycle'
import { LifeCycleEventType } from '../lifeCycle'

/**
* We want to attach to an event:
Expand All @@ -13,6 +12,13 @@ import { LifeCycleEventType } from './lifeCycle'

export const URL_CONTEXT_TIME_OUT_DELAY = SESSION_TIME_OUT_DELAY

export interface UrlContext extends Context {
view: {
url: string
referrer: string
}
}

export interface UrlContexts {
findUrl: (startTime?: RelativeTime) => UrlContext | undefined
stop: () => void
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { RelativeTime } from '@datadog/browser-core'
import { relativeToClocks, CLEAR_OLD_CONTEXTS_INTERVAL } from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../test/specHelper'
import { setup } from '../../test/specHelper'
import { LifeCycleEventType } from './lifeCycle'
import type { TestSetupBuilder } from '../../../test/specHelper'
import { setup } from '../../../test/specHelper'
import { LifeCycleEventType } from '../lifeCycle'
import type { ViewCreatedEvent } from '../rumEventsCollection/view/trackViews'
import type { ViewContexts } from './viewContexts'
import { startViewContexts, VIEW_CONTEXT_TIME_OUT_DELAY } from './viewContexts'
import type { ViewCreatedEvent } from './rumEventsCollection/view/trackViews'

describe('viewContexts', () => {
const FAKE_ID = 'fake'
Expand Down
Loading