Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔊 [RUMF-1577] Collect page lifecycle states #2229

Merged
merged 9 commits into from
May 17, 2023
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 @@ -25,6 +25,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 @@ -42,6 +43,7 @@ function startRumStub(
location: Location,
domMutationObservable: Observable<void>,
locationChangeObservable: Observable<LocationChange>,
pageStateHistory: PageStateHistory,
reportError: (error: RawError) => void
) {
const { stop: rumEventCollectionStop, foregroundContexts } = startRumEventCollection(
Expand All @@ -66,6 +68,7 @@ function startRumStub(
locationChangeObservable,
foregroundContexts,
startFeatureFlagContexts(lifeCycle),
pageStateHistory,
noopRecorderApi
)

Expand All @@ -88,7 +91,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 @@ -97,6 +108,7 @@ describe('rum session', () => {
location,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
noop
)
}
Expand Down Expand Up @@ -141,7 +153,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 @@ -150,6 +170,7 @@ describe('rum session keep alive', () => {
location,
domMutationObservable,
locationChangeObservable,
pageStateHistory,
noop
)
}
Expand Down Expand Up @@ -212,7 +233,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 @@ -221,6 +250,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