diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 767dcb0521..3845334c8c 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -39,6 +39,7 @@ export interface SessionContext extends Context { trackingType: TrackingType isReplayForced: boolean anonymousId: string | undefined + expire: string | undefined } export const VISIBILITY_CHECK_DELAY = ONE_MINUTE @@ -88,6 +89,15 @@ export function startSessionManager( } } + sessionStore.sessionStateUpdateObservable.subscribe(({ previousState, newState }) => { + if (previousState.id === newState.id && previousState.expire !== newState.expire) { + const entry = sessionContextHistory.find() + if (entry) { + entry.expire = newState.expire + } + } + }) + trackingConsentState.observable.subscribe(() => { if (trackingConsentState.isGranted()) { sessionStore.expandOrRenewSession() @@ -115,6 +125,7 @@ export function startSessionManager( trackingType: SESSION_NOT_TRACKED as TrackingType, isReplayForced: false, anonymousId: undefined, + expire: undefined, } } @@ -123,6 +134,7 @@ export function startSessionManager( trackingType: session[productKey] as TrackingType, isReplayForced: !!session.forcedReplay, anonymousId: session.anonymousId, + expire: session.expire, } } diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index 8143b845e3..0c1357473a 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -73,7 +73,7 @@ function startRumStub( const hooks = createHooks() const viewHistory = startViewHistory(lifeCycle) const urlContexts = startUrlContexts(lifeCycle, hooks, locationChangeObservable, location) - startSessionContext(hooks, sessionManager, noopRecorderApi, viewHistory) + startSessionContext(hooks, sessionManager, noopRecorderApi, viewHistory, pageStateHistory) const { stop: rumEventCollectionStop } = startRumEventCollection( lifeCycle, diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 0e9d6fa180..84778cc4b7 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -136,7 +136,7 @@ export function startRum( const urlContexts = startUrlContexts(lifeCycle, hooks, locationChangeObservable, location) cleanupTasks.push(() => urlContexts.stop()) const featureFlagContexts = startFeatureFlagContexts(lifeCycle, hooks, configuration) - startSessionContext(hooks, session, recorderApi, viewHistory) + startSessionContext(hooks, session, recorderApi, viewHistory, pageStateHistory) startConnectivityContext(hooks) startTrackingConsentContext(hooks, trackingConsentState) const globalContext = startGlobalContext(hooks, configuration, 'rum', true) diff --git a/packages/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index fd8d42aa99..fa3a136774 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -8,6 +8,7 @@ import { mockRumConfiguration, mockViewHistory, noopRecorderApi, + mockPageStateHistory, } from '../../test' import type { RumEventDomainContext } from '../domainContext.types' import type { RawRumEvent } from '../rawRumEvent.types' @@ -574,7 +575,7 @@ function setupAssemblyTestWithDefaults({ const recorderApi = noopRecorderApi const viewHistory = { ...mockViewHistory(), findView: () => findView() } startGlobalContext(hooks, mockRumConfiguration(), 'rum', true) - startSessionContext(hooks, rumSessionManager, recorderApi, viewHistory) + startSessionContext(hooks, rumSessionManager, recorderApi, viewHistory, mockPageStateHistory()) startRumAssembly(mockRumConfiguration(partialConfiguration), lifeCycle, hooks, reportErrorSpy) registerCleanupTask(() => { diff --git a/packages/rum-core/src/domain/contexts/pageStateHistory.ts b/packages/rum-core/src/domain/contexts/pageStateHistory.ts index 0f7e543c5c..355f1471fc 100644 --- a/packages/rum-core/src/domain/contexts/pageStateHistory.ts +++ b/packages/rum-core/src/domain/contexts/pageStateHistory.ts @@ -39,6 +39,7 @@ export interface PageStateEntry { export interface PageStateHistory { wasInPageStateDuringPeriod: (state: PageState, startTime: RelativeTime, duration: Duration) => boolean addPageState(nextPageState: PageState, startTime?: RelativeTime): void + findPageStatesForPeriod: (startTime: RelativeTime, duration: Duration) => PageStateServerEntry[] | undefined stop: () => void } @@ -99,14 +100,18 @@ export function startPageStateHistory( return pageStateEntryHistory.findAll(startTime, duration).some((pageState) => pageState.state === state) } + function findPageStatesForPeriod(startTime: RelativeTime, duration: Duration) { + const pageStateEntries = pageStateEntryHistory.findAll(startTime, duration) + return processPageStates(pageStateEntries, startTime, maxPageStateEntriesSelectable) + } + hooks.register( HookNames.Assemble, ({ startTime, duration = 0 as Duration, eventType }): DefaultRumEventAttributes | SKIPPED => { if (eventType === RumEventType.VIEW) { - const pageStates = pageStateEntryHistory.findAll(startTime, duration) return { type: eventType, - _dd: { page_states: processPageStates(pageStates, startTime, maxPageStateEntriesSelectable) }, + _dd: { page_states: findPageStatesForPeriod(startTime, duration) }, } } @@ -124,6 +129,7 @@ export function startPageStateHistory( return { wasInPageStateDuringPeriod, addPageState, + findPageStatesForPeriod, stop: () => { stopEventListeners() pageStateEntryHistory.stop() diff --git a/packages/rum-core/src/domain/contexts/sessionContext.spec.ts b/packages/rum-core/src/domain/contexts/sessionContext.spec.ts index 40feff50e0..1eed61ad69 100644 --- a/packages/rum-core/src/domain/contexts/sessionContext.spec.ts +++ b/packages/rum-core/src/domain/contexts/sessionContext.spec.ts @@ -1,7 +1,11 @@ import type { RelativeTime } from '@datadog/browser-core' import { clocksNow, DISCARDED, HookNames } from '@datadog/browser-core' -import type { RumSessionManagerMock } from '../../../test' -import { createRumSessionManagerMock, noopRecorderApi } from '../../../test' +import { + type RumSessionManagerMock, + mockPageStateHistory, + createRumSessionManagerMock, + noopRecorderApi, +} from '../../../test' import { SessionType } from '../rumSessionManager' import type { DefaultRumEventAttributes, DefaultTelemetryEventAttributes, Hooks } from '../hooks' import { createHooks } from '../hooks' @@ -37,7 +41,7 @@ describe('session context', () => { getReplayStatsSpy = spyOn(recorderApi, 'getReplayStats') findViewSpy = spyOn(viewHistory, 'findView').and.returnValue(fakeView) - startSessionContext(hooks, sessionManager, recorderApi, viewHistory) + startSessionContext(hooks, sessionManager, recorderApi, viewHistory, mockPageStateHistory()) }) it('should set id and type', () => { diff --git a/packages/rum-core/src/domain/contexts/sessionContext.ts b/packages/rum-core/src/domain/contexts/sessionContext.ts index 6d114a5242..47935aea34 100644 --- a/packages/rum-core/src/domain/contexts/sessionContext.ts +++ b/packages/rum-core/src/domain/contexts/sessionContext.ts @@ -1,16 +1,32 @@ -import { DISCARDED, HookNames, SKIPPED } from '@datadog/browser-core' -import { SessionReplayState, SessionType } from '../rumSessionManager' +import type { ContextValue } from '@datadog/browser-core' +import { + toServerDuration, + DISCARDED, + HookNames, + SKIPPED, + dateNow, + ONE_MINUTE, + addTelemetryDebug, + elapsed, + relativeNow, + relativeToClocks, +} from '@datadog/browser-core' import type { RumSessionManager } from '../rumSessionManager' +import { SessionReplayState, SessionType } from '../rumSessionManager' +import type { PageStateServerEntry } from '../../rawRumEvent.types' import { RumEventType } from '../../rawRumEvent.types' import type { RecorderApi } from '../../boot/rumPublicApi' import type { DefaultRumEventAttributes, DefaultTelemetryEventAttributes, Hooks } from '../hooks' import type { ViewHistory } from './viewHistory' +import type { PageStateHistory } from './pageStateHistory' +import { PageState } from './pageStateHistory' export function startSessionContext( hooks: Hooks, sessionManager: RumSessionManager, recorderApi: RecorderApi, - viewHistory: ViewHistory + viewHistory: ViewHistory, + pageStateHistory: PageStateHistory ) { hooks.register(HookNames.Assemble, ({ eventType, startTime }): DefaultRumEventAttributes | DISCARDED => { const session = sessionManager.findTrackedSession(startTime) @@ -19,6 +35,42 @@ export function startSessionContext( if (!session || !view) { return DISCARDED } + // TODO share constant + const isSessionExpired = dateNow() - Number(session.expire) > 15 * ONE_MINUTE + const eventDuration = elapsed(startTime, relativeNow()) + const wasInFrozenState = pageStateHistory.wasInPageStateDuringPeriod(PageState.FROZEN, startTime, eventDuration) + if (isSessionExpired || (wasInFrozenState && eventType !== RumEventType.VIEW)) { + const pageStatesForPeriod = pageStateHistory.findPageStatesForPeriod(startTime, eventDuration)! + // monitor-until: 2026-01-01 + addTelemetryDebug('Event sent after session expiration or frozen page state', { + debug: { + eventDuration, + eventType, + isSessionExpired, + sessionExpiredSince: isSessionExpired ? dateNow() - Number(session.expire) : undefined, + elapsedBetweenStartAndExpire: isSessionExpired + ? Number(session.expire) - relativeToClocks(startTime).timeStamp + : undefined, + wasInFrozenState, + pageStateDuringEventDuration: pageStatesForPeriod as ContextValue, + sumFrozenDuration: wasInFrozenState ? computeSumFrozenDuration(pageStatesForPeriod) : undefined, + }, + }) + } + + /** + * Compute a frozen period duration by looking at the frozen entry start time and the next entry start time + */ + function computeSumFrozenDuration(pageStates: PageStateServerEntry[]) { + let sum = 0 + for (let i = 0; i < pageStates.length; i++) { + if (pageStates[i].state === PageState.FROZEN) { + const nextStateStart = pageStates[i + 1] ? pageStates[i + 1].start : toServerDuration(relativeNow()) + sum += nextStateStart - pageStates[i].start + } + } + return sum + } let hasReplay let sampledForReplay diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index e2ed2d9775..be10402a64 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -31,6 +31,7 @@ export interface RumSession { id: string sessionReplay: SessionReplayState anonymousId?: string + expire?: string } export const enum RumTrackingType { @@ -88,6 +89,7 @@ export function startRumSessionManager( ? SessionReplayState.FORCED : SessionReplayState.OFF, anonymousId: session.anonymousId, + expire: session.expire, } }, expire: sessionManager.expire, diff --git a/packages/rum-core/test/mockPageStateHistory.ts b/packages/rum-core/test/mockPageStateHistory.ts index f9c862f47e..c30317e641 100644 --- a/packages/rum-core/test/mockPageStateHistory.ts +++ b/packages/rum-core/test/mockPageStateHistory.ts @@ -5,6 +5,7 @@ export function mockPageStateHistory(partialPageStateHistory?: Partial [], wasInPageStateDuringPeriod: () => false, ...partialPageStateHistory, }