diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index e40b67b287..94cbb21148 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -51,7 +51,7 @@ function startRumStub( pageStateHistory: PageStateHistory, reportError: (error: RawError) => void ) { - const { stop: rumEventCollectionStop, foregroundContexts } = startRumEventCollection( + const { stop: rumEventCollectionStop } = startRumEventCollection( lifeCycle, configuration, location, @@ -71,7 +71,6 @@ function startRumStub( location, domMutationObservable, locationChangeObservable, - foregroundContexts, startFeatureFlagContexts(lifeCycle), pageStateHistory, noopRecorderApi diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 948a5d0562..8f5d0329d7 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -11,7 +11,6 @@ import { import { createDOMMutationObservable } from '../browser/domMutationObservable' import { startPerformanceCollection } from '../browser/performanceCollection' import { startRumAssembly } from '../domain/assembly' -import { startForegroundContexts } from '../domain/contexts/foregroundContexts' import { startInternalContext } from '../domain/contexts/internalContext' import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle' import { startViewContexts } from '../domain/contexts/viewContexts' @@ -102,17 +101,16 @@ export function startRum( const domMutationObservable = createDOMMutationObservable() const locationChangeObservable = createLocationChangeObservable(location) - const { viewContexts, foregroundContexts, pageStateHistory, urlContexts, actionContexts, addAction } = - startRumEventCollection( - lifeCycle, - configuration, - location, - session, - locationChangeObservable, - domMutationObservable, - () => buildCommonContext(globalContextManager, userContextManager, recorderApi), - reportError - ) + const { viewContexts, pageStateHistory, urlContexts, actionContexts, addAction } = startRumEventCollection( + lifeCycle, + configuration, + location, + session, + locationChangeObservable, + domMutationObservable, + () => buildCommonContext(globalContextManager, userContextManager, recorderApi), + reportError + ) addTelemetryConfiguration(serializeRumConfiguration(initConfiguration)) @@ -124,13 +122,12 @@ export function startRum( location, domMutationObservable, locationChangeObservable, - foregroundContexts, featureFlagContexts, pageStateHistory, recorderApi, initialViewOptions ) - const { addError } = startErrorCollection(lifeCycle, foregroundContexts, featureFlagContexts) + const { addError } = startErrorCollection(lifeCycle, pageStateHistory, featureFlagContexts) startRequestCollection(lifeCycle, configuration, session) startPerformanceCollection(lifeCycle, configuration) @@ -179,14 +176,13 @@ export function startRumEventCollection( const viewContexts = startViewContexts(lifeCycle) const urlContexts = startUrlContexts(lifeCycle, locationChangeObservable, location) - const foregroundContexts = startForegroundContexts() const pageStateHistory = startPageStateHistory() const { addAction, actionContexts } = startActionCollection( lifeCycle, domMutationObservable, configuration, - foregroundContexts + pageStateHistory ) startRumAssembly( @@ -202,14 +198,13 @@ export function startRumEventCollection( return { viewContexts, - foregroundContexts, pageStateHistory, urlContexts, addAction, actionContexts, stop: () => { viewContexts.stop() - foregroundContexts.stop() + pageStateHistory.stop() }, } } diff --git a/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts b/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts index 1ee0b4be03..140372ad84 100644 --- a/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts +++ b/packages/rum-core/src/domain/contexts/foregroundContexts.spec.ts @@ -2,28 +2,40 @@ import type { RelativeTime, Duration, ServerDuration } from '@datadog/browser-co import { relativeNow } from '@datadog/browser-core' import type { TestSetupBuilder } from '../../../test' import { setup } from '../../../test' -import type { ForegroundContexts } from './foregroundContexts' -import { - startForegroundContexts, - MAX_NUMBER_OF_SELECTABLE_FOREGROUND_PERIODS, - MAX_NUMBER_OF_STORED_FOREGROUND_PERIODS, - closeForegroundPeriod, - addNewForegroundPeriod, -} from './foregroundContexts' +import { mapToForegroundPeriods } from './foregroundContexts' +import type { PageStateHistory } from './pageStateHistory' +import { PageState, startPageStateHistory } from './pageStateHistory' const FOCUS_PERIOD_LENGTH = 10 as Duration const BLUR_PERIOD_LENGTH = 5 as Duration describe('foreground context', () => { - let foregroundContext: ForegroundContexts let setupBuilder: TestSetupBuilder + let pageStateHistory: PageStateHistory + + function addNewForegroundPeriod() { + pageStateHistory.addPageState(PageState.ACTIVE) + } + + function closeForegroundPeriod() { + pageStateHistory.addPageState(PageState.PASSIVE) + } + + function selectInForegroundPeriodsFor(startTime: RelativeTime, duration: Duration) { + const pageStates = pageStateHistory.findAll(startTime, duration) + return mapToForegroundPeriods(pageStates || [], duration) + } + + function isInForegroundAt(startTime: RelativeTime) { + return pageStateHistory.isInActivePageStateAt(startTime) + } beforeEach(() => { setupBuilder = setup() .withFakeClock() .beforeBuild(() => { - foregroundContext = startForegroundContexts() - return foregroundContext + pageStateHistory = startPageStateHistory() + return pageStateHistory }) }) @@ -34,6 +46,7 @@ describe('foreground context', () => { describe('when the page do not have the focus when starting', () => { beforeEach(() => { spyOn(Document.prototype, 'hasFocus').and.callFake(() => false) + pageStateHistory = startPageStateHistory() }) describe('without any focus nor blur event', () => { describe('isInForegroundAt', () => { @@ -42,7 +55,7 @@ describe('foreground context', () => { clock.tick(1_000) - expect(foregroundContext.isInForegroundAt(relativeNow())).toEqual(false) + expect(isInForegroundAt(relativeNow())).toEqual(false) }) }) @@ -52,7 +65,7 @@ describe('foreground context', () => { clock.tick(1_000) - expect(foregroundContext.selectInForegroundPeriodsFor(relativeNow(), 0 as Duration)).toEqual([]) + expect(selectInForegroundPeriodsFor(relativeNow(), 0 as Duration)).toEqual([]) }) }) }) @@ -80,66 +93,66 @@ describe('foreground context', () => { it('isInForegroundAt should match the focused/burred period', () => { // first blurred period - expect(foregroundContext.isInForegroundAt(2 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(2 as RelativeTime)).toEqual(false) // first focused period - expect(foregroundContext.isInForegroundAt(10 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(10 as RelativeTime)).toEqual(true) // second blurred period - expect(foregroundContext.isInForegroundAt(17 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(17 as RelativeTime)).toEqual(false) // second focused period - expect(foregroundContext.isInForegroundAt(25 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(25 as RelativeTime)).toEqual(true) // third blurred period - expect(foregroundContext.isInForegroundAt(32 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(32 as RelativeTime)).toEqual(false) // current focused periods - expect(foregroundContext.isInForegroundAt(42 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(42 as RelativeTime)).toEqual(true) }) describe('selectInForegroundPeriodsFor', () => { it('should have 3 in foreground periods for the whole period', () => { - const periods = foregroundContext.selectInForegroundPeriodsFor(0 as RelativeTime, 50 as Duration) + const periods = selectInForegroundPeriodsFor(0 as RelativeTime, 50 as Duration) expect(periods).toHaveSize(3) - expect(periods![0]).toEqual({ + expect(periods[0]).toEqual({ start: (5 * 1e6) as ServerDuration, duration: (10 * 1e6) as ServerDuration, }) - expect(periods![1]).toEqual({ + expect(periods[1]).toEqual({ start: (20 * 1e6) as ServerDuration, duration: (10 * 1e6) as ServerDuration, }) - expect(periods![2]).toEqual({ + expect(periods[2]).toEqual({ start: (35 * 1e6) as ServerDuration, duration: (15 * 1e6) as ServerDuration, }) }) it('should have 2 in foreground periods when in between the two full periods', () => { - const periods = foregroundContext.selectInForegroundPeriodsFor(10 as RelativeTime, 15 as Duration) + const periods = selectInForegroundPeriodsFor(10 as RelativeTime, 15 as Duration) expect(periods).toHaveSize(2) - expect(periods![0]).toEqual({ + expect(periods[0]).toEqual({ start: 0 as ServerDuration, duration: (5 * 1e6) as ServerDuration, }) - expect(periods![1]).toEqual({ + expect(periods[1]).toEqual({ start: (10 * 1e6) as ServerDuration, duration: (5 * 1e6) as ServerDuration, }) }) it('should have 2 periods, when in between the the full period and ongoing periods', () => { - const periods = foregroundContext.selectInForegroundPeriodsFor(25 as RelativeTime, 20 as Duration) + const periods = selectInForegroundPeriodsFor(25 as RelativeTime, 20 as Duration) expect(periods).toHaveSize(2) - expect(periods![0]).toEqual({ + expect(periods[0]).toEqual({ start: 0 as ServerDuration, duration: (5 * 1e6) as ServerDuration, }) - expect(periods![1]).toEqual({ + expect(periods[1]).toEqual({ start: (10 * 1e6) as ServerDuration, duration: (10 * 1e6) as ServerDuration, }) @@ -164,56 +177,33 @@ describe('foreground context', () => { clock.tick(BLUR_PERIOD_LENGTH) }) it('isInForegroundAt should match the focused/burred period', () => { - expect(foregroundContext.isInForegroundAt(2 as RelativeTime)).toEqual(false) - expect(foregroundContext.isInForegroundAt(10 as RelativeTime)).toEqual(true) - expect(foregroundContext.isInForegroundAt(20 as RelativeTime)).toEqual(true) - expect(foregroundContext.isInForegroundAt(30 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(2 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(10 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(20 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(30 as RelativeTime)).toEqual(false) }) }) - it('should not record anything after reaching the maximum number of focus periods', () => { - const { clock } = setupBuilder.build() - const start = relativeNow() - for (let i = 0; i < MAX_NUMBER_OF_STORED_FOREGROUND_PERIODS + 1; i++) { - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() - clock.tick(BLUR_PERIOD_LENGTH) - } - - addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - - expect(foregroundContext.isInForegroundAt(relativeNow())).toEqual(false) - const duration = (((FOCUS_PERIOD_LENGTH as number) + (BLUR_PERIOD_LENGTH as number)) * - MAX_NUMBER_OF_STORED_FOREGROUND_PERIODS) as Duration - expect(foregroundContext.selectInForegroundPeriodsFor(start, duration)).toHaveSize( - MAX_NUMBER_OF_SELECTABLE_FOREGROUND_PERIODS - ) - }) - it('should not be in foreground, when the periods is closed twice', () => { const { clock } = setupBuilder.build() addNewForegroundPeriod() - clock.tick(FOCUS_PERIOD_LENGTH) - closeForegroundPeriod() clock.tick(BLUR_PERIOD_LENGTH) - closeForegroundPeriod() + pageStateHistory.addPageState(PageState.PASSIVE) - expect(foregroundContext.isInForegroundAt(relativeNow())).toEqual(false) + expect(isInForegroundAt(relativeNow())).toEqual(false) }) it('after starting with a blur even, should not be in foreground', () => { - setupBuilder.build() - closeForegroundPeriod() + pageStateHistory.addPageState(PageState.PASSIVE) - expect(foregroundContext.isInForegroundAt(relativeNow())).toEqual(false) + expect(isInForegroundAt(relativeNow())).toEqual(false) }) }) describe('when the page has focus when starting', () => { beforeEach(() => { spyOn(Document.prototype, 'hasFocus').and.callFake(() => true) + pageStateHistory = startPageStateHistory() }) describe('when there is no focus event', () => { @@ -222,7 +212,7 @@ describe('foreground context', () => { clock.tick(FOCUS_PERIOD_LENGTH) closeForegroundPeriod() - expect(foregroundContext.isInForegroundAt(2 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(2 as RelativeTime)).toEqual(true) }) it('should return false after the first focused period', () => { @@ -230,7 +220,7 @@ describe('foreground context', () => { clock.tick(FOCUS_PERIOD_LENGTH) closeForegroundPeriod() - expect(foregroundContext.isInForegroundAt(12 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(12 as RelativeTime)).toEqual(false) }) }) @@ -242,7 +232,7 @@ describe('foreground context', () => { clock.tick(FOCUS_PERIOD_LENGTH / 2) closeForegroundPeriod() - expect(foregroundContext.isInForegroundAt(2 as RelativeTime)).toEqual(true) + expect(isInForegroundAt(2 as RelativeTime)).toEqual(true) }) it('should return false after the focused period', () => { @@ -252,7 +242,7 @@ describe('foreground context', () => { clock.tick(FOCUS_PERIOD_LENGTH / 2) closeForegroundPeriod() - expect(foregroundContext.isInForegroundAt(12 as RelativeTime)).toEqual(false) + expect(isInForegroundAt(12 as RelativeTime)).toEqual(false) }) }) }) diff --git a/packages/rum-core/src/domain/contexts/foregroundContexts.ts b/packages/rum-core/src/domain/contexts/foregroundContexts.ts index 15b7290a05..ce21728ea5 100644 --- a/packages/rum-core/src/domain/contexts/foregroundContexts.ts +++ b/packages/rum-core/src/domain/contexts/foregroundContexts.ts @@ -1,129 +1,32 @@ -import type { RelativeTime, Duration } from '@datadog/browser-core' -import { addDuration, addEventListener, DOM_EVENT, elapsed, relativeNow, toServerDuration } from '@datadog/browser-core' -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 -// Arbitrary value to cap number of element mostly for memory consumption in the browser -export const MAX_NUMBER_OF_STORED_FOREGROUND_PERIODS = 2500 - -export interface ForegroundContexts { - isInForegroundAt: (startTime: RelativeTime) => boolean | undefined - selectInForegroundPeriodsFor: (startTime: RelativeTime, duration: Duration) => InForegroundPeriod[] | undefined - stop: () => void -} +import type { RelativeTime, Duration, ServerDuration } from '@datadog/browser-core' +import { toServerDuration } from '@datadog/browser-core' +import type { InForegroundPeriod, PageStateServerEntry } from '../../rawRumEvent.types' +import { PageState } from './pageStateHistory' export interface ForegroundPeriod { start: RelativeTime end?: RelativeTime } -let foregroundPeriods: ForegroundPeriod[] = [] - -export function startForegroundContexts(): ForegroundContexts { - if (document.hasFocus()) { - addNewForegroundPeriod() - } - - const { stop: stopForegroundTracking } = trackFocus(addNewForegroundPeriod) - const { stop: stopBlurTracking } = trackBlur(closeForegroundPeriod) - return { - isInForegroundAt, - selectInForegroundPeriodsFor, - stop: () => { - foregroundPeriods = [] - stopForegroundTracking() - stopBlurTracking() - }, - } -} - -export function addNewForegroundPeriod() { - if (foregroundPeriods.length > MAX_NUMBER_OF_STORED_FOREGROUND_PERIODS) { - return - } - const currentForegroundPeriod = foregroundPeriods[foregroundPeriods.length - 1] - const now = relativeNow() - if (currentForegroundPeriod !== undefined && currentForegroundPeriod.end === undefined) { - return - } - foregroundPeriods.push({ - start: now, - }) -} - -export function closeForegroundPeriod() { - if (foregroundPeriods.length === 0) { - return - } - const currentForegroundPeriod = foregroundPeriods[foregroundPeriods.length - 1] - const now = relativeNow() - if (currentForegroundPeriod.end !== undefined) { - return - } - currentForegroundPeriod.end = now -} - -function trackFocus(onFocusChange: () => void) { - return addEventListener(window, DOM_EVENT.FOCUS, (event) => { - if (!event.isTrusted) { - return - } - onFocusChange() - }) -} - -function trackBlur(onBlurChange: () => void) { - return addEventListener(window, DOM_EVENT.BLUR, (event) => { - if (!event.isTrusted) { - return - } - onBlurChange() - }) -} - -function isInForegroundAt(startTime: RelativeTime): boolean { - for (let i = foregroundPeriods.length - 1; i >= 0; i--) { - const foregroundPeriod = foregroundPeriods[i] - if (foregroundPeriod.end !== undefined && startTime > foregroundPeriod.end) { - break - } - if ( - startTime > foregroundPeriod.start && - (foregroundPeriod.end === undefined || startTime < foregroundPeriod.end) - ) { - return true +// Todo: Remove in the next major release +export function mapToForegroundPeriods( + pageStateServerEntries: PageStateServerEntry[], + duration: Duration +): InForegroundPeriod[] { + const foregroundPeriods: InForegroundPeriod[] = [] + for (let i = 0; i < pageStateServerEntries.length; i++) { + const current = pageStateServerEntries[i] + const next = pageStateServerEntries[i + 1] + + if (current.state === PageState.ACTIVE) { + const start = current.start >= 0 ? current.start : (0 as ServerDuration) + const end = next ? next.start : toServerDuration(duration) + foregroundPeriods.push({ + start, + duration: (end - start) as ServerDuration, + }) } } - return false -} - -function selectInForegroundPeriodsFor(eventStartTime: RelativeTime, duration: Duration): InForegroundPeriod[] { - const eventEndTime = addDuration(eventStartTime, duration) - const filteredForegroundPeriods: InForegroundPeriod[] = [] - const earliestIndex = Math.max(0, foregroundPeriods.length - MAX_NUMBER_OF_SELECTABLE_FOREGROUND_PERIODS) - for (let i = foregroundPeriods.length - 1; i >= earliestIndex; i--) { - const foregroundPeriod = foregroundPeriods[i] - if (foregroundPeriod.end !== undefined && eventStartTime > foregroundPeriod.end) { - // event starts after the end of the current focus period - // since the array is sorted, we can stop looking for foreground periods - break - } - if (eventEndTime < foregroundPeriod.start) { - // event ends before the start of the current focus period - // continue to previous one - continue - } - const startTime = eventStartTime > foregroundPeriod.start ? eventStartTime : foregroundPeriod.start - const startDuration = elapsed(eventStartTime, startTime) - const endTime = - foregroundPeriod.end === undefined || eventEndTime < foregroundPeriod.end ? eventEndTime : foregroundPeriod.end - const endDuration = elapsed(startTime, endTime) - filteredForegroundPeriods.unshift({ - start: toServerDuration(startDuration), - duration: toServerDuration(endDuration), - }) - } - return filteredForegroundPeriods + return foregroundPeriods } diff --git a/packages/rum-core/src/domain/contexts/pageStateHistory.spec.ts b/packages/rum-core/src/domain/contexts/pageStateHistory.spec.ts index 5f1547ea0f..45628361e9 100644 --- a/packages/rum-core/src/domain/contexts/pageStateHistory.spec.ts +++ b/packages/rum-core/src/domain/contexts/pageStateHistory.spec.ts @@ -17,63 +17,86 @@ describe('pageStateHistory', () => { clock.cleanup() }) - it('should have the current state when starting', () => { - expect(pageStateHistory.findAll(0 as RelativeTime, 10 as RelativeTime)).toBeDefined() - }) + describe('findAll', () => { + it('should have the current state when starting', () => { + expect(pageStateHistory.findAll(0 as RelativeTime, 10 as RelativeTime)).toBeDefined() + }) - it('should return undefined if the time period is out of history bounds', () => { - expect(pageStateHistory.findAll(-10 as RelativeTime, 0 as RelativeTime)).not.toBeDefined() - }) + it('should return undefined if the time period is out of history bounds', () => { + expect(pageStateHistory.findAll(-10 as RelativeTime, 0 as RelativeTime)).not.toBeDefined() + }) + + it('should return the correct page states for the given time period', () => { + pageStateHistory.addPageState(PageState.ACTIVE) + + clock.tick(10) + pageStateHistory.addPageState(PageState.PASSIVE) + + clock.tick(10) + pageStateHistory.addPageState(PageState.HIDDEN) + + clock.tick(10) + pageStateHistory.addPageState(PageState.FROZEN) - it('should return the correct page states for the given time period', () => { - pageStateHistory.addPageState(PageState.ACTIVE) - - clock.tick(10) - pageStateHistory.addPageState(PageState.PASSIVE) - - clock.tick(10) - pageStateHistory.addPageState(PageState.HIDDEN) - - clock.tick(10) - pageStateHistory.addPageState(PageState.FROZEN) - - clock.tick(10) - pageStateHistory.addPageState(PageState.TERMINATED) - - /* - page state time 0 10 20 30 40 - event time 15<-------->35 - */ - const event = { - startTime: 15 as RelativeTime, - duration: 20 as RelativeTime, - } - expect(pageStateHistory.findAll(event.startTime, event.duration)).toEqual([ - { - state: PageState.PASSIVE, - start: -5000000 as ServerDuration, - }, - { - state: PageState.HIDDEN, - start: 5000000 as ServerDuration, - }, - { - state: PageState.FROZEN, - start: 15000000 as ServerDuration, - }, - ]) + clock.tick(10) + pageStateHistory.addPageState(PageState.TERMINATED) + + /* + page state time 0 10 20 30 40 + event time 15<-------->35 + */ + const event = { + startTime: 15 as RelativeTime, + duration: 20 as RelativeTime, + } + expect(pageStateHistory.findAll(event.startTime, event.duration)).toEqual([ + { + state: PageState.PASSIVE, + start: -5000000 as ServerDuration, + }, + { + state: PageState.HIDDEN, + start: 5000000 as ServerDuration, + }, + { + state: PageState.FROZEN, + start: 15000000 as ServerDuration, + }, + ]) + }) + + it('should limit the number of selectable entries', () => { + const maxPageStateEntriesSelectable = 1 + pageStateHistory = startPageStateHistory(maxPageStateEntriesSelectable) + + pageStateHistory.addPageState(PageState.ACTIVE) + clock.tick(10) + pageStateHistory.addPageState(PageState.PASSIVE) + + expect(pageStateHistory.findAll(0 as RelativeTime, Infinity as RelativeTime)?.length).toEqual( + maxPageStateEntriesSelectable + ) + }) }) - it('should limit the number of selectable entries', () => { - const maxPageStateEntriesSelectable = 1 - pageStateHistory = startPageStateHistory(maxPageStateEntriesSelectable) + describe('isInActivePageStateAt', () => { + it('should return true if the page was active at the given time', () => { + pageStateHistory.addPageState(PageState.ACTIVE) + + clock.tick(10) + pageStateHistory.addPageState(PageState.PASSIVE) + + expect(pageStateHistory.isInActivePageStateAt(0 as RelativeTime)).toEqual(true) + }) - pageStateHistory.addPageState(PageState.ACTIVE) - clock.tick(10) - pageStateHistory.addPageState(PageState.PASSIVE) + it('should return false if the page was not active at the given time', () => { + const maxPageStateEntriesSelectable = 1 + pageStateHistory = startPageStateHistory(maxPageStateEntriesSelectable) - expect(pageStateHistory.findAll(0 as RelativeTime, Infinity as RelativeTime)?.length).toEqual( - maxPageStateEntriesSelectable - ) + pageStateHistory.addPageState(PageState.ACTIVE) + clock.tick(10) + pageStateHistory.addPageState(PageState.PASSIVE) + expect(pageStateHistory.isInActivePageStateAt(10 as RelativeTime)).toEqual(false) + }) }) }) diff --git a/packages/rum-core/src/domain/contexts/pageStateHistory.ts b/packages/rum-core/src/domain/contexts/pageStateHistory.ts index f829be5c07..61e2fc517d 100644 --- a/packages/rum-core/src/domain/contexts/pageStateHistory.ts +++ b/packages/rum-core/src/domain/contexts/pageStateHistory.ts @@ -29,6 +29,7 @@ export type PageStateEntry = { state: PageState; startTime: RelativeTime } export interface PageStateHistory { findAll: (startTime: RelativeTime, duration: Duration) => PageStateServerEntry[] | undefined + isInActivePageStateAt: (startTime: RelativeTime) => boolean addPageState(nextPageState: PageState, startTime?: RelativeTime): void stop: () => void } @@ -98,6 +99,10 @@ export function startPageStateHistory( return pageStateServerEntries }, + isInActivePageStateAt: (startTime: RelativeTime) => { + const pageStateEntry = pageStateHistory.find(startTime) + return pageStateEntry !== undefined && pageStateEntry.state === PageState.ACTIVE + }, addPageState, stop: () => { stopEventListeners() diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts index a6b2ca4c77..9471c11ed8 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.spec.ts @@ -12,11 +12,11 @@ describe('actionCollection', () => { beforeEach(() => { setupBuilder = setup() - .withForegroundContexts({ - isInForegroundAt: () => true, + .withPageStateHistory({ + isInActivePageStateAt: () => true, }) - .beforeBuild(({ lifeCycle, configuration, domMutationObservable, foregroundContexts }) => { - ;({ addAction } = startActionCollection(lifeCycle, domMutationObservable, configuration, foregroundContexts)) + .beforeBuild(({ lifeCycle, configuration, domMutationObservable, pageStateHistory }) => { + ;({ addAction } = startActionCollection(lifeCycle, domMutationObservable, configuration, pageStateHistory)) }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts index ac0208f383..06ddc3d3b1 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/action/actionCollection.ts @@ -5,9 +5,9 @@ import type { RawRumActionEvent } from '../../../rawRumEvent.types' import { ActionType, RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import type { ForegroundContexts } from '../../contexts/foregroundContexts' import type { RumConfiguration } from '../../configuration' import type { CommonContext } from '../../contexts/commonContext' +import type { PageStateHistory } from '../../contexts/pageStateHistory' import type { ActionContexts, ClickAction } from './trackClickActions' import { trackClickActions } from './trackClickActions' @@ -26,10 +26,10 @@ export function startActionCollection( lifeCycle: LifeCycle, domMutationObservable: Observable, configuration: RumConfiguration, - foregroundContexts: ForegroundContexts + pageStateHistory: PageStateHistory ) { lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, (action) => - lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processAction(action, foregroundContexts)) + lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processAction(action, pageStateHistory)) ) let actionContexts: ActionContexts = { findActionId: noop as () => undefined } @@ -45,7 +45,7 @@ export function startActionCollection( { savedCommonContext, }, - processAction(action, foregroundContexts) + processAction(action, pageStateHistory) ) ) }, @@ -55,7 +55,7 @@ export function startActionCollection( function processAction( action: AutoAction | CustomAction, - foregroundContexts: ForegroundContexts + pageStateHistory: PageStateHistory ): RawRumEventCollectedData { const autoActionProperties = isAutoAction(action) ? { @@ -95,13 +95,11 @@ function processAction( }, date: action.startClocks.timeStamp, type: RumEventType.ACTION as const, + view: { in_foreground: pageStateHistory.isInActivePageStateAt(action.startClocks.relative) }, }, autoActionProperties ) - const inForeground = foregroundContexts.isInForegroundAt(action.startClocks.relative) - if (inForeground !== undefined) { - actionEvent.view = { in_foreground: inForeground } - } + return { customerContext, rawRumEvent: actionEvent, diff --git a/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.spec.ts index e29d1acd2d..4bdd437b95 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.spec.ts @@ -20,11 +20,11 @@ describe('error collection', () => { beforeEach(() => { setupBuilder = setup() .withViewContexts(viewContextsStub) - .withForegroundContexts({ - isInForegroundAt: () => true, + .withPageStateHistory({ + isInActivePageStateAt: () => true, }) - .beforeBuild(({ lifeCycle, foregroundContexts, featureFlagContexts }) => { - ;({ addError } = doStartErrorCollection(lifeCycle, foregroundContexts, featureFlagContexts)) + .beforeBuild(({ lifeCycle, pageStateHistory, featureFlagContexts }) => { + ;({ addError } = doStartErrorCollection(lifeCycle, pageStateHistory, featureFlagContexts)) }) }) diff --git a/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts index b2687aceec..4fa34d8dc9 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/error/errorCollection.ts @@ -15,9 +15,9 @@ import type { RawRumErrorEvent } from '../../../rawRumEvent.types' import { RumEventType } from '../../../rawRumEvent.types' import type { LifeCycle, RawRumEventCollectedData } from '../../lifeCycle' import { LifeCycleEventType } from '../../lifeCycle' -import type { ForegroundContexts } from '../../contexts/foregroundContexts' import type { FeatureFlagContexts } from '../../contexts/featureFlagContext' import type { CommonContext } from '../../contexts/commonContext' +import type { PageStateHistory } from '../../contexts/pageStateHistory' import { trackConsoleError } from './trackConsoleError' import { trackReportError } from './trackReportError' @@ -30,7 +30,7 @@ export interface ProvidedError { export function startErrorCollection( lifeCycle: LifeCycle, - foregroundContexts: ForegroundContexts, + pageStateHistory: PageStateHistory, featureFlagContexts: FeatureFlagContexts ) { const errorObservable = new Observable() @@ -41,12 +41,12 @@ export function startErrorCollection( errorObservable.subscribe((error) => lifeCycle.notify(LifeCycleEventType.RAW_ERROR_COLLECTED, { error })) - return doStartErrorCollection(lifeCycle, foregroundContexts, featureFlagContexts) + return doStartErrorCollection(lifeCycle, pageStateHistory, featureFlagContexts) } export function doStartErrorCollection( lifeCycle: LifeCycle, - foregroundContexts: ForegroundContexts, + pageStateHistory: PageStateHistory, featureFlagContexts: FeatureFlagContexts ) { lifeCycle.subscribe(LifeCycleEventType.RAW_ERROR_COLLECTED, ({ error, customerContext, savedCommonContext }) => { @@ -57,7 +57,7 @@ export function doStartErrorCollection( customerContext, savedCommonContext, }, - processError(error, foregroundContexts, featureFlagContexts) + processError(error, pageStateHistory, featureFlagContexts) ) ) }) @@ -89,7 +89,7 @@ export function doStartErrorCollection( function processError( error: RawError, - foregroundContexts: ForegroundContexts, + pageStateHistory: PageStateHistory, featureFlagContexts: FeatureFlagContexts ): RawRumEventCollectedData { const rawRumEvent: RawRumErrorEvent = { @@ -107,11 +107,7 @@ function processError( fingerprint: error.fingerprint, }, type: RumEventType.ERROR as const, - } - - const inForeground = foregroundContexts.isInForegroundAt(error.startClocks.relative) - if (inForeground) { - rawRumEvent.view = { in_foreground: inForeground } + view: { in_foreground: pageStateHistory.isInActivePageStateAt(error.startClocks.relative) }, } const featureFlagContext = featureFlagContexts.findFeatureFlagEvaluations(error.startClocks.relative) diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts index 6517e04ead..998c22d7ec 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.spec.ts @@ -6,6 +6,7 @@ import { setup, noopRecorderApi } from '../../../../test' import type { RawRumViewEvent } from '../../../rawRumEvent.types' import { RumEventType, ViewLoadingType } from '../../../rawRumEvent.types' import { LifeCycleEventType } from '../../lifeCycle' +import { PageState } from '../../contexts/pageStateHistory' import type { ViewEvent } from './trackViews' import { startViewCollection } from './viewCollection' @@ -57,14 +58,16 @@ describe('viewCollection', () => { beforeEach(() => { setupBuilder = setup() - .withForegroundContexts({ - selectInForegroundPeriodsFor: () => [{ start: 0 as ServerDuration, duration: 10 as ServerDuration }], + .withPageStateHistory({ + findAll: () => [ + { start: 0 as ServerDuration, state: PageState.ACTIVE }, + { start: 10 as ServerDuration, state: PageState.PASSIVE }, + ], }) .beforeBuild( ({ lifeCycle, configuration, - foregroundContexts, featureFlagContexts, domMutationObservable, locationChangeObservable, @@ -77,7 +80,6 @@ describe('viewCollection', () => { location, domMutationObservable, locationChangeObservable, - foregroundContexts, featureFlagContexts, pageStateHistory, { diff --git a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts index 2e2036386b..3ef63f2456 100644 --- a/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts +++ b/packages/rum-core/src/domain/rumEventsCollection/view/viewCollection.ts @@ -12,7 +12,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 '../../contexts/foregroundContexts' +import { mapToForegroundPeriods } from '../../contexts/foregroundContexts' import type { LocationChange } from '../../../browser/locationChangeObservable' import type { RumConfiguration } from '../../configuration' import type { FeatureFlagContexts } from '../../contexts/featureFlagContext' @@ -26,7 +26,6 @@ export function startViewCollection( location: Location, domMutationObservable: Observable, locationChangeObservable: Observable, - foregroundContexts: ForegroundContexts, featureFlagContexts: FeatureFlagContexts, pageStateHistory: PageStateHistory, recorderApi: RecorderApi, @@ -35,7 +34,7 @@ export function startViewCollection( lifeCycle.subscribe(LifeCycleEventType.VIEW_UPDATED, (view) => lifeCycle.notify( LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, - processViewUpdate(view, configuration, foregroundContexts, featureFlagContexts, recorderApi, pageStateHistory) + processViewUpdate(view, configuration, featureFlagContexts, recorderApi, pageStateHistory) ) ) const trackViewResult = trackViews( @@ -54,7 +53,6 @@ export function startViewCollection( function processViewUpdate( view: ViewEvent, configuration: RumConfiguration, - foregroundContexts: ForegroundContexts, featureFlagContexts: FeatureFlagContexts, recorderApi: RecorderApi, pageStateHistory: PageStateHistory @@ -62,11 +60,12 @@ function processViewUpdate( const replayStats = recorderApi.getReplayStats(view.id) const featureFlagContext = featureFlagContexts.findFeatureFlagEvaluations(view.startClocks.relative) const pageStatesEnabled = isExperimentalFeatureEnabled(ExperimentalFeature.PAGE_STATES) + const pageStates = pageStateHistory.findAll(view.startClocks.relative, view.duration) const viewEvent: RawRumViewEvent = { _dd: { document_version: view.documentVersion, replay_stats: replayStats, - page_states: pageStatesEnabled ? pageStateHistory.findAll(view.startClocks.relative, view.duration) : undefined, + page_states: pageStatesEnabled ? pageStates : undefined, }, date: view.startClocks.timeStamp, type: RumEventType.VIEW, @@ -101,9 +100,8 @@ function processViewUpdate( count: view.eventCounts.resourceCount, }, time_spent: toServerDuration(view.duration), - in_foreground_periods: !pageStatesEnabled - ? foregroundContexts.selectInForegroundPeriodsFor(view.startClocks.relative, view.duration) - : undefined, + in_foreground_periods: + !pageStatesEnabled && pageStates ? mapToForegroundPeriods(pageStates, view.duration) : undefined, // Todo: Remove in the next major release }, feature_flags: featureFlagContext && !isEmptyObject(featureFlagContext) ? featureFlagContext : undefined, display: view.scrollMetrics diff --git a/packages/rum-core/test/testSetupBuilder.ts b/packages/rum-core/test/testSetupBuilder.ts index c0bc3b58ef..be5388986a 100644 --- a/packages/rum-core/test/testSetupBuilder.ts +++ b/packages/rum-core/test/testSetupBuilder.ts @@ -1,4 +1,4 @@ -import type { Context, ContextManager, ServerDuration, TimeStamp } from '@datadog/browser-core' +import type { Context, ContextManager, TimeStamp } from '@datadog/browser-core' import { CustomerDataType, assign, combine, createContextManager, noop, Observable } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock, buildLocation, SPEC_ENDPOINTS } from '@datadog/browser-core/test' @@ -6,9 +6,7 @@ import type { LocationChange } from '../src/browser/locationChangeObservable' import type { RumConfiguration } from '../src/domain/configuration' import { validateAndBuildRumConfiguration } from '../src/domain/configuration' import type { FeatureFlagContexts } from '../src/domain/contexts/featureFlagContext' -import type { ForegroundContexts } from '../src/domain/contexts/foregroundContexts' import type { PageStateHistory } from '../src/domain/contexts/pageStateHistory' -import { PageState } from '../src/domain/contexts/pageStateHistory' import type { UrlContexts } from '../src/domain/contexts/urlContexts' import type { ViewContexts } from '../src/domain/contexts/viewContexts' import type { RawRumEventCollectedData } from '../src/domain/lifeCycle' @@ -26,7 +24,7 @@ export interface TestSetupBuilder { withConfiguration: (overrides: Partial) => TestSetupBuilder withViewContexts: (stub: Partial) => TestSetupBuilder withActionContexts: (stub: ActionContexts) => TestSetupBuilder - withForegroundContexts: (stub: Partial) => TestSetupBuilder + withPageStateHistory: (stub: Partial) => TestSetupBuilder withFeatureFlagContexts: (stub: Partial) => TestSetupBuilder withFakeClock: () => TestSetupBuilder beforeBuild: (callback: BeforeBuildCallback) => TestSetupBuilder @@ -49,7 +47,6 @@ export interface BuildContext { applicationId: string viewContexts: ViewContexts actionContexts: ActionContexts - foregroundContexts: ForegroundContexts pageStateHistory: PageStateHistory featureFlagContexts: FeatureFlagContexts urlContexts: UrlContexts @@ -95,17 +92,14 @@ export function setup(): TestSetupBuilder { let actionContexts: ActionContexts = { findActionId: noop as () => undefined, } - let foregroundContexts: ForegroundContexts = { - isInForegroundAt: () => undefined, - selectInForegroundPeriodsFor: () => undefined, - stop: noop, - } + const globalContextManager = createContextManager(CustomerDataType.GlobalContext) const userContextManager = createContextManager(CustomerDataType.User) - const pageStateHistory: PageStateHistory = { - findAll: () => [{ start: 0 as ServerDuration, state: PageState.ACTIVE }], + let pageStateHistory: PageStateHistory = { + findAll: () => undefined, addPageState: noop, stop: noop, + isInActivePageStateAt: () => false, } const FAKE_APP_ID = 'appId' const configuration: RumConfiguration = { @@ -154,8 +148,8 @@ export function setup(): TestSetupBuilder { actionContexts = stub return setupBuilder }, - withForegroundContexts(stub: Partial) { - foregroundContexts = { ...foregroundContexts, ...stub } + withPageStateHistory(stub: Partial) { + pageStateHistory = { ...pageStateHistory, ...stub } return setupBuilder }, withFeatureFlagContexts(stub: Partial) { @@ -180,7 +174,6 @@ export function setup(): TestSetupBuilder { viewContexts, urlContexts, actionContexts, - foregroundContexts, pageStateHistory, featureFlagContexts, sessionManager,