diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index b8ace3be22..7fe07498a6 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -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' diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index c728241da1..10dff7494a 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -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' @@ -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' diff --git a/packages/rum-core/src/browser/viewportObservable.spec.ts b/packages/rum-core/src/browser/viewportObservable.spec.ts new file mode 100644 index 0000000000..02df2617f1 --- /dev/null +++ b/packages/rum-core/src/browser/viewportObservable.spec.ts @@ -0,0 +1,59 @@ +import type { Clock } from '@datadog/browser-core/test/specHelper' +import { createNewEvent, mockClock } from '@datadog/browser-core/test/specHelper' +import type { Subscription } from '@datadog/browser-core/src/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', () => { + afterEach(() => { + document.body.style.removeProperty('margin-bottom') + document.body.style.removeProperty('margin-right') + }) + + // 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/rum-core/src/browser/viewportObservable.ts b/packages/rum-core/src/browser/viewportObservable.ts new file mode 100644 index 0000000000..08e600e1e1 --- /dev/null +++ b/packages/rum-core/src/browser/viewportObservable.ts @@ -0,0 +1,46 @@ +import { monitor, Observable, throttle, addEventListener, DOM_EVENT } from '@datadog/browser-core' + +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/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index 765ab01569..b0874103af 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -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' @@ -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 diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index bf4c12a011..5e56920b88 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -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 @@ -133,6 +134,7 @@ export function startRumAssembly( action: needToAssembleWithAction(rawRumEvent) && actionId ? { id: actionId } : undefined, synthetics: syntheticsContext, ci_test: ciTestContext, + display: getDisplayContext(), } const serverRumEvent = combine(rumContext as RumContext & Context, rawRumEvent) as RumEvent & Context diff --git a/packages/rum-core/src/domain/ciTestContext.spec.ts b/packages/rum-core/src/domain/contexts/ciTestContext.spec.ts similarity index 95% rename from packages/rum-core/src/domain/ciTestContext.spec.ts rename to packages/rum-core/src/domain/contexts/ciTestContext.spec.ts index 2309b479e7..576b17cc9a 100644 --- a/packages/rum-core/src/domain/ciTestContext.spec.ts +++ b/packages/rum-core/src/domain/contexts/ciTestContext.spec.ts @@ -1,4 +1,4 @@ -import { mockCiVisibilityWindowValues, cleanupCiVisibilityWindowValues } from '../../test/specHelper' +import { mockCiVisibilityWindowValues, cleanupCiVisibilityWindowValues } from '../../../test/specHelper' import { getCiTestContext } from './ciTestContext' describe('getCiTestContext', () => { diff --git a/packages/rum-core/src/domain/ciTestContext.ts b/packages/rum-core/src/domain/contexts/ciTestContext.ts similarity index 100% rename from packages/rum-core/src/domain/ciTestContext.ts rename to packages/rum-core/src/domain/contexts/ciTestContext.ts diff --git a/packages/rum-core/src/domain/contexts/displayContext.spec.ts b/packages/rum-core/src/domain/contexts/displayContext.spec.ts new file mode 100644 index 0000000000..0333a776b5 --- /dev/null +++ b/packages/rum-core/src/domain/contexts/displayContext.spec.ts @@ -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() + }) +}) diff --git a/packages/rum-core/src/domain/contexts/displayContext.ts b/packages/rum-core/src/domain/contexts/displayContext.ts new file mode 100644 index 0000000000..557131db12 --- /dev/null +++ b/packages/rum-core/src/domain/contexts/displayContext.ts @@ -0,0 +1,25 @@ +import { isExperimentalFeatureEnabled } from '@datadog/browser-core' +import { getViewportDimension, initViewportObservable } from '../../browser/viewportObservable' + +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 +} diff --git a/packages/rum-core/src/domain/foregroundContexts.spec.ts b/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts similarity index 98% rename from packages/rum-core/src/domain/foregroundContexts.spec.ts rename to packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts index e543ec65c1..eab409c1d2 100644 --- a/packages/rum-core/src/domain/foregroundContexts.spec.ts +++ b/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts @@ -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, diff --git a/packages/rum-core/src/domain/foregroundContexts.ts b/packages/rum-core/src/domain/contexts/foregroundContexts.ts similarity index 98% rename from packages/rum-core/src/domain/foregroundContexts.ts rename to packages/rum-core/src/domain/contexts/foregroundContexts.ts index 2aa579647e..a626d10443 100644 --- a/packages/rum-core/src/domain/foregroundContexts.ts +++ b/packages/rum-core/src/domain/contexts/foregroundContexts.ts @@ -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 diff --git a/packages/rum-core/src/domain/internalContext.spec.ts b/packages/rum-core/src/domain/contexts/internalContext.spec.ts similarity index 88% rename from packages/rum-core/src/domain/internalContext.spec.ts rename to packages/rum-core/src/domain/contexts/internalContext.spec.ts index 72d45d680d..1ee00a1f42 100644 --- a/packages/rum-core/src/domain/internalContext.spec.ts +++ b/packages/rum-core/src/domain/contexts/internalContext.spec.ts @@ -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 diff --git a/packages/rum-core/src/domain/internalContext.ts b/packages/rum-core/src/domain/contexts/internalContext.ts similarity index 76% rename from packages/rum-core/src/domain/internalContext.ts rename to packages/rum-core/src/domain/contexts/internalContext.ts index bbdca29ecf..4d9764bfde 100644 --- a/packages/rum-core/src/domain/internalContext.ts +++ b/packages/rum-core/src/domain/contexts/internalContext.ts @@ -1,10 +1,23 @@ import type { RelativeTime } 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 diff --git a/packages/rum-core/src/domain/syntheticsContext.spec.ts b/packages/rum-core/src/domain/contexts/syntheticsContext.spec.ts similarity index 98% rename from packages/rum-core/src/domain/syntheticsContext.spec.ts rename to packages/rum-core/src/domain/contexts/syntheticsContext.spec.ts index 5314f629ae..8114e66a93 100644 --- a/packages/rum-core/src/domain/syntheticsContext.spec.ts +++ b/packages/rum-core/src/domain/contexts/syntheticsContext.spec.ts @@ -1,4 +1,4 @@ -import { mockSyntheticsWorkerValues, cleanupSyntheticsWorkerValues } from '../../test/specHelper' +import { mockSyntheticsWorkerValues, cleanupSyntheticsWorkerValues } from '../../../test/specHelper' import { getSyntheticsContext, willSyntheticsInjectRum } from './syntheticsContext' describe('getSyntheticsContext', () => { diff --git a/packages/rum-core/src/domain/syntheticsContext.ts b/packages/rum-core/src/domain/contexts/syntheticsContext.ts similarity index 100% rename from packages/rum-core/src/domain/syntheticsContext.ts rename to packages/rum-core/src/domain/contexts/syntheticsContext.ts diff --git a/packages/rum-core/src/domain/urlContexts.spec.ts b/packages/rum-core/src/domain/contexts/urlContexts.spec.ts similarity index 94% rename from packages/rum-core/src/domain/urlContexts.spec.ts rename to packages/rum-core/src/domain/contexts/urlContexts.spec.ts index 26fa87264c..df2e9dd6e4 100644 --- a/packages/rum-core/src/domain/urlContexts.spec.ts +++ b/packages/rum-core/src/domain/contexts/urlContexts.spec.ts @@ -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 diff --git a/packages/rum-core/src/domain/urlContexts.ts b/packages/rum-core/src/domain/contexts/urlContexts.ts similarity index 89% rename from packages/rum-core/src/domain/urlContexts.ts rename to packages/rum-core/src/domain/contexts/urlContexts.ts index 70d7910b24..f73da95601 100644 --- a/packages/rum-core/src/domain/urlContexts.ts +++ b/packages/rum-core/src/domain/contexts/urlContexts.ts @@ -1,9 +1,8 @@ import type { RelativeTime, Observable } 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: @@ -13,6 +12,11 @@ import { LifeCycleEventType } from './lifeCycle' export const URL_CONTEXT_TIME_OUT_DELAY = SESSION_TIME_OUT_DELAY +export interface UrlContext { + url: string + referrer: string +} + export interface UrlContexts { findUrl: (startTime?: RelativeTime) => UrlContext | undefined stop: () => void diff --git a/packages/rum-core/src/domain/viewContexts.spec.ts b/packages/rum-core/src/domain/contexts/viewContexts.spec.ts similarity index 96% rename from packages/rum-core/src/domain/viewContexts.spec.ts rename to packages/rum-core/src/domain/contexts/viewContexts.spec.ts index 43f29d4a80..e6d9684b58 100644 --- a/packages/rum-core/src/domain/viewContexts.spec.ts +++ b/packages/rum-core/src/domain/contexts/viewContexts.spec.ts @@ -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' diff --git a/packages/rum-core/src/domain/viewContexts.ts b/packages/rum-core/src/domain/contexts/viewContexts.ts similarity index 81% rename from packages/rum-core/src/domain/viewContexts.ts rename to packages/rum-core/src/domain/contexts/viewContexts.ts index 26b4081196..0714353550 100644 --- a/packages/rum-core/src/domain/viewContexts.ts +++ b/packages/rum-core/src/domain/contexts/viewContexts.ts @@ -1,12 +1,18 @@ import type { RelativeTime } from '@datadog/browser-core' import { SESSION_TIME_OUT_DELAY, ContextHistory } from '@datadog/browser-core' -import type { ViewContext } from '../rawRumEvent.types' -import type { LifeCycle } from './lifeCycle' -import { LifeCycleEventType } from './lifeCycle' -import type { ViewCreatedEvent } from './rumEventsCollection/view/trackViews' +import type { LifeCycle } from '../lifeCycle' +import { LifeCycleEventType } from '../lifeCycle' +import type { ViewCreatedEvent } from '../rumEventsCollection/view/trackViews' export const VIEW_CONTEXT_TIME_OUT_DELAY = SESSION_TIME_OUT_DELAY +export interface ViewContext { + service?: string + version?: string + id: string + name?: string +} + export interface ViewContexts { findView: (startTime?: RelativeTime) => ViewContext | undefined stop: () => void diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts index 84881ae768..dc80fb21e5 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts @@ -5,7 +5,7 @@ import type { CommonContext, RawRumActionEvent } from '../../../rawRumEvent.type import { ActionType, RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import type { ForegroundContexts } from '../../foregroundContexts' +import type { ForegroundContexts } from '../../contexts/foregroundContexts' import type { RumConfiguration } from '../../configuration' import type { ActionContexts, ClickAction } from './trackClickActions' import { trackClickActions } from './trackClickActions' diff --git a/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts index c733d5b50e..d46b5b8f7c 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts @@ -13,7 +13,7 @@ import type { CommonContext, RawRumErrorEvent } from '../../../rawRumEvent.types import { RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import type { ForegroundContexts } from '../../foregroundContexts' +import type { ForegroundContexts } from '../../contexts/foregroundContexts' import { trackConsoleError } from './trackConsoleError' import { trackReportError } from './trackReportError' diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index cf8cbb54fe..f42dc64e39 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -5,7 +5,7 @@ import type { RawRumViewEvent } from '../../../rawRumEvent.types' import { RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import type { ForegroundContexts } from '../../foregroundContexts' +import type { ForegroundContexts } from '../../contexts/foregroundContexts' import type { LocationChange } from '../../../browser/locationChangeObservable' import type { RumConfiguration } from '../../configuration' import type { ViewEvent, ViewOptions } from './trackViews' diff --git a/packages/rum-core/src/index.ts b/packages/rum-core/src/index.ts index 64eed8067d..bae4de32b8 100644 --- a/packages/rum-core/src/index.ts +++ b/packages/rum-core/src/index.ts @@ -18,11 +18,12 @@ export { RumViewEventDomainContext, RumEventDomainContext, } from './domainContext.types' -export { ViewContext, CommonContext, ReplayStats } from './rawRumEvent.types' +export { CommonContext, ReplayStats } from './rawRumEvent.types' export { startRum } from './boot/startRum' export { LifeCycle, LifeCycleEventType } from './domain/lifeCycle' export { ViewCreatedEvent } from './domain/rumEventsCollection/view/trackViews' -export { ViewContexts } from './domain/viewContexts' +export { ViewContexts, ViewContext } from './domain/contexts/viewContexts' export { RumSessionManager, RumSessionPlan } from './domain/rumSessionManager' export { getMutationObserverConstructor } from './browser/domMutationObservable' +export { initViewportObservable, getViewportDimension } from './browser/viewportObservable' export { RumInitConfiguration, RumConfiguration } from './domain/configuration' diff --git a/packages/rum-core/src/rawRumEvent.types.ts b/packages/rum-core/src/rawRumEvent.types.ts index 57c668295b..fcb4bb85c7 100644 --- a/packages/rum-core/src/rawRumEvent.types.ts +++ b/packages/rum-core/src/rawRumEvent.types.ts @@ -194,6 +194,12 @@ export interface RumContext { type: string has_replay?: boolean } + display?: { + viewport: { + width: number + height: number + } + } view: { id: string referrer?: string @@ -207,6 +213,9 @@ export interface RumContext { test_id: string result_id: string } + ci_test?: { + test_execution_id: string + } _dd: { format_version: 2 drift: number @@ -215,41 +224,6 @@ export interface RumContext { } browser_sdk_version?: string } - ci_test?: { - test_execution_id: string - } -} - -export interface ViewContext { - service?: string - version?: string - id: string - name?: string -} - -export interface ActionContext { - action: { - id: string | string[] - } -} - -export interface UrlContext { - url: string - referrer: string -} - -export interface InternalContext { - application_id: string - session_id: string | undefined - view?: { - id: string - url: string - referrer: string - name?: string - } - user_action?: { - id: string | string[] - } } export interface User { diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 4d6347ca1d..c9fa3768f3 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -68,11 +68,11 @@ export type RumActionEvent = CommonProperties & { */ readonly position?: { /** - * X coordinate of the action (in pixels) + * X coordinate relative to the target element of the action (in pixels) */ readonly x: number /** - * Y coordinate of the action (in pixels) + * Y coordinate relative to the target element of the action (in pixels) */ readonly y: number [k: string]: unknown @@ -790,6 +790,26 @@ export interface CommonProperties { } [k: string]: unknown } + /** + * Display properties + */ + display?: { + /** + * The viewport represents the rectangular area that is currently being viewed. Content outside the viewport is not visible onscreen until scrolled into view. + */ + readonly viewport?: { + /** + * Width of the viewport (in pixels) + */ + readonly width: number + /** + * Height of the viewport (in pixels) + */ + readonly height: number + [k: string]: unknown + } + [k: string]: unknown + } /** * Synthetics properties */ @@ -818,6 +838,46 @@ export interface CommonProperties { readonly test_execution_id: string [k: string]: unknown } + /** + * Operating system properties + */ + os?: { + /** + * Operating system name, e.g. Android, iOS + */ + readonly name: string + /** + * Full operating system version, e.g. 8.1.1 + */ + readonly version: string + /** + * Major operating system version, e.g. 8 + */ + readonly version_major: string + [k: string]: unknown + } + /** + * Device properties + */ + device?: { + /** + * Device type info + */ + readonly type: 'mobile' | 'desktop' | 'tablet' | 'tv' | 'gaming_console' | 'bot' | 'other' + /** + * Device marketing name, e.g. Xiaomi Redmi Note 8 Pro, Pixel 5, etc. + */ + readonly name?: string + /** + * Device SKU model, e.g. Samsung SM-988GN, etc. Quite often name and model can be the same. + */ + readonly model?: string + /** + * Device marketing brand, e.g. Apple, OPPO, Xiaomi, etc. + */ + readonly brand?: string + [k: string]: unknown + } /** * Internal properties */ diff --git a/packages/rum-core/test/specHelper.ts b/packages/rum-core/test/specHelper.ts index 9b03736062..f7cd934d8c 100644 --- a/packages/rum-core/test/specHelper.ts +++ b/packages/rum-core/test/specHelper.ts @@ -3,24 +3,24 @@ import { assign, combine, Observable, noop, setCookie, deleteCookie, ONE_MINUTE import type { Clock } from '../../core/test/specHelper' import { SPEC_ENDPOINTS, mockClock, buildLocation } from '../../core/test/specHelper' import type { RecorderApi } from '../src/boot/rumPublicApi' -import type { ForegroundContexts } from '../src/domain/foregroundContexts' +import type { ForegroundContexts } from '../src/domain/contexts/foregroundContexts' import type { RawRumEventCollectedData } from '../src/domain/lifeCycle' import { LifeCycle, LifeCycleEventType } from '../src/domain/lifeCycle' -import type { ViewContexts } from '../src/domain/viewContexts' +import type { ViewContexts } from '../src/domain/contexts/viewContexts' import type { ViewEvent, ViewOptions } from '../src/domain/rumEventsCollection/view/trackViews' import { trackViews } from '../src/domain/rumEventsCollection/view/trackViews' import type { RumSessionManager } from '../src/domain/rumSessionManager' import { RumSessionPlan } from '../src/domain/rumSessionManager' import type { RawRumEvent, RumContext } from '../src/rawRumEvent.types' import type { LocationChange } from '../src/browser/locationChangeObservable' -import type { UrlContexts } from '../src/domain/urlContexts' -import type { BrowserWindow } from '../src/domain/syntheticsContext' +import type { UrlContexts } from '../src/domain/contexts/urlContexts' +import type { BrowserWindow } from '../src/domain/contexts/syntheticsContext' import { SYNTHETICS_INJECTS_RUM_COOKIE_NAME, SYNTHETICS_RESULT_ID_COOKIE_NAME, SYNTHETICS_TEST_ID_COOKIE_NAME, -} from '../src/domain/syntheticsContext' -import type { CiTestWindow } from '../src/domain/ciTestContext' +} from '../src/domain/contexts/syntheticsContext' +import type { CiTestWindow } from '../src/domain/contexts/ciTestContext' import type { RumConfiguration } from '../src/domain/configuration' import { validateAndBuildRumConfiguration } from '../src/domain/configuration' import type { ActionContexts } from '../src/domain/rumEventsCollection/action/actionCollection' diff --git a/packages/rum/src/domain/record/observer.ts b/packages/rum/src/domain/record/observer.ts index f0c1cdb678..6b0585476a 100644 --- a/packages/rum/src/domain/record/observer.ts +++ b/packages/rum/src/domain/record/observer.ts @@ -10,6 +10,7 @@ import { addEventListener, noop, } from '@datadog/browser-core' +import { initViewportObservable } from '@datadog/browser-rum-core' import { NodePrivacyLevel } from '../../constants' import type { InputState, @@ -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..c70e9c32cf 100644 --- a/packages/rum/src/domain/record/record.ts +++ b/packages/rum/src/domain/record/record.ts @@ -1,5 +1,6 @@ import { assign, timeStampNow } from '@datadog/browser-core' import type { DefaultPrivacyLevel, TimeStamp } from '@datadog/browser-core' +import { getViewportDimension } from '@datadog/browser-rum-core' import type { IncrementalSnapshotRecord, IncrementalData, @@ -18,7 +19,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 +43,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) {