From 09c516af25428174559142e9cbfedb07d50214f0 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Tue, 4 Nov 2025 16:47:46 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=8A=20Add=20debug=20log=20on=20eve?= =?UTF-8?q?nts=20sent=20after=20session=20expiration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/session/sessionManager.ts | 3 ++ packages/rum-core/src/boot/startRum.spec.ts | 2 +- packages/rum-core/src/boot/startRum.ts | 2 +- packages/rum-core/src/domain/assembly.spec.ts | 3 +- .../src/domain/contexts/pageStateHistory.ts | 10 +++++-- .../domain/contexts/sessionContext.spec.ts | 10 +++++-- .../src/domain/contexts/sessionContext.ts | 30 +++++++++++++++++-- .../rum-core/src/domain/rumSessionManager.ts | 2 ++ .../rum-core/test/mockPageStateHistory.ts | 1 + 9 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 767dcb0521..b8513b94c0 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 @@ -115,6 +116,7 @@ export function startSessionManager( trackingType: SESSION_NOT_TRACKED as TrackingType, isReplayForced: false, anonymousId: undefined, + expire: undefined, } } @@ -123,6 +125,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..66fabea2d8 100644 --- a/packages/rum-core/src/domain/contexts/sessionContext.ts +++ b/packages/rum-core/src/domain/contexts/sessionContext.ts @@ -1,16 +1,28 @@ -import { DISCARDED, HookNames, SKIPPED } from '@datadog/browser-core' -import { SessionReplayState, SessionType } from '../rumSessionManager' +import type { ContextValue } from '@datadog/browser-core' +import { + DISCARDED, + HookNames, + SKIPPED, + dateNow, + ONE_MINUTE, + addTelemetryDebug, + elapsed, + relativeNow, +} from '@datadog/browser-core' import type { RumSessionManager } from '../rumSessionManager' +import { SessionReplayState, SessionType } from '../rumSessionManager' 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' 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 +31,18 @@ export function startSessionContext( if (!session || !view) { return DISCARDED } + if (session.expire && dateNow() - Number(session.expire) > ONE_MINUTE) { + const duration = elapsed(startTime, relativeNow()) + // monitor-until: 2026-01-01 + addTelemetryDebug('Event sent after session expiration', { + debug: { + duration, + eventType, + expired_since: dateNow() - Number(session.expire), + page_state: pageStateHistory.findPageStatesForPeriod(startTime, duration) as ContextValue, + }, + }) + } 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, } From 2413b851c15a471c1688ad0b41b37d03235ef767 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Wed, 5 Nov 2025 18:49:41 +0100 Subject: [PATCH 2/3] tweak telemetry --- .../core/src/domain/session/sessionManager.ts | 9 +++++++ .../src/domain/contexts/sessionContext.ts | 25 ++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index b8513b94c0..3845334c8c 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -89,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() diff --git a/packages/rum-core/src/domain/contexts/sessionContext.ts b/packages/rum-core/src/domain/contexts/sessionContext.ts index 66fabea2d8..00b7f22c02 100644 --- a/packages/rum-core/src/domain/contexts/sessionContext.ts +++ b/packages/rum-core/src/domain/contexts/sessionContext.ts @@ -8,6 +8,7 @@ import { addTelemetryDebug, elapsed, relativeNow, + relativeToClocks, } from '@datadog/browser-core' import type { RumSessionManager } from '../rumSessionManager' import { SessionReplayState, SessionType } from '../rumSessionManager' @@ -16,6 +17,7 @@ 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, @@ -31,15 +33,26 @@ export function startSessionContext( if (!session || !view) { return DISCARDED } - if (session.expire && dateNow() - Number(session.expire) > ONE_MINUTE) { - const duration = elapsed(startTime, relativeNow()) + // 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)) { // monitor-until: 2026-01-01 - addTelemetryDebug('Event sent after session expiration', { + addTelemetryDebug('Event sent after session expiration or frozen page state', { debug: { - duration, + eventDuration, eventType, - expired_since: dateNow() - Number(session.expire), - page_state: pageStateHistory.findPageStatesForPeriod(startTime, duration) as ContextValue, + isSessionExpired, + sessionExpiredSince: isSessionExpired ? dateNow() - Number(session.expire) : undefined, + elapsedBetweenStartAndExpire: isSessionExpired + ? Number(session.expire) - relativeToClocks(startTime).timeStamp + : undefined, + wasInFrozenState, + pageStateDuringEventDuration: pageStateHistory.findPageStatesForPeriod( + startTime, + eventDuration + ) as ContextValue, }, }) } From 4718603c794a6ded3bcfc281dedac827902b6bb2 Mon Sep 17 00:00:00 2001 From: Bastien Caudan Date: Thu, 6 Nov 2025 15:32:28 +0100 Subject: [PATCH 3/3] report frozen period duration --- .../src/domain/contexts/sessionContext.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/rum-core/src/domain/contexts/sessionContext.ts b/packages/rum-core/src/domain/contexts/sessionContext.ts index 00b7f22c02..47935aea34 100644 --- a/packages/rum-core/src/domain/contexts/sessionContext.ts +++ b/packages/rum-core/src/domain/contexts/sessionContext.ts @@ -1,5 +1,6 @@ import type { ContextValue } from '@datadog/browser-core' import { + toServerDuration, DISCARDED, HookNames, SKIPPED, @@ -12,6 +13,7 @@ import { } 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' @@ -38,6 +40,7 @@ export function startSessionContext( 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: { @@ -49,14 +52,26 @@ export function startSessionContext( ? Number(session.expire) - relativeToClocks(startTime).timeStamp : undefined, wasInFrozenState, - pageStateDuringEventDuration: pageStateHistory.findPageStatesForPeriod( - startTime, - eventDuration - ) as ContextValue, + 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 let isActive