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
45 changes: 11 additions & 34 deletions packages/rum-core/src/domain/contexts/pageStateHistory.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { RelativeTime } from '@datadog/browser-core'
import type { RelativeTime, ServerDuration } from '@datadog/browser-core'
import { resetExperimentalFeatures } from '@datadog/browser-core'
import type { TestSetupBuilder } from '../../../test'
import { setup } from '../../../test'
import type { PageStateHistory } from './pageStateHistory'
import { resetPageStates, startPageStateHistory, addPageState, PageState } from './pageStateHistory'
import { startPageStateHistory, PageState } from './pageStateHistory'

describe('pageStateHistory', () => {
let pageStateHistory: PageStateHistory
Expand All @@ -20,7 +20,7 @@ describe('pageStateHistory', () => {

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

Expand All @@ -36,56 +36,33 @@ describe('pageStateHistory', () => {

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)
pageStateHistory.addPageState(PageState.TERMINATED)

expect(pageStateHistory.findAll(15 as RelativeTime, 20 as RelativeTime)).toEqual([
{
state: PageState.PASSIVE,
startTime: 10 as RelativeTime,
start: 0 as ServerDuration,
},
{
state: PageState.HIDDEN,
startTime: 20 as RelativeTime,
start: 5000000 as ServerDuration,
},
{
state: PageState.FROZEN,
startTime: 30 as RelativeTime,
},
])
})

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

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

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

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

expect(pageStateHistory.findAll(0 as RelativeTime, 40 as RelativeTime)).toEqual([
{
state: PageState.HIDDEN,
startTime: 30 as RelativeTime,
start: 15000000 as ServerDuration,
},
])
})
Expand Down
108 changes: 60 additions & 48 deletions packages/rum-core/src/domain/contexts/pageStateHistory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import type { Duration, RelativeTime } from '@datadog/browser-core'
import { addDuration, addEventListeners, relativeNow, DOM_EVENT } from '@datadog/browser-core'
import {
elapsed,
ValueHistory,
SESSION_TIME_OUT_DELAY,
toServerDuration,
addEventListeners,
relativeNow,
DOM_EVENT,
} from '@datadog/browser-core'
import type { PageStateServerEntry } from '../../rawRumEvent.types'

export const MAX_PAGE_STATE_ENTRIES = 500
export const MAX_PAGE_STATE_ENTRIES = 200
bcaudan marked this conversation as resolved.
Show resolved Hide resolved

export const PAGE_STATE_CONTEXT_TIME_OUT_DELAY = SESSION_TIME_OUT_DELAY

export const enum PageState {
ACTIVE = 'active',
Expand All @@ -10,18 +21,32 @@ export const enum PageState {
FROZEN = 'frozen',
TERMINATED = 'terminated',
}

export type PageStateEntry = { state: PageState; startTime: RelativeTime }

export interface PageStateHistory {
findAll: (startTime: RelativeTime, duration: Duration) => PageStateEntry[] | undefined
findAll: (startTime: RelativeTime, duration?: Duration) => PageStateServerEntry[] | undefined
addPageState(nextPageState: PageState, startTime?: RelativeTime): void
stop: () => void
}
let pageStateEntries: PageStateEntry[] = []
let currentPageState: PageState | undefined

export function startPageStateHistory(): PageStateHistory {
addPageState(getPageState())
const pageStateHistory = new ValueHistory<PageStateEntry>(PAGE_STATE_CONTEXT_TIME_OUT_DELAY, MAX_PAGE_STATE_ENTRIES)

let currentPageState: PageState
addPageState(getPageState(), relativeNow())

function addPageState(nextPageState: PageState, startTime = relativeNow()) {
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
if (nextPageState === currentPageState) {
return
}

currentPageState = nextPageState
pageStateHistory.closeActive(startTime)
pageStateHistory.add({ state: currentPageState, startTime }, startTime)
}

const { stop } = addEventListeners(
const { stop: stopEventListeners } = addEventListeners(
window,
[
DOM_EVENT.PAGE_SHOW,
Expand All @@ -38,67 +63,54 @@ export function startPageStateHistory(): PageStateHistory {
if (!event.isTrusted) {
return
}
const startTime = event.timeStamp as RelativeTime

if (event.type === DOM_EVENT.FREEZE) {
addPageState(PageState.FROZEN)
addPageState(PageState.FROZEN, startTime)
} else if (event.type === DOM_EVENT.PAGE_HIDE) {
addPageState((event as PageTransitionEvent).persisted ? PageState.FROZEN : PageState.TERMINATED)
addPageState((event as PageTransitionEvent).persisted ? PageState.FROZEN : PageState.TERMINATED, startTime)
} else {
addPageState(getPageState())
addPageState(getPageState(), startTime)
amortemousque marked this conversation as resolved.
Show resolved Hide resolved
}
},
{ capture: true }
)

return {
findAll(startTime: RelativeTime, duration: Duration) {
const entries: PageStateEntry[] = []
const endTime = addDuration(startTime, duration)
for (let i = pageStateEntries.length - 1; i >= 0; i--) {
const { startTime: stateStartTime } = pageStateEntries[i]

if (stateStartTime >= endTime) {
continue
}

entries.unshift(pageStateEntries[i])

if (stateStartTime < startTime) {
break
}
findAll: (startTime: RelativeTime, duration?: Duration): PageStateServerEntry[] | undefined => {
const pageStateEntries = pageStateHistory
.findAll(startTime, duration)
.reverse()
.map((pageState) => {
const correctedStartTime = startTime > pageState.startTime ? startTime : pageState.startTime
const recenteredStartTime = elapsed(startTime, correctedStartTime)

return {
state: pageState.state,
start: toServerDuration(recenteredStartTime),
}
})

if (pageStateEntries.length > 0) {
return pageStateEntries
}
amortemousque marked this conversation as resolved.
Show resolved Hide resolved

return entries.length ? entries : undefined
},
stop,
addPageState,
stop: () => {
stopEventListeners()
pageStateHistory.stop()
},
}
}

function getPageState(): PageState {
export function getPageState() {
if (document.visibilityState === 'hidden') {
return PageState.HIDDEN
}

if (document.hasFocus()) {
return PageState.ACTIVE
}
return PageState.PASSIVE
}

export function addPageState(nextPageState: PageState, maxPageStateEntries = MAX_PAGE_STATE_ENTRIES) {
if (nextPageState === currentPageState) {
return
}

currentPageState = nextPageState

if (pageStateEntries.length === maxPageStateEntries) {
pageStateEntries.shift()
}

pageStateEntries.push({ state: currentPageState, startTime: relativeNow() })
}

export function resetPageStates() {
pageStateEntries = []
currentPageState = undefined
return PageState.PASSIVE
}
7 changes: 5 additions & 2 deletions packages/rum-core/src/rawRumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
TimeStamp,
RawErrorCause,
} from '@datadog/browser-core'
import type { PageStateEntry } from './domain/contexts/pageStateHistory'
import type { PageState } from './domain/contexts/pageStateHistory'
import type { RumSessionPlan } from './domain/rumSessionManager'

export const enum RumEventType {
Expand Down Expand Up @@ -42,7 +42,7 @@ export interface RawRumResourceEvent {
span_id?: string // not available for initial document tracing
rule_psr?: number
discarded: boolean
page_states?: PageStateEntry[]
page_states?: PageStateServerEntry[]
}
}

Expand Down Expand Up @@ -110,6 +110,7 @@ export interface RawRumViewEvent {
_dd: {
document_version: number
replay_stats?: ReplayStats
page_states?: PageStateServerEntry[]
}
}

Expand All @@ -118,6 +119,8 @@ export interface InForegroundPeriod {
duration: ServerDuration
}

export type PageStateServerEntry = { state: PageState; start: ServerDuration }

export const enum ViewLoadingType {
INITIAL_LOAD = 'initial_load',
ROUTE_CHANGE = 'route_change',
Expand Down