Skip to content

Commit

Permalink
🔊 [RUMF-1577] Collect page lifecycle states (#2229)
Browse files Browse the repository at this point in the history
  • Loading branch information
amortemousque authored May 17, 2023
1 parent 0a0e00a commit a865fc1
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 135 deletions.
1 change: 1 addition & 0 deletions packages/core/src/tools/experimentalFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum ExperimentalFeature {
PAGEHIDE = 'pagehide',
FEATURE_FLAGS = 'feature_flags',
RESOURCE_PAGE_STATES = 'resource_page_states',
PAGE_STATES = 'page_states',
COLLECT_FLUSH_REASON = 'collect_flush_reason',
}

Expand Down
23 changes: 22 additions & 1 deletion packages/core/src/tools/valueHistory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { addDuration, ONE_MINUTE } from './utils/timeUtils'
import { CLEAR_OLD_VALUES_INTERVAL, ValueHistory } from './valueHistory'

const EXPIRE_DELAY = 10 * ONE_MINUTE
const MAX_ENTRIES = 5

describe('valueHistory', () => {
let valueHistory: ValueHistory<string>
let clock: Clock

beforeEach(() => {
clock = mockClock()
valueHistory = new ValueHistory(EXPIRE_DELAY)
valueHistory = new ValueHistory(EXPIRE_DELAY, MAX_ENTRIES)
})

afterEach(() => {
Expand Down Expand Up @@ -86,6 +87,17 @@ describe('valueHistory', () => {

expect(valueHistory.findAll(15 as RelativeTime)).toEqual([])
})

it('should return all context overlapping with the duration', () => {
valueHistory.add('foo', 0 as RelativeTime)
valueHistory.add('bar', 5 as RelativeTime).close(10 as RelativeTime)
valueHistory.add('baz', 10 as RelativeTime).close(15 as RelativeTime)
valueHistory.add('qux', 15 as RelativeTime)

expect(valueHistory.findAll(0 as RelativeTime, 20 as Duration)).toEqual(['qux', 'baz', 'bar', 'foo'])
expect(valueHistory.findAll(6 as RelativeTime, 5 as Duration)).toEqual(['baz', 'bar', 'foo'])
expect(valueHistory.findAll(11 as RelativeTime, 5 as Duration)).toEqual(['qux', 'baz', 'foo'])
})
})

describe('removing entries', () => {
Expand Down Expand Up @@ -124,4 +136,13 @@ describe('valueHistory', () => {

expect(valueHistory.find(originalTime)).toBeUndefined()
})

it('should limit the number of entries', () => {
for (let i = 0; i < MAX_ENTRIES + 1; i++) {
valueHistory.add(`${i}`, 0 as RelativeTime)
}
const values = valueHistory.findAll()
expect(values.length).toEqual(5)
expect(values).toEqual(['5', '4', '3', '2', '1'])
})
})
22 changes: 15 additions & 7 deletions packages/core/src/tools/valueHistory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setInterval, clearInterval } from './timer'
import type { TimeoutId } from './timer'
import type { RelativeTime } from './utils/timeUtils'
import { relativeNow, ONE_MINUTE } from './utils/timeUtils'
import type { Duration, RelativeTime } from './utils/timeUtils'
import { addDuration, relativeNow, ONE_MINUTE } from './utils/timeUtils'

const END_OF_TIMES = Infinity as RelativeTime

Expand All @@ -23,7 +23,7 @@ export class ValueHistory<Value> {
private entries: Array<ValueHistoryEntry<Value>> = []
private clearOldValuesInterval: TimeoutId

constructor(private expireDelay: number) {
constructor(private expireDelay: number, private maxEntries?: number) {
this.clearOldValuesInterval = setInterval(() => this.clearOldValues(), CLEAR_OLD_VALUES_INTERVAL)
}

Expand All @@ -46,7 +46,13 @@ export class ValueHistory<Value> {
entry.endTime = endTime
},
}

if (this.maxEntries && this.entries.length >= this.maxEntries) {
this.entries.pop()
}

this.entries.unshift(entry)

return entry
}

Expand Down Expand Up @@ -77,12 +83,14 @@ export class ValueHistory<Value> {
}

/**
* Return all values that were active during `startTime`, or all currently active values if no
* `startTime` is provided.
* Return all values with an active period overlapping with the duration,
* or all values that were active during `startTime` if no duration is provided,
* or all currently active values if no `startTime` is provided.
*/
findAll(startTime: RelativeTime = END_OF_TIMES): Value[] {
findAll(startTime: RelativeTime = END_OF_TIMES, duration = 0 as Duration): Value[] {
const endTime = addDuration(startTime, duration)
return this.entries
.filter((entry) => entry.startTime <= startTime && startTime <= entry.endTime)
.filter((entry) => entry.startTime <= endTime && startTime <= entry.endTime)
.map((entry) => entry.value)
}

Expand Down
36 changes: 33 additions & 3 deletions packages/rum-core/src/boot/startRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { RumSessionManager } from '..'
import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration'
import { RumEventType } from '../rawRumEvent.types'
import { startFeatureFlagContexts } from '../domain/contexts/featureFlagContext'
import type { PageStateHistory } from '../domain/contexts/pageStateHistory'
import { startRum, startRumEventCollection } from './startRum'

function collectServerEvents(lifeCycle: LifeCycle) {
Expand All @@ -47,6 +48,7 @@ function startRumStub(
location: Location,
domMutationObservable: Observable<void>,
locationChangeObservable: Observable<LocationChange>,
pageStateHistory: PageStateHistory,
reportError: (error: RawError) => void
) {
const { stop: rumEventCollectionStop, foregroundContexts } = startRumEventCollection(
Expand All @@ -71,6 +73,7 @@ function startRumStub(
locationChangeObservable,
foregroundContexts,
startFeatureFlagContexts(lifeCycle),
pageStateHistory,
noopRecorderApi
)

Expand All @@ -93,7 +96,15 @@ describe('rum session', () => {
}

setupBuilder = setup().beforeBuild(
({ location, lifeCycle, configuration, sessionManager, domMutationObservable, locationChangeObservable }) => {
({
location,
lifeCycle,
configuration,
sessionManager,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
}) => {
serverRumEvents = collectServerEvents(lifeCycle)
return startRumStub(
lifeCycle,
Expand All @@ -102,6 +113,7 @@ describe('rum session', () => {
location,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
noop
)
}
Expand Down Expand Up @@ -146,7 +158,15 @@ describe('rum session keep alive', () => {
.withFakeClock()
.withSessionManager(sessionManager)
.beforeBuild(
({ location, lifeCycle, configuration, sessionManager, domMutationObservable, locationChangeObservable }) => {
({
location,
lifeCycle,
configuration,
sessionManager,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
}) => {
serverRumEvents = collectServerEvents(lifeCycle)
return startRumStub(
lifeCycle,
Expand All @@ -155,6 +175,7 @@ describe('rum session keep alive', () => {
location,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
noop
)
}
Expand Down Expand Up @@ -217,7 +238,15 @@ describe('rum events url', () => {

beforeEach(() => {
setupBuilder = setup().beforeBuild(
({ location, lifeCycle, configuration, sessionManager, domMutationObservable, locationChangeObservable }) => {
({
location,
lifeCycle,
configuration,
sessionManager,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
}) => {
serverRumEvents = collectServerEvents(lifeCycle)
return startRumStub(
lifeCycle,
Expand All @@ -226,6 +255,7 @@ describe('rum events url', () => {
location,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
noop
)
}
Expand Down
26 changes: 15 additions & 11 deletions packages/rum-core/src/boot/startRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,21 @@ export function startRum(
const domMutationObservable = createDOMMutationObservable()
const locationChangeObservable = createLocationChangeObservable(location)

const { viewContexts, foregroundContexts, urlContexts, actionContexts, addAction } = startRumEventCollection(
lifeCycle,
configuration,
location,
session,
locationChangeObservable,
domMutationObservable,
() => buildCommonContext(globalContextManager, userContextManager, recorderApi),
reportError
)
const { viewContexts, foregroundContexts, pageStateHistory, urlContexts, actionContexts, addAction } =
startRumEventCollection(
lifeCycle,
configuration,
location,
session,
locationChangeObservable,
domMutationObservable,
() => buildCommonContext(globalContextManager, userContextManager, recorderApi),
reportError
)

addTelemetryConfiguration(serializeRumConfiguration(initConfiguration))

startLongTaskCollection(lifeCycle, session)
const pageStateHistory = startPageStateHistory()
startResourceCollection(lifeCycle, configuration, session, pageStateHistory)
const { addTiming, startView } = startViewCollection(
lifeCycle,
Expand All @@ -126,6 +126,7 @@ export function startRum(
locationChangeObservable,
foregroundContexts,
featureFlagContexts,
pageStateHistory,
recorderApi,
initialViewOptions
)
Expand Down Expand Up @@ -179,6 +180,8 @@ export function startRumEventCollection(
const urlContexts = startUrlContexts(lifeCycle, locationChangeObservable, location)

const foregroundContexts = startForegroundContexts()
const pageStateHistory = startPageStateHistory()

const { addAction, actionContexts } = startActionCollection(
lifeCycle,
domMutationObservable,
Expand All @@ -200,6 +203,7 @@ export function startRumEventCollection(
return {
viewContexts,
foregroundContexts,
pageStateHistory,
urlContexts,
addAction,
actionContexts,
Expand Down
83 changes: 35 additions & 48 deletions packages/rum-core/src/domain/contexts/pageStateHistory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,79 @@
import type { RelativeTime } from '@datadog/browser-core'
import { resetExperimentalFeatures } from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../../test'
import { setup } from '../../../test'
import type { RelativeTime, ServerDuration } from '@datadog/browser-core'
import type { Clock } from '../../../../core/test'
import { mockClock } from '../../../../core/test'
import type { PageStateHistory } from './pageStateHistory'
import { resetPageStates, startPageStateHistory, addPageState, PageState } from './pageStateHistory'
import { startPageStateHistory, PageState } from './pageStateHistory'

describe('pageStateHistory', () => {
let pageStateHistory: PageStateHistory
let setupBuilder: TestSetupBuilder

let clock: Clock
beforeEach(() => {
setupBuilder = setup()
.withFakeClock()
.beforeBuild(() => {
pageStateHistory = startPageStateHistory()
return pageStateHistory
})
clock = mockClock()
pageStateHistory = startPageStateHistory()
})

afterEach(() => {
setupBuilder.cleanup()
resetPageStates()
resetExperimentalFeatures()
pageStateHistory.stop()
clock.cleanup()
})

it('should have the current state when starting', () => {
setupBuilder.build()
expect(pageStateHistory.findAll(0 as RelativeTime, 10 as RelativeTime)).toBeDefined()
})

it('should return undefined if the time period is out of history bounds', () => {
pageStateHistory = startPageStateHistory()
expect(pageStateHistory.findAll(-10 as RelativeTime, 0 as RelativeTime)).not.toBeDefined()
})

it('should return the correct page states for the given time period', () => {
const { clock } = setupBuilder.build()
resetPageStates()

addPageState(PageState.ACTIVE)
pageStateHistory.addPageState(PageState.ACTIVE)

clock.tick(10)
addPageState(PageState.PASSIVE)
pageStateHistory.addPageState(PageState.PASSIVE)

clock.tick(10)
addPageState(PageState.HIDDEN)
pageStateHistory.addPageState(PageState.HIDDEN)

clock.tick(10)
addPageState(PageState.FROZEN)
pageStateHistory.addPageState(PageState.FROZEN)

clock.tick(10)
addPageState(PageState.TERMINATED)

expect(pageStateHistory.findAll(15 as RelativeTime, 20 as RelativeTime)).toEqual([
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,
startTime: 10 as RelativeTime,
start: -5000000 as ServerDuration,
},
{
state: PageState.HIDDEN,
startTime: 20 as RelativeTime,
start: 5000000 as ServerDuration,
},
{
state: PageState.FROZEN,
startTime: 30 as RelativeTime,
start: 15000000 as ServerDuration,
},
])
})

it('should limit the history entry number', () => {
const limit = 1
const { clock } = setupBuilder.build()
resetPageStates()

clock.tick(10)
addPageState(PageState.ACTIVE, limit)
it('should limit the number of selectable entries', () => {
const maxPageStateEntriesSelectable = 1
pageStateHistory = startPageStateHistory(maxPageStateEntriesSelectable)

pageStateHistory.addPageState(PageState.ACTIVE)
clock.tick(10)
addPageState(PageState.PASSIVE, limit)
pageStateHistory.addPageState(PageState.PASSIVE)

clock.tick(10)
addPageState(PageState.HIDDEN, limit)

expect(pageStateHistory.findAll(0 as RelativeTime, 40 as RelativeTime)).toEqual([
{
state: PageState.HIDDEN,
startTime: 30 as RelativeTime,
},
])
expect(pageStateHistory.findAll(0 as RelativeTime, Infinity as RelativeTime)?.length).toEqual(
maxPageStateEntriesSelectable
)
})
})
Loading

0 comments on commit a865fc1

Please sign in to comment.