From 43a563eab13bf26c8d4ed681c1e2d57f7a1fa285 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Fri, 29 Aug 2025 18:31:28 +0200 Subject: [PATCH 01/41] async Logs session manager initialization --- packages/logs/src/boot/logsPublicApi.spec.ts | 8 ++- packages/logs/src/boot/logsPublicApi.ts | 3 +- packages/logs/src/boot/preStartLogs.spec.ts | 56 ++++++++++++++++- packages/logs/src/boot/preStartLogs.ts | 29 +++++++-- packages/logs/src/boot/startLogs.spec.ts | 60 +++---------------- packages/logs/src/boot/startLogs.ts | 16 ++--- .../src/domain/logsSessionManager.spec.ts | 60 ++++++++++--------- .../logs/src/domain/logsSessionManager.ts | 9 +-- packages/logs/test/mockLogsSessionManager.ts | 2 +- 9 files changed, 136 insertions(+), 107 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 27c9e3844c..7270bbb33a 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,5 +1,5 @@ import type { ContextManager, TimeStamp } from '@datadog/browser-core' -import { monitor, display, createContextManager } from '@datadog/browser-core' +import { monitor, display, createContextManager, stopSessionManager } from '@datadog/browser-core' import type { Logger, LogsMessage } from '../domain/logger' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' @@ -34,6 +34,10 @@ describe('logs entry', () => { startLogs = jasmine.createSpy().and.callFake(() => ({ handleLog: handleLogSpy, getInternalContext })) }) + afterEach(() => { + stopSessionManager() + }) + it('should add a `_setDebug` that works', () => { const displaySpy = spyOn(display, 'error') const LOGS = makeLogsPublicApi(startLogs) @@ -76,7 +80,7 @@ describe('logs entry', () => { it('should have the current date, view and global context', () => { LOGS.setGlobalContextProperty('foo', 'bar') - const getCommonContext = startLogs.calls.mostRecent().args[1] + const getCommonContext = startLogs.calls.mostRecent().args[2] expect(getCommonContext()).toEqual({ view: { referrer: document.referrer, diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index c96b30f66e..505a836968 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -272,9 +272,10 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { let strategy = createPreStartStrategy( buildCommonContext, trackingConsentState, - (initConfiguration, configuration) => { + (initConfiguration, configuration, logsSessionManager) => { const startLogsResult = startLogsImpl( configuration, + logsSessionManager, buildCommonContext, trackingConsentState, bufferedDataObservable diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 4186f64d0a..897981a7fe 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -1,16 +1,26 @@ -import { callbackAddsInstrumentation, type Clock, mockClock, mockEventBridge } from '@datadog/browser-core/test' +import { + callbackAddsInstrumentation, + type Clock, + mockClock, + mockEventBridge, + mockSyntheticsWorkerValues, +} from '@datadog/browser-core/test' import type { TimeStamp, TrackingConsentState } from '@datadog/browser-core' import { ONE_SECOND, + SESSION_STORE_KEY, TrackingConsent, createTrackingConsentState, display, + getCookie, resetFetchObservable, + stopSessionManager, } from '@datadog/browser-core' import type { CommonContext } from '../rawLogsEvent.types' import type { HybridInitConfiguration, LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' import type { Logger } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' +import type { LogsSessionManager } from '../domain/logsSessionManager' import type { Strategy } from './logsPublicApi' import { createPreStartStrategy } from './preStartLogs' import type { StartLogsResult } from './startLogs' @@ -20,7 +30,11 @@ const INVALID_INIT_CONFIGURATION = {} as LogsInitConfiguration describe('preStartLogs', () => { let doStartLogsSpy: jasmine.Spy< - (initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration) => StartLogsResult + ( + initConfiguration: LogsInitConfiguration, + configuration: LogsConfiguration, + sessionManager: LogsSessionManager + ) => StartLogsResult > let handleLogSpy: jasmine.Spy let getCommonContextSpy: jasmine.Spy<() => CommonContext> @@ -44,6 +58,7 @@ describe('preStartLogs', () => { afterEach(() => { resetFetchObservable() + stopSessionManager() }) describe('configuration validation', () => { @@ -248,4 +263,41 @@ describe('preStartLogs', () => { expect(doStartLogsSpy).not.toHaveBeenCalled() }) }) + + describe('sampling', () => { + it('should be applied when event bridge is present (rate 0)', () => { + mockEventBridge() + + strategy.init({ ...DEFAULT_INIT_CONFIGURATION, sessionSampleRate: 0 }) + const sessionManager = doStartLogsSpy.calls.mostRecent().args[2] + expect(sessionManager.findTrackedSession()).toBeUndefined() + }) + + it('should be applied when event bridge is present (rate 100)', () => { + mockEventBridge() + + strategy.init({ ...DEFAULT_INIT_CONFIGURATION, sessionSampleRate: 100 }) + const sessionManager = doStartLogsSpy.calls.mostRecent().args[2] + expect(sessionManager.findTrackedSession()).toBeTruthy() + }) + }) + + describe('logs session creation', () => { + it('creates a session on normal conditions', () => { + strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(getCookie(SESSION_STORE_KEY)).toBeDefined() + }) + + it('does not create a session if event bridge is present', () => { + mockEventBridge() + strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() + }) + + it('does not create a session if synthetics worker will inject RUM', () => { + mockSyntheticsWorkerValues({ injectsRum: true }) + strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() + }) + }) }) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 257811bab0..db5bdb720f 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -14,17 +14,24 @@ import { addTelemetryConfiguration, buildGlobalContextManager, buildUserContextManager, + willSyntheticsInjectRum, } from '@datadog/browser-core' import type { LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' import { serializeLogsConfiguration, validateAndBuildLogsConfiguration } from '../domain/configuration' import type { CommonContext } from '../rawLogsEvent.types' +import type { LogsSessionManager } from '../domain/logsSessionManager' +import { startLogsSessionManagerStub, startLogsSessionManager } from '../domain/logsSessionManager' import type { Strategy } from './logsPublicApi' import type { StartLogsResult } from './startLogs' export function createPreStartStrategy( getCommonContext: () => CommonContext, trackingConsentState: TrackingConsentState, - doStartLogs: (initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration) => StartLogsResult + doStartLogs: ( + initConfiguration: LogsInitConfiguration, + configuration: LogsConfiguration, + sessionManager: LogsSessionManager + ) => StartLogsResult ): Strategy { const bufferApiCalls = createBoundedBuffer() @@ -40,15 +47,14 @@ export function createPreStartStrategy( let cachedInitConfiguration: LogsInitConfiguration | undefined let cachedConfiguration: LogsConfiguration | undefined - const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartLogs) + let sessionManager: LogsSessionManager | undefined function tryStartLogs() { - if (!cachedConfiguration || !cachedInitConfiguration || !trackingConsentState.isGranted()) { + if (!cachedConfiguration || !cachedInitConfiguration || !sessionManager) { return } - trackingConsentStateSubscription.unsubscribe() - const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration) + const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, sessionManager) bufferApiCalls.drain(startLogsResult) } @@ -88,7 +94,18 @@ export function createPreStartStrategy( initFetchObservable().subscribe(noop) trackingConsentState.tryToInit(configuration.trackingConsent) - tryStartLogs() + + trackingConsentState.onGrantedOnce(() => { + if (configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum()) { + startLogsSessionManager(configuration, trackingConsentState, (newSessionManager) => { + sessionManager = newSessionManager + tryStartLogs() + }) + } else { + sessionManager = startLogsSessionManagerStub(configuration) + tryStartLogs() + } + }) }, get initConfiguration() { diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 82616e3615..7d5d63bbbc 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -2,14 +2,11 @@ import type { BufferedData, Payload } from '@datadog/browser-core' import { ErrorSource, display, - stopSessionManager, getCookie, SESSION_STORE_KEY, createTrackingConsentState, TrackingConsent, - setCookie, STORAGE_POLL_DELAY, - ONE_MINUTE, BufferedObservable, FLUSH_DURATION_LIMIT, } from '@datadog/browser-core' @@ -21,7 +18,6 @@ import { mockSyntheticsWorkerValues, registerCleanupTask, mockClock, - expireCookie, DEFAULT_FETCH_MOCK, } from '@datadog/browser-core/test' @@ -30,6 +26,7 @@ import { validateAndBuildLogsConfiguration } from '../domain/configuration' import { Logger } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' import type { LogsEvent } from '../logsEvent.types' +import { createLogsSessionManagerMock } from '../../test/mockLogsSessionManager' import { startLogs } from './startLogs' function getLoggedMessage(requests: Request[], index: number) { @@ -57,12 +54,14 @@ function startLogsWithDefaults( trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) ) { const endpointBuilder = mockEndpointBuilder('https://localhost/v1/input/log') + const sessionManager = createLogsSessionManagerMock() const { handleLog, stop, globalContext, accountContext, userContext } = startLogs( { ...validateAndBuildLogsConfiguration({ clientToken: 'xxx', service: 'service', telemetrySampleRate: 0 })!, logsEndpointBuilder: endpointBuilder, ...configuration, }, + sessionManager, () => COMMON_CONTEXT, trackingConsentState, new BufferedObservable(100) @@ -72,7 +71,7 @@ function startLogsWithDefaults( const logger = new Logger(handleLog) - return { handleLog, logger, endpointBuilder, globalContext, accountContext, userContext } + return { handleLog, logger, endpointBuilder, globalContext, accountContext, userContext, sessionManager } } describe('logs', () => { @@ -88,7 +87,6 @@ describe('logs', () => { afterEach(() => { delete window.DD_RUM - stopSessionManager() }) describe('request', () => { @@ -160,30 +158,6 @@ describe('logs', () => { }) }) - describe('sampling', () => { - it('should be applied when event bridge is present (rate 0)', () => { - const sendSpy = spyOn(mockEventBridge(), 'send') - - const { handleLog, logger } = startLogsWithDefaults({ - configuration: { sessionSampleRate: 0 }, - }) - handleLog(DEFAULT_MESSAGE, logger) - - expect(sendSpy).not.toHaveBeenCalled() - }) - - it('should be applied when event bridge is present (rate 100)', () => { - const sendSpy = spyOn(mockEventBridge(), 'send') - - const { handleLog, logger } = startLogsWithDefaults({ - configuration: { sessionSampleRate: 100 }, - }) - handleLog(DEFAULT_MESSAGE, logger) - - expect(sendSpy).toHaveBeenCalled() - }) - }) - it('should not print the log twice when console handler is enabled', () => { const consoleLogSpy = spyOn(console, 'log') const displayLogSpy = spyOn(display, 'log') @@ -198,35 +172,15 @@ describe('logs', () => { expect(displayLogSpy).not.toHaveBeenCalled() }) - describe('logs session creation', () => { - it('creates a session on normal conditions', () => { - startLogsWithDefaults() - expect(getCookie(SESSION_STORE_KEY)).toBeDefined() - }) - - it('does not create a session if event bridge is present', () => { - mockEventBridge() - startLogsWithDefaults() - expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() - }) - - it('does not create a session if synthetics worker will inject RUM', () => { - mockSyntheticsWorkerValues({ injectsRum: true }) - startLogsWithDefaults() - expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() - }) - }) - describe('session lifecycle', () => { it('sends logs without session id when the session expires ', async () => { - setCookie(SESSION_STORE_KEY, 'id=foo&logs=1', ONE_MINUTE) - const { handleLog, logger } = startLogsWithDefaults() + const { handleLog, logger, sessionManager } = startLogsWithDefaults() interceptor.withFetch(DEFAULT_FETCH_MOCK, DEFAULT_FETCH_MOCK) handleLog({ status: StatusType.info, message: 'message 1' }, logger) - expireCookie() + sessionManager.expire() clock.tick(STORAGE_POLL_DELAY * 2) handleLog({ status: StatusType.info, message: 'message 2' }, logger) @@ -239,7 +193,7 @@ describe('logs', () => { expect(requests.length).toEqual(2) expect(firstRequest.message).toEqual('message 1') - expect(firstRequest.session_id).toEqual('foo') + expect(firstRequest.session_id).toEqual('session-id') expect(secondRequest.message).toEqual('message 2') expect(secondRequest.session_id).toBeUndefined() diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 929de4096f..d883cc37af 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -13,7 +13,7 @@ import { startUserContext, isWorkerEnvironment, } from '@datadog/browser-core' -import { startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' +import { LogsSessionManager, startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' import type { LogsConfiguration } from '../domain/configuration' import { startLogsAssembly } from '../domain/assembly' import { startConsoleCollection } from '../domain/console/consoleCollection' @@ -39,6 +39,7 @@ export type StartLogsResult = ReturnType export function startLogs( configuration: LogsConfiguration, + sessionManager: LogsSessionManager, getCommonContext: () => CommonContext, // `startLogs` and its subcomponents assume tracking consent is granted initially and starts @@ -69,16 +70,11 @@ export function startLogs( ) cleanupTasks.push(telemetry.stop) - const session = - configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum() - ? startLogsSessionManager(configuration, trackingConsentState) - : startLogsSessionManagerStub(configuration) - startTrackingConsentContext(hooks, trackingConsentState) // Start user and account context first to allow overrides from global context - startSessionContext(hooks, configuration, session) + startSessionContext(hooks, configuration, sessionManager) const accountContext = startAccountContext(hooks, configuration, LOGS_STORAGE_KEY) - const userContext = startUserContext(hooks, configuration, session, LOGS_STORAGE_KEY) + const userContext = startUserContext(hooks, configuration, sessionManager, LOGS_STORAGE_KEY) const globalContext = startGlobalContext(hooks, configuration, LOGS_STORAGE_KEY, false) startRUMInternalContext(hooks) @@ -97,14 +93,14 @@ export function startLogs( lifeCycle, reportError, pageMayExitObservable, - session + sessionManager ) cleanupTasks.push(() => stopLogsBatch()) } else { startLogsBridge(lifeCycle) } - const internalContext = startInternalContext(session) + const internalContext = startInternalContext(sessionManager) return { handleLog, diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index b0b03a76f0..9230e851b5 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -18,6 +18,7 @@ import type { LogsConfiguration } from './configuration' import { LOGS_SESSION_KEY, LoggerTrackingType, + LogsSessionManager, startLogsSessionManager, startLogsSessionManagerStub, } from './logsSessionManager' @@ -37,39 +38,39 @@ describe('logs session manager', () => { clock.tick(new Date().getTime()) }) - it('when tracked should store tracking type and session id', () => { - startLogsSessionManagerWithDefaults() + it('when tracked should store tracking type and session id', async () => { + await startLogsSessionManagerWithDefaults() expect(getSessionState(SESSION_STORE_KEY).id).toMatch(/[a-f0-9-]+/) expect(getSessionState(SESSION_STORE_KEY)[LOGS_SESSION_KEY]).toBe(LoggerTrackingType.TRACKED) }) - it('when not tracked should store tracking type', () => { - startLogsSessionManagerWithDefaults({ configuration: { sessionSampleRate: 0 } }) + it('when not tracked should store tracking type', async () => { + await startLogsSessionManagerWithDefaults({ configuration: { sessionSampleRate: 0 } }) expect(getSessionState(SESSION_STORE_KEY)[LOGS_SESSION_KEY]).toBe(LoggerTrackingType.NOT_TRACKED) expect(getSessionState(SESSION_STORE_KEY).isExpired).toBeUndefined() }) - it('when tracked should keep existing tracking type and session id', () => { + it('when tracked should keep existing tracking type and session id', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) - startLogsSessionManagerWithDefaults() + await startLogsSessionManagerWithDefaults() expect(getSessionState(SESSION_STORE_KEY).id).toBe('abcdef') expect(getSessionState(SESSION_STORE_KEY)[LOGS_SESSION_KEY]).toBe(LoggerTrackingType.TRACKED) }) - it('when not tracked should keep existing tracking type', () => { + it('when not tracked should keep existing tracking type', async () => { setCookie(SESSION_STORE_KEY, 'logs=0', DURATION) - startLogsSessionManagerWithDefaults() + await startLogsSessionManagerWithDefaults() expect(getSessionState(SESSION_STORE_KEY)[LOGS_SESSION_KEY]).toBe(LoggerTrackingType.NOT_TRACKED) }) - it('should renew on activity after expiration', () => { - startLogsSessionManagerWithDefaults() + it('should renew on activity after expiration', async () => { + await startLogsSessionManagerWithDefaults() expireCookie() expect(getSessionState(SESSION_STORE_KEY).isExpired).toBe('1') @@ -82,37 +83,37 @@ describe('logs session manager', () => { }) describe('findTrackedSession', () => { - it('should return the current active session', () => { + it('should return the current active session', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) - const logsSessionManager = startLogsSessionManagerWithDefaults() + const logsSessionManager = await startLogsSessionManagerWithDefaults() expect(logsSessionManager.findTrackedSession()!.id).toBe('abcdef') }) - it('should return undefined if the session is not tracked', () => { + it('should return undefined if the session is not tracked', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=0', DURATION) - const logsSessionManager = startLogsSessionManagerWithDefaults() + const logsSessionManager = await startLogsSessionManagerWithDefaults() expect(logsSessionManager.findTrackedSession()).toBeUndefined() }) - it('should not return the current session if it has expired by default', () => { + it('should not return the current session if it has expired by default', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) - const logsSessionManager = startLogsSessionManagerWithDefaults() + const logsSessionManager = await startLogsSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) expireCookie() clock.tick(STORAGE_POLL_DELAY) expect(logsSessionManager.findTrackedSession()).toBeUndefined() }) - it('should return the current session if it has expired when returnExpired = true', () => { - const logsSessionManager = startLogsSessionManagerWithDefaults() + it('should return the current session if it has expired when returnExpired = true', async () => { + const logsSessionManager = await startLogsSessionManagerWithDefaults() expireCookie() clock.tick(STORAGE_POLL_DELAY) expect(logsSessionManager.findTrackedSession(relativeNow(), { returnInactive: true })).toBeDefined() }) - it('should return session corresponding to start time', () => { + it('should return session corresponding to start time', async () => { setCookie(SESSION_STORE_KEY, 'id=foo&logs=1', DURATION) - const logsSessionManager = startLogsSessionManagerWithDefaults() + const logsSessionManager = await startLogsSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) setCookie(SESSION_STORE_KEY, 'id=bar&logs=1', DURATION) // simulate a click to renew the session @@ -124,14 +125,17 @@ describe('logs session manager', () => { }) function startLogsSessionManagerWithDefaults({ configuration }: { configuration?: Partial } = {}) { - return startLogsSessionManager( - { - sessionSampleRate: 100, - sessionStoreStrategyType: { type: SessionPersistence.COOKIE, cookieOptions: {} }, - ...configuration, - } as LogsConfiguration, - createTrackingConsentState(TrackingConsent.GRANTED) - ) + return new Promise((resolve) => { + startLogsSessionManager( + { + sessionSampleRate: 100, + sessionStoreStrategyType: { type: SessionPersistence.COOKIE, cookieOptions: {} }, + ...configuration, + } as LogsConfiguration, + createTrackingConsentState(TrackingConsent.GRANTED), + resolve + ) + }) } }) diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index 778e4d299b..d3eeedff0a 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -21,15 +21,16 @@ export const enum LoggerTrackingType { export function startLogsSessionManager( configuration: LogsConfiguration, - trackingConsentState: TrackingConsentState -): LogsSessionManager { + trackingConsentState: TrackingConsentState, + onReady: (sessionManager: LogsSessionManager) => void +) { const sessionManager = startSessionManager( configuration, LOGS_SESSION_KEY, (rawTrackingType) => computeTrackingType(configuration, rawTrackingType), trackingConsentState ) - return { + onReady({ findTrackedSession: (startTime?: RelativeTime, options = { returnInactive: false }) => { const session = sessionManager.findSession(startTime, options) return session && session.trackingType === LoggerTrackingType.TRACKED @@ -40,7 +41,7 @@ export function startLogsSessionManager( : undefined }, expireObservable: sessionManager.expireObservable, - } + }) } export function startLogsSessionManagerStub(configuration: LogsConfiguration): LogsSessionManager { diff --git a/packages/logs/test/mockLogsSessionManager.ts b/packages/logs/test/mockLogsSessionManager.ts index cdace1a5d8..66d467e4ba 100644 --- a/packages/logs/test/mockLogsSessionManager.ts +++ b/packages/logs/test/mockLogsSessionManager.ts @@ -20,7 +20,7 @@ export function createLogsSessionManagerMock(): LogsSessionManagerMock { }, findTrackedSession: (_startTime, options) => { if (sessionStatus === LoggerTrackingType.TRACKED && (sessionIsActive || options?.returnInactive)) { - return { id } + return { id, anonymousId: 'device-123' } } }, expireObservable: new Observable(), From 400b5bd0532593bc4b3cb9b0f92a59f726c6e7c6 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Fri, 29 Aug 2025 15:55:43 +0200 Subject: [PATCH 02/41] async RUM session manager initialization --- .../core/src/domain/trackingConsent.spec.ts | 18 ++++ packages/core/src/domain/trackingConsent.ts | 21 +++- .../rum-core/src/boot/preStartRum.spec.ts | 14 ++- packages/rum-core/src/boot/preStartRum.ts | 25 +++-- .../rum-core/src/boot/rumPublicApi.spec.ts | 10 +- packages/rum-core/src/boot/rumPublicApi.ts | 7 +- packages/rum-core/src/boot/startRum.spec.ts | 3 +- packages/rum-core/src/boot/startRum.ts | 30 +++--- .../src/domain/rumSessionManager.spec.ts | 96 ++++++++++--------- .../rum-core/src/domain/rumSessionManager.ts | 24 ++--- .../rum-core/test/mockRumSessionManager.ts | 1 + 11 files changed, 152 insertions(+), 97 deletions(-) diff --git a/packages/core/src/domain/trackingConsent.spec.ts b/packages/core/src/domain/trackingConsent.spec.ts index 35ae358576..35381e6d56 100644 --- a/packages/core/src/domain/trackingConsent.spec.ts +++ b/packages/core/src/domain/trackingConsent.spec.ts @@ -41,4 +41,22 @@ describe('createTrackingConsentState', () => { trackingConsentState.tryToInit(TrackingConsent.NOT_GRANTED) expect(trackingConsentState.isGranted()).toBeTrue() }) + + describe('onGrantedOnce', () => { + it('calls onGrantedOnce when consent was already granted', () => { + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + const spy = jasmine.createSpy() + trackingConsentState.onGrantedOnce(spy) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('calls onGrantedOnce when consent is granted', () => { + const trackingConsentState = createTrackingConsentState() + const spy = jasmine.createSpy() + trackingConsentState.onGrantedOnce(spy) + expect(spy).toHaveBeenCalledTimes(0) + trackingConsentState.update(TrackingConsent.GRANTED) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) }) diff --git a/packages/core/src/domain/trackingConsent.ts b/packages/core/src/domain/trackingConsent.ts index 02e8150eee..4d7d02fe03 100644 --- a/packages/core/src/domain/trackingConsent.ts +++ b/packages/core/src/domain/trackingConsent.ts @@ -11,24 +11,39 @@ export interface TrackingConsentState { update: (trackingConsent: TrackingConsent) => void isGranted: () => boolean observable: Observable + onGrantedOnce: (callback: () => void) => void } export function createTrackingConsentState(currentConsent?: TrackingConsent): TrackingConsentState { const observable = new Observable() + function isGranted() { + return currentConsent === TrackingConsent.GRANTED + } + return { tryToInit(trackingConsent: TrackingConsent) { if (!currentConsent) { currentConsent = trackingConsent } }, + onGrantedOnce(fn) { + if (isGranted()) { + fn() + } else { + const subscription = observable.subscribe(() => { + if (isGranted()) { + fn() + subscription.unsubscribe() + } + }) + } + }, update(trackingConsent: TrackingConsent) { currentConsent = trackingConsent observable.notify() }, - isGranted() { - return currentConsent === TrackingConsent.GRANTED - }, + isGranted, observable, } } diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index 7b922265a5..3384d5d1ea 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -10,6 +10,7 @@ import { DefaultPrivacyLevel, resetExperimentalFeatures, resetFetchObservable, + stopSessionManager, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { @@ -25,6 +26,7 @@ import { ActionType, VitalType } from '../rawRumEvent.types' import type { CustomAction } from '../domain/action/actionCollection' import type { RumPlugin } from '../domain/plugins' import { createCustomVitalsState } from '../domain/vital/vitalCollection' +import type { RumSessionManager } from '../domain/rumSessionManager' import type { RumPublicApi, Strategy } from './rumPublicApi' import type { StartRumResult } from './startRum' import { createPreStartStrategy } from './preStartRum' @@ -40,6 +42,7 @@ describe('preStartRum', () => { let doStartRumSpy: jasmine.Spy< ( configuration: RumConfiguration, + sessionManager: RumSessionManager, deflateWorker: DeflateWorker | undefined, initialViewOptions?: ViewOptions ) => StartRumResult @@ -51,6 +54,7 @@ describe('preStartRum', () => { afterEach(() => { resetFetchObservable() + stopSessionManager() }) describe('configuration validation', () => { @@ -247,7 +251,7 @@ describe('preStartRum', () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) expect(startDeflateWorkerSpy).not.toHaveBeenCalled() - const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[1] + const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] expect(worker).toBeUndefined() }) }) @@ -263,7 +267,7 @@ describe('preStartRum', () => { ) expect(startDeflateWorkerSpy).toHaveBeenCalledTimes(1) - const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[1] + const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] expect(worker).toBeDefined() }) @@ -358,7 +362,7 @@ describe('preStartRum', () => { strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).toHaveBeenCalled() - const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[2] + const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) expect(startViewSpy).not.toHaveBeenCalled() }) @@ -393,7 +397,7 @@ describe('preStartRum', () => { strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).toHaveBeenCalled() - const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[2] + const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) expect(startViewSpy).toHaveBeenCalledOnceWith({ name: 'bar' }, relativeToClocks(clock.relative(20))) }) @@ -405,7 +409,7 @@ describe('preStartRum', () => { strategy.startView({ name: 'foo' }) expect(doStartRumSpy).toHaveBeenCalled() - const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[2] + const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) expect(startViewSpy).not.toHaveBeenCalled() }) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 0ea06776e4..6717a2d6a2 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -33,6 +33,8 @@ import type { FailureReason, } from '../domain/vital/vitalCollection' import { startDurationVital, stopDurationVital } from '../domain/vital/vitalCollection' +import type { RumSessionManager } from '../domain/rumSessionManager' +import { startRumSessionManager, startRumSessionManagerStub } from '../domain/rumSessionManager' import { callPluginsMethod } from '../domain/plugins' import type { StartRumResult } from './startRum' import type { RumPublicApiOptions, Strategy } from './rumPublicApi' @@ -43,6 +45,7 @@ export function createPreStartStrategy( customVitalsState: CustomVitalsState, doStartRum: ( configuration: RumConfiguration, + sessionManager: RumSessionManager, deflateWorker: DeflateWorker | undefined, initialViewOptions?: ViewOptions ) => StartRumResult @@ -66,18 +69,15 @@ export function createPreStartStrategy( let cachedInitConfiguration: RumInitConfiguration | undefined let cachedConfiguration: RumConfiguration | undefined - - const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartRum) + let sessionManager: RumSessionManager | undefined const emptyContext: Context = {} function tryStartRum() { - if (!cachedInitConfiguration || !cachedConfiguration || !trackingConsentState.isGranted()) { + if (!cachedInitConfiguration || !cachedConfiguration || !sessionManager) { return } - trackingConsentStateSubscription.unsubscribe() - let initialViewOptions: ViewOptions | undefined if (cachedConfiguration.trackViewsManually) { @@ -94,7 +94,7 @@ export function createPreStartStrategy( initialViewOptions = firstStartViewCall.options } - const startRumResult = doStartRum(cachedConfiguration, deflateWorker, initialViewOptions) + const startRumResult = doStartRum(cachedConfiguration, sessionManager, deflateWorker, initialViewOptions) bufferApiCalls.drain(startRumResult) } @@ -147,7 +147,18 @@ export function createPreStartStrategy( initFetchObservable().subscribe(noop) trackingConsentState.tryToInit(configuration.trackingConsent) - tryStartRum() + + trackingConsentState.onGrantedOnce(() => { + if (canUseEventBridge()) { + sessionManager = startRumSessionManagerStub() + tryStartRum() + } else { + startRumSessionManager(configuration, trackingConsentState, (newSessionManager) => { + sessionManager = newSessionManager + tryStartRum() + }) + } + }) } const addDurationVital = (vital: DurationVital) => { diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index cb1eedf30f..d44b1c34f9 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1,5 +1,5 @@ import type { RelativeTime, DeflateWorker, TimeStamp } from '@datadog/browser-core' -import { ONE_SECOND, display, DefaultPrivacyLevel, timeStampToClocks } from '@datadog/browser-core' +import { ONE_SECOND, display, DefaultPrivacyLevel, timeStampToClocks, stopSessionManager } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock } from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' @@ -24,7 +24,7 @@ const noopStartRum = (): ReturnType => ({ lifeCycle: {} as any, viewHistory: {} as any, longTaskContexts: {} as any, - session: {} as any, + sessionManager: {} as any, stopSession: () => undefined, startDurationVital: () => ({}) as DurationVitalReference, stopDurationVital: () => undefined, @@ -41,6 +41,10 @@ const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' } const FAKE_WORKER = {} as DeflateWorker describe('rum public api', () => { + afterEach(() => { + stopSessionManager() + }) + describe('init', () => { let startRumSpy: jasmine.Spy @@ -945,7 +949,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - const sdkName = startRumSpy.calls.argsFor(0)[8] + const sdkName = startRumSpy.calls.argsFor(0)[9] expect(sdkName).toBe('rum-slim') }) }) diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 0bd96bb88b..1a355c7638 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -545,7 +545,7 @@ export function makeRumPublicApi( options, trackingConsentState, customVitalsState, - (configuration, deflateWorker, initialViewOptions) => { + (configuration, sessionManager, deflateWorker, initialViewOptions) => { const createEncoder = deflateWorker && options.createDeflateEncoder ? (streamId: DeflateEncoderStreamId) => options.createDeflateEncoder!(configuration, deflateWorker, streamId) @@ -553,6 +553,7 @@ export function makeRumPublicApi( const startRumResult = startRumImpl( configuration, + sessionManager, recorderApi, profilerApi, initialViewOptions, @@ -566,7 +567,7 @@ export function makeRumPublicApi( recorderApi.onRumStart( startRumResult.lifeCycle, configuration, - startRumResult.session, + sessionManager, startRumResult.viewHistory, deflateWorker, startRumResult.telemetry @@ -576,7 +577,7 @@ export function makeRumPublicApi( startRumResult.lifeCycle, startRumResult.hooks, configuration, - startRumResult.session, + sessionManager, startRumResult.viewHistory, startRumResult.longTaskContexts, createEncoder diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index da2fabae33..f96e3daf65 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -1,7 +1,6 @@ import type { RawError, Duration, BufferedData } from '@datadog/browser-core' import { Observable, - stopSessionManager, toServerDuration, ONE_SECOND, findLast, @@ -162,6 +161,7 @@ describe('view events', () => { function setupViewCollectionTest() { const startResult = startRum( mockRumConfiguration(), + createRumSessionManagerMock(), noopRecorderApi, noopProfilerApi, undefined, @@ -181,7 +181,6 @@ describe('view events', () => { registerCleanupTask(() => { stop() - stopSessionManager() }) }) diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 1e7eff1b9a..9885a7e49b 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -28,8 +28,6 @@ import { startActionCollection } from '../domain/action/actionCollection' import { startErrorCollection } from '../domain/error/errorCollection' import { startResourceCollection } from '../domain/resource/resourceCollection' import { startViewCollection } from '../domain/view/viewCollection' -import type { RumSessionManager } from '../domain/rumSessionManager' -import { startRumSessionManager, startRumSessionManagerStub } from '../domain/rumSessionManager' import { startRumBatch } from '../transport/startRumBatch' import { startRumEventBridge } from '../transport/startRumEventBridge' import { startUrlContexts } from '../domain/contexts/urlContexts' @@ -56,6 +54,7 @@ import { createHooks } from '../domain/hooks' import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext' +import type { RumSessionManager } from '../domain/rumSessionManager' import type { RecorderApi, ProfilerApi } from './rumPublicApi' export type StartRum = typeof startRum @@ -63,6 +62,7 @@ export type StartRumResult = ReturnType export function startRum( configuration: RumConfiguration, + sessionManager: RumSessionManager, recorderApi: RecorderApi, profilerApi: ProfilerApi, initialViewOptions: ViewOptions | undefined, @@ -82,6 +82,8 @@ export function startRum( lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (event) => sendToExtension('rum', event)) + sessionManager.expireObservable.subscribe(() => lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)) + const reportError = (error: RawError) => { lifeCycle.notify(LifeCycleEventType.RAW_ERROR_COLLECTED, { error }) // monitor-until: forever, to keep an eye on the errors reported to customers @@ -104,17 +106,13 @@ export function startRum( ) cleanupTasks.push(telemetry.stop) - const session = !canUseEventBridge() - ? startRumSessionManager(configuration, lifeCycle, trackingConsentState) - : startRumSessionManagerStub() - if (!canUseEventBridge()) { const batch = startRumBatch( configuration, lifeCycle, reportError, pageMayExitObservable, - session.expireObservable, + sessionManager.expireObservable, createEncoder ) cleanupTasks.push(() => batch.stop()) @@ -132,7 +130,7 @@ export function startRum( lifeCycle, hooks, configuration, - session, + sessionManager, recorderApi, initialViewOptions, customVitalsState, @@ -149,8 +147,8 @@ export function startRum( return { ...startRumEventCollectionResult, lifeCycle, - session, - stopSession: () => session.expire(), + sessionManager, + stopSession: () => sessionManager.expire(), telemetry, stop: () => { cleanupTasks.forEach((task) => task()) @@ -163,7 +161,7 @@ export function startRumEventCollection( lifeCycle: LifeCycle, hooks: Hooks, configuration: RumConfiguration, - session: RumSessionManager, + sessionManager: RumSessionManager, recorderApi: RecorderApi, initialViewOptions: ViewOptions | undefined, customVitalsState: CustomVitalsState, @@ -186,10 +184,10 @@ export function startRumEventCollection( const urlContexts = startUrlContexts(lifeCycle, hooks, locationChangeObservable, location) cleanupTasks.push(() => urlContexts.stop()) const featureFlagContexts = startFeatureFlagContexts(lifeCycle, hooks, configuration) - startSessionContext(hooks, session, recorderApi, viewHistory) + startSessionContext(hooks, sessionManager, recorderApi, viewHistory) startConnectivityContext(hooks) const globalContext = startGlobalContext(hooks, configuration, 'rum', true) - const userContext = startUserContext(hooks, configuration, session, 'rum') + const userContext = startUserContext(hooks, configuration, sessionManager, 'rum') const accountContext = startAccountContext(hooks, configuration, 'rum') const actionCollection = startActionCollection( @@ -244,13 +242,13 @@ export function startRumEventCollection( const { addError } = startErrorCollection(lifeCycle, configuration, bufferedDataObservable) - startRequestCollection(lifeCycle, configuration, session, userContext, accountContext) + startRequestCollection(lifeCycle, configuration, sessionManager, userContext, accountContext) const vitalCollection = startVitalCollection(lifeCycle, pageStateHistory, customVitalsState) const internalContext = startInternalContext( configuration.applicationId, - session, + sessionManager, viewHistory, actionCollection.actionContexts, urlContexts @@ -268,6 +266,8 @@ export function startRumEventCollection( getViewContext, setViewName, viewHistory, + sessionManager, + stopSession: () => sessionManager.expire(), getInternalContext: internalContext.get, startDurationVital: vitalCollection.startDurationVital, stopDurationVital: vitalCollection.stopDurationVital, diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index 29f92fc158..bbe0e93771 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -22,7 +22,7 @@ import { import { mockRumConfiguration } from '../../test' import type { RumConfiguration } from './configuration' -import { LifeCycle, LifeCycleEventType } from './lifeCycle' +import type { RumSessionManager } from './rumSessionManager' import { RUM_SESSION_KEY, RumTrackingType, @@ -33,7 +33,6 @@ import { describe('rum session manager', () => { const DURATION = 123456 - let lifeCycle: LifeCycle let expireSessionSpy: jasmine.Spy let renewSessionSpy: jasmine.Spy let clock: Clock @@ -42,9 +41,6 @@ describe('rum session manager', () => { clock = mockClock() expireSessionSpy = jasmine.createSpy('expireSessionSpy') renewSessionSpy = jasmine.createSpy('renewSessionSpy') - lifeCycle = new LifeCycle() - lifeCycle.subscribe(LifeCycleEventType.SESSION_EXPIRED, expireSessionSpy) - lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, renewSessionSpy) registerCleanupTask(() => { // remove intervals first @@ -55,8 +51,10 @@ describe('rum session manager', () => { }) describe('cookie storage', () => { - it('when tracked with session replay should store session type and id', () => { - startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 } }) + it('when tracked with session replay should store session type and id', async () => { + await startRumSessionManagerWithDefaults({ + configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 }, + }) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -65,8 +63,10 @@ describe('rum session manager', () => { expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(RumTrackingType.TRACKED_WITH_SESSION_REPLAY) }) - it('when tracked without session replay should store session type and id', () => { - startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 0 } }) + it('when tracked without session replay should store session type and id', async () => { + await startRumSessionManagerWithDefaults({ + configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 0 }, + }) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -74,8 +74,8 @@ describe('rum session manager', () => { expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(RumTrackingType.TRACKED_WITHOUT_SESSION_REPLAY) }) - it('when not tracked should store session type', () => { - startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 0 } }) + it('when not tracked should store session type', async () => { + await startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 0 } }) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -84,10 +84,10 @@ describe('rum session manager', () => { expect(getSessionState(SESSION_STORE_KEY).isExpired).not.toBeDefined() }) - it('when tracked should keep existing session type and id', () => { + it('when tracked should keep existing session type and id', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - startRumSessionManagerWithDefaults() + await startRumSessionManagerWithDefaults() expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -95,20 +95,22 @@ describe('rum session manager', () => { expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(RumTrackingType.TRACKED_WITH_SESSION_REPLAY) }) - it('when not tracked should keep existing session type', () => { + it('when not tracked should keep existing session type', async () => { setCookie(SESSION_STORE_KEY, 'rum=0', DURATION) - startRumSessionManagerWithDefaults() + await startRumSessionManagerWithDefaults() expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() expect(getSessionState(SESSION_STORE_KEY)[RUM_SESSION_KEY]).toBe(RumTrackingType.NOT_TRACKED) }) - it('should renew on activity after expiration', () => { + it('should renew on activity after expiration', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 } }) + await startRumSessionManagerWithDefaults({ + configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 }, + }) expireCookie() expect(getSessionState(SESSION_STORE_KEY).isExpired).toBe('1') @@ -126,28 +128,28 @@ describe('rum session manager', () => { }) describe('findSession', () => { - it('should return the current session', () => { + it('should return the current session', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - const rumSessionManager = startRumSessionManagerWithDefaults() + const rumSessionManager = await startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()!.id).toBe('abcdef') }) - it('should return undefined if the session is not tracked', () => { + it('should return undefined if the session is not tracked', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=0', DURATION) - const rumSessionManager = startRumSessionManagerWithDefaults() + const rumSessionManager = await startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) - it('should return undefined if the session has expired', () => { - const rumSessionManager = startRumSessionManagerWithDefaults() + it('should return undefined if the session has expired', async () => { + const rumSessionManager = await startRumSessionManagerWithDefaults() expireCookie() clock.tick(STORAGE_POLL_DELAY) expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) - it('should return session corresponding to start time', () => { + it('should return session corresponding to start time', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - const rumSessionManager = startRumSessionManagerWithDefaults() + const rumSessionManager = await startRumSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) expireCookie() clock.tick(STORAGE_POLL_DELAY) @@ -155,21 +157,21 @@ describe('rum session manager', () => { expect(rumSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') }) - it('should return session TRACKED_WITH_SESSION_REPLAY', () => { + it('should return session TRACKED_WITH_SESSION_REPLAY', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) - const rumSessionManager = startRumSessionManagerWithDefaults() + const rumSessionManager = await startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()!.sessionReplay).toBe(SessionReplayState.SAMPLED) }) - it('should return session TRACKED_WITHOUT_SESSION_REPLAY', () => { + it('should return session TRACKED_WITHOUT_SESSION_REPLAY', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=2', DURATION) - const rumSessionManager = startRumSessionManagerWithDefaults() + const rumSessionManager = await startRumSessionManagerWithDefaults() expect(rumSessionManager.findTrackedSession()!.sessionReplay).toBe(SessionReplayState.OFF) }) - it('should update current entity when replay recording is forced', () => { + it('should update current entity when replay recording is forced', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=2', DURATION) - const rumSessionManager = startRumSessionManagerWithDefaults() + const rumSessionManager = await startRumSessionManagerWithDefaults() rumSessionManager.setForcedReplay() expect(rumSessionManager.findTrackedSession()!.sessionReplay).toBe(SessionReplayState.FORCED) @@ -198,8 +200,8 @@ describe('rum session manager', () => { sessionReplaySampleRate: number expectSessionReplay: SessionReplayState }) => { - it(description, () => { - const rumSessionManager = startRumSessionManagerWithDefaults({ + it(description, async () => { + const rumSessionManager = await startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate }, }) expect(rumSessionManager.findTrackedSession()!.sessionReplay).toBe(expectSessionReplay) @@ -209,17 +211,23 @@ describe('rum session manager', () => { }) function startRumSessionManagerWithDefaults({ configuration }: { configuration?: Partial } = {}) { - return startRumSessionManager( - mockRumConfiguration({ - sessionSampleRate: 50, - sessionReplaySampleRate: 50, - trackResources: true, - trackLongTasks: true, - ...configuration, - }), - lifeCycle, - createTrackingConsentState(TrackingConsent.GRANTED) - ) + return new Promise((resolve) => { + startRumSessionManager( + mockRumConfiguration({ + sessionSampleRate: 50, + sessionReplaySampleRate: 50, + trackResources: true, + trackLongTasks: true, + ...configuration, + }), + createTrackingConsentState(TrackingConsent.GRANTED), + (sessionManager) => { + sessionManager.expireObservable.subscribe(expireSessionSpy) + sessionManager.renewObservable.subscribe(renewSessionSpy) + resolve(sessionManager) + } + ) + }) } }) diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index e2ed2d9775..3e8b06e733 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -9,8 +9,6 @@ import { startSessionManager, } from '@datadog/browser-core' import type { RumConfiguration } from './configuration' -import type { LifeCycle } from './lifeCycle' -import { LifeCycleEventType } from './lifeCycle' export const enum SessionType { SYNTHETICS = 'synthetics', @@ -24,6 +22,7 @@ export interface RumSessionManager { findTrackedSession: (startTime?: RelativeTime) => RumSession | undefined expire: () => void expireObservable: Observable + renewObservable: Observable setForcedReplay: () => void } @@ -47,9 +46,9 @@ export const enum SessionReplayState { export function startRumSessionManager( configuration: RumConfiguration, - lifeCycle: LifeCycle, - trackingConsentState: TrackingConsentState -): RumSessionManager { + trackingConsentState: TrackingConsentState, + onReady: (sessionManager: RumSessionManager) => void +) { const sessionManager = startSessionManager( configuration, RUM_SESSION_KEY, @@ -57,14 +56,6 @@ export function startRumSessionManager( trackingConsentState ) - sessionManager.expireObservable.subscribe(() => { - lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED) - }) - - sessionManager.renewObservable.subscribe(() => { - lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED) - }) - sessionManager.sessionStateUpdateObservable.subscribe(({ previousState, newState }) => { if (!previousState.forcedReplay && newState.forcedReplay) { const sessionEntity = sessionManager.findSession() @@ -73,7 +64,8 @@ export function startRumSessionManager( } } }) - return { + + onReady({ findTrackedSession: (startTime) => { const session = sessionManager.findSession(startTime) if (!session || session.trackingType === RumTrackingType.NOT_TRACKED) { @@ -92,8 +84,9 @@ export function startRumSessionManager( }, expire: sessionManager.expire, expireObservable: sessionManager.expireObservable, + renewObservable: sessionManager.renewObservable, setForcedReplay: () => sessionManager.updateSessionState({ forcedReplay: '1' }), - } + }) } /** @@ -108,6 +101,7 @@ export function startRumSessionManagerStub(): RumSessionManager { findTrackedSession: () => session, expire: noop, expireObservable: new Observable(), + renewObservable: new Observable(), setForcedReplay: noop, } } diff --git a/packages/rum-core/test/mockRumSessionManager.ts b/packages/rum-core/test/mockRumSessionManager.ts index 3911e6d7fc..b2e4a76e73 100644 --- a/packages/rum-core/test/mockRumSessionManager.ts +++ b/packages/rum-core/test/mockRumSessionManager.ts @@ -45,6 +45,7 @@ export function createRumSessionManagerMock(): RumSessionManagerMock { this.expireObservable.notify() }, expireObservable: new Observable(), + renewObservable: new Observable(), setId(newId) { id = newId return this From 496a142f8089f6e292ad01985b06b9e70708b656 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 1 Sep 2025 11:57:38 +0200 Subject: [PATCH 03/41] make sessionManager start asynchronously --- .../src/domain/session/sessionManager.spec.ts | 233 ++++++++++-------- .../core/src/domain/session/sessionManager.ts | 82 +++--- .../core/src/domain/session/sessionStore.ts | 55 +++-- .../logs/src/domain/logsSessionManager.ts | 30 +-- .../rum-core/src/domain/rumSessionManager.ts | 67 ++--- 5 files changed, 257 insertions(+), 210 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index bdcdd7aa22..0e68553f53 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -14,6 +14,7 @@ import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' import type { Configuration } from '../configuration' import type { TrackingConsentState } from '../trackingConsent' import { TrackingConsent, createTrackingConsentState } from '../trackingConsent' +import { isChromium } from '../../tools/utils/browserDetection' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' import { @@ -25,6 +26,7 @@ import { import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' import { STORAGE_POLL_DELAY } from './sessionStore' +import { createLock, LOCK_RETRY_DELAY } from './sessionStoreOperations' const enum FakeTrackingType { NOT_TRACKED = SESSION_NOT_TRACKED, @@ -97,9 +99,9 @@ describe('startSessionManager', () => { }) describe('resume from a frozen tab ', () => { - it('when session in store, do nothing', () => { + it('when session in store, do nothing', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&first=tracked', DURATION) - const sessionManager = startSessionManagerWithDefaults() + const sessionManager = await startSessionManagerWithDefaults() window.dispatchEvent(createNewEvent(DOM_EVENT.RESUME)) @@ -107,8 +109,8 @@ describe('startSessionManager', () => { expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) - it('when session not in store, reinitialize a session in store', () => { - const sessionManager = startSessionManagerWithDefaults() + it('when session not in store, reinitialize a session in store', async () => { + const sessionManager = await startSessionManagerWithDefaults() deleteSessionCookie() @@ -122,15 +124,15 @@ describe('startSessionManager', () => { }) describe('cookie management', () => { - it('when tracked, should store tracking type and session id', () => { - const sessionManager = startSessionManagerWithDefaults() + it('when tracked, should store tracking type and session id', async () => { + const sessionManager = await startSessionManagerWithDefaults() expectSessionIdToBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) - it('when not tracked should store tracking type', () => { - const sessionManager = startSessionManagerWithDefaults({ + it('when not tracked should store tracking type', async () => { + const sessionManager = await startSessionManagerWithDefaults({ computeTrackingType: () => FakeTrackingType.NOT_TRACKED, }) @@ -138,19 +140,19 @@ describe('startSessionManager', () => { expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) }) - it('when tracked should keep existing tracking type and session id', () => { + it('when tracked should keep existing tracking type and session id', async () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&first=tracked', DURATION) - const sessionManager = startSessionManagerWithDefaults() + const sessionManager = await startSessionManagerWithDefaults() expectSessionIdToBe(sessionManager, 'abcdef') expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) - it('when not tracked should keep existing tracking type', () => { + it('when not tracked should keep existing tracking type', async () => { setCookie(SESSION_STORE_KEY, `first=${SESSION_NOT_TRACKED}`, DURATION) - const sessionManager = startSessionManagerWithDefaults({ + const sessionManager = await startSessionManagerWithDefaults({ computeTrackingType: () => FakeTrackingType.NOT_TRACKED, }) @@ -166,33 +168,33 @@ describe('startSessionManager', () => { spy = jasmine.createSpy().and.returnValue(FakeTrackingType.TRACKED) }) - it('should be called with an empty value if the cookie is not defined', () => { - startSessionManagerWithDefaults({ computeTrackingType: spy }) + it('should be called with an empty value if the cookie is not defined', async () => { + await startSessionManagerWithDefaults({ computeTrackingType: spy }) expect(spy).toHaveBeenCalledWith(undefined) }) - it('should be called with an invalid value if the cookie has an invalid value', () => { + it('should be called with an invalid value if the cookie has an invalid value', async () => { setCookie(SESSION_STORE_KEY, 'first=invalid', DURATION) - startSessionManagerWithDefaults({ computeTrackingType: spy }) + await startSessionManagerWithDefaults({ computeTrackingType: spy }) expect(spy).toHaveBeenCalledWith('invalid') }) - it('should be called with TRACKED', () => { + it('should be called with TRACKED', async () => { setCookie(SESSION_STORE_KEY, 'first=tracked', DURATION) - startSessionManagerWithDefaults({ computeTrackingType: spy }) + await startSessionManagerWithDefaults({ computeTrackingType: spy }) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) - it('should be called with NOT_TRACKED', () => { + it('should be called with NOT_TRACKED', async () => { setCookie(SESSION_STORE_KEY, `first=${SESSION_NOT_TRACKED}`, DURATION) - startSessionManagerWithDefaults({ computeTrackingType: spy }) + await startSessionManagerWithDefaults({ computeTrackingType: spy }) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) }) describe('session renewal', () => { - it('should renew on activity after expiration', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should renew on activity after expiration', async () => { + const sessionManager = await startSessionManagerWithDefaults() const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -210,8 +212,8 @@ describe('startSessionManager', () => { expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) - it('should not renew on visibility after expiration', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should not renew on visibility after expiration', async () => { + const sessionManager = await startSessionManagerWithDefaults() const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -223,8 +225,8 @@ describe('startSessionManager', () => { expectSessionToBeExpired(sessionManager) }) - it('should not renew on activity if cookie is deleted by a 3rd party', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should not renew on activity if cookie is deleted by a 3rd party', async () => { + const sessionManager = await startSessionManagerWithDefaults() const renewSessionSpy = jasmine.createSpy('renewSessionSpy') sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -244,18 +246,20 @@ describe('startSessionManager', () => { }) describe('multiple startSessionManager calls', () => { - it('should re-use the same session id', () => { - const firstSessionManager = startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) - const idA = firstSessionManager.findSession()!.id + it('should re-use the same session id', async () => { + const [firstSessionManager, secondSessionManager] = await Promise.all([ + startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }), + startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }), + ]) - const secondSessionManager = startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) + const idA = firstSessionManager.findSession()!.id const idB = secondSessionManager.findSession()!.id expect(idA).toBe(idB) }) - it('should not erase other session type', () => { - startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) + it('should not erase other session type', async () => { + await startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) // schedule an expandOrRenewSession document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) @@ -265,7 +269,7 @@ describe('startSessionManager', () => { // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) - startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) + await startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) // cookie correctly set expect(getSessionState(SESSION_STORE_KEY).first).toBeDefined() @@ -278,28 +282,33 @@ describe('startSessionManager', () => { expect(getSessionState(SESSION_STORE_KEY).second).toBeDefined() }) - it('should have independent tracking types', () => { - const firstSessionManager = startSessionManagerWithDefaults({ - productKey: FIRST_PRODUCT_KEY, - computeTrackingType: () => FakeTrackingType.TRACKED, - }) - const secondSessionManager = startSessionManagerWithDefaults({ - productKey: SECOND_PRODUCT_KEY, - computeTrackingType: () => FakeTrackingType.NOT_TRACKED, - }) + it('should have independent tracking types', async () => { + const [firstSessionManager, secondSessionManager] = await Promise.all([ + startSessionManagerWithDefaults({ + productKey: FIRST_PRODUCT_KEY, + computeTrackingType: () => FakeTrackingType.TRACKED, + }), + startSessionManagerWithDefaults({ + productKey: SECOND_PRODUCT_KEY, + computeTrackingType: () => FakeTrackingType.NOT_TRACKED, + }), + ]) expect(firstSessionManager.findSession()!.trackingType).toEqual(FakeTrackingType.TRACKED) expect(secondSessionManager.findSession()!.trackingType).toEqual(FakeTrackingType.NOT_TRACKED) }) - it('should notify each expire and renew observables', () => { - const firstSessionManager = startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) + it('should notify each expire and renew observables', async () => { + const [firstSessionManager, secondSessionManager] = await Promise.all([ + startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }), + startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }), + ]) + const expireSessionASpy = jasmine.createSpy() firstSessionManager.expireObservable.subscribe(expireSessionASpy) const renewSessionASpy = jasmine.createSpy() firstSessionManager.renewObservable.subscribe(renewSessionASpy) - const secondSessionManager = startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) const expireSessionBSpy = jasmine.createSpy() secondSessionManager.expireObservable.subscribe(expireSessionBSpy) const renewSessionBSpy = jasmine.createSpy() @@ -320,8 +329,8 @@ describe('startSessionManager', () => { }) describe('session timeout', () => { - it('should expire the session when the time out delay is reached', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should expire the session when the time out delay is reached', async () => { + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -333,10 +342,10 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalled() }) - it('should renew an existing timed out session', () => { + it('should renew an existing timed out session', async () => { setCookie(SESSION_STORE_KEY, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) - const sessionManager = startSessionManagerWithDefaults() + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -345,10 +354,10 @@ describe('startSessionManager', () => { expect(expireSessionSpy).not.toHaveBeenCalled() // the session has not been active from the start }) - it('should not add created date to an existing session from an older versions', () => { + it('should not add created date to an existing session from an older versions', async () => { setCookie(SESSION_STORE_KEY, 'id=abcde&first=tracked', DURATION) - const sessionManager = startSessionManagerWithDefaults() + const sessionManager = await startSessionManagerWithDefaults() expect(sessionManager.findSession()!.id).toBe('abcde') expect(getSessionState(SESSION_STORE_KEY).created).toBeUndefined() @@ -364,8 +373,8 @@ describe('startSessionManager', () => { restorePageVisibility() }) - it('should expire the session after expiration delay', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should expire the session after expiration delay', async () => { + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -376,8 +385,8 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalled() }) - it('should expand duration on activity', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should expand duration on activity', async () => { + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -395,8 +404,8 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalled() }) - it('should expand not tracked session duration on activity', () => { - const sessionManager = startSessionManagerWithDefaults({ + it('should expand not tracked session duration on activity', async () => { + const sessionManager = await startSessionManagerWithDefaults({ computeTrackingType: () => FakeTrackingType.NOT_TRACKED, }) const expireSessionSpy = jasmine.createSpy() @@ -416,10 +425,10 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalled() }) - it('should expand session on visibility', () => { + it('should expand session on visibility', async () => { setPageVisibility('visible') - const sessionManager = startSessionManagerWithDefaults() + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -437,10 +446,10 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalled() }) - it('should expand not tracked session on visibility', () => { + it('should expand not tracked session on visibility', async () => { setPageVisibility('visible') - const sessionManager = startSessionManagerWithDefaults({ + const sessionManager = await startSessionManagerWithDefaults({ computeTrackingType: () => FakeTrackingType.NOT_TRACKED, }) const expireSessionSpy = jasmine.createSpy() @@ -462,8 +471,8 @@ describe('startSessionManager', () => { }) describe('manual session expiration', () => { - it('expires the session when calling expire()', () => { - const sessionManager = startSessionManagerWithDefaults() + it('expires the session when calling expire()', async () => { + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -473,8 +482,8 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalled() }) - it('notifies expired session only once when calling expire() multiple times', () => { - const sessionManager = startSessionManagerWithDefaults() + it('notifies expired session only once when calling expire() multiple times', async () => { + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -485,8 +494,8 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalledTimes(1) }) - it('notifies expired session only once when calling expire() after the session has been expired', () => { - const sessionManager = startSessionManagerWithDefaults() + it('notifies expired session only once when calling expire() after the session has been expired', async () => { + const sessionManager = await startSessionManagerWithDefaults() const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -497,8 +506,8 @@ describe('startSessionManager', () => { expect(expireSessionSpy).toHaveBeenCalledTimes(1) }) - it('renew the session on user activity', () => { - const sessionManager = startSessionManagerWithDefaults() + it('renew the session on user activity', async () => { + const sessionManager = await startSessionManagerWithDefaults() clock.tick(STORAGE_POLL_DELAY) sessionManager.expire() @@ -510,22 +519,22 @@ describe('startSessionManager', () => { }) describe('session history', () => { - it('should return undefined when there is no current session and no startTime', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should return undefined when there is no current session and no startTime', async () => { + const sessionManager = await startSessionManagerWithDefaults() expireSessionCookie() expect(sessionManager.findSession()).toBeUndefined() }) - it('should return the current session context when there is no start time', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should return the current session context when there is no start time', async () => { + const sessionManager = await startSessionManagerWithDefaults() expect(sessionManager.findSession()!.id).toBeDefined() expect(sessionManager.findSession()!.trackingType).toBeDefined() }) - it('should return the session context corresponding to startTime', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should return the session context corresponding to startTime', async () => { + const sessionManager = await startSessionManagerWithDefaults() // 0s to 10s: first session clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY) @@ -550,8 +559,8 @@ describe('startSessionManager', () => { }) describe('option `returnInactive` is true', () => { - it('should return the session context even when the session is expired', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should return the session context even when the session is expired', async () => { + const sessionManager = await startSessionManagerWithDefaults() // 0s to 10s: first session clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY) @@ -567,8 +576,8 @@ describe('startSessionManager', () => { }) }) - it('should return the current session context in the renewObservable callback', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should return the current session context in the renewObservable callback', async () => { + const sessionManager = await startSessionManagerWithDefaults() let currentSession sessionManager.renewObservable.subscribe(() => (currentSession = sessionManager.findSession())) @@ -580,8 +589,8 @@ describe('startSessionManager', () => { expect(currentSession).toBeDefined() }) - it('should return the current session context in the expireObservable callback', () => { - const sessionManager = startSessionManagerWithDefaults() + it('should return the current session context in the expireObservable callback', async () => { + const sessionManager = await startSessionManagerWithDefaults() let currentSession sessionManager.expireObservable.subscribe(() => (currentSession = sessionManager.findSession())) @@ -594,9 +603,9 @@ describe('startSessionManager', () => { }) describe('tracking consent', () => { - it('expires the session when tracking consent is withdrawn', () => { + it('expires the session when tracking consent is withdrawn', async () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + const sessionManager = await startSessionManagerWithDefaults({ trackingConsentState }) trackingConsentState.update(TrackingConsent.NOT_GRANTED) @@ -604,9 +613,9 @@ describe('startSessionManager', () => { expect(getSessionState(SESSION_STORE_KEY).isExpired).toBe('1') }) - it('does not renew the session when tracking consent is withdrawn', () => { + it('does not renew the session when tracking consent is withdrawn', async () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + const sessionManager = await startSessionManagerWithDefaults({ trackingConsentState }) trackingConsentState.update(TrackingConsent.NOT_GRANTED) @@ -615,9 +624,9 @@ describe('startSessionManager', () => { expectSessionToBeExpired(sessionManager) }) - it('renews the session when tracking consent is granted', () => { + it('renews the session when tracking consent is granted', async () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + const sessionManager = await startSessionManagerWithDefaults({ trackingConsentState }) const initialSessionId = sessionManager.findSession()!.id trackingConsentState.update(TrackingConsent.NOT_GRANTED) @@ -644,9 +653,9 @@ describe('startSessionManager', () => { }) describe('session state update', () => { - it('should notify session manager update observable', () => { + it('should notify session manager update observable', async () => { const sessionStateUpdateSpy = jasmine.createSpy() - const sessionManager = startSessionManagerWithDefaults() + const sessionManager = await startSessionManagerWithDefaults() sessionManager.sessionStateUpdateObservable.subscribe(sessionStateUpdateSpy) sessionManager.updateSessionState({ extra: 'extra' }) @@ -660,6 +669,31 @@ describe('startSessionManager', () => { }) }) + describe('delayed session manager initialization', () => { + it('starts the session manager synchronously if the session cookie is not locked', () => { + void startSessionManagerWithDefaults() + expect(getSessionState(SESSION_STORE_KEY).id).toBeDefined() + expect(getSessionState(SESSION_STORE_KEY)[FIRST_PRODUCT_KEY]).toEqual(FakeTrackingType.TRACKED) + }) + + it('delays the session manager initialization if the session cookie is locked', () => { + if (!isChromium()) { + pending('the lock is only enabled in Chromium') + } + setCookie(SESSION_STORE_KEY, `lock=${createLock()}`, DURATION) + void startSessionManagerWithDefaults() + expect(getSessionState(SESSION_STORE_KEY).id).toBeUndefined() + expect(getSessionState(SESSION_STORE_KEY)[FIRST_PRODUCT_KEY]).not.toBeDefined() + + // Remove the lock + setCookie(SESSION_STORE_KEY, 'id=abcde', DURATION) + clock.tick(LOCK_RETRY_DELAY) + + expect(getSessionState(SESSION_STORE_KEY).id).toBe('abcde') + expect(getSessionState(SESSION_STORE_KEY)[FIRST_PRODUCT_KEY]).toEqual(FakeTrackingType.TRACKED) + }) + }) + function startSessionManagerWithDefaults({ configuration, productKey = FIRST_PRODUCT_KEY, @@ -671,14 +705,17 @@ describe('startSessionManager', () => { computeTrackingType?: () => FakeTrackingType trackingConsentState?: TrackingConsentState } = {}) { - return startSessionManager( - { - sessionStoreStrategyType: STORE_TYPE, - ...configuration, - } as Configuration, - productKey, - computeTrackingType, - trackingConsentState - ) + return new Promise>((resolve) => { + startSessionManager( + { + sessionStoreStrategyType: STORE_TYPE, + ...configuration, + } as Configuration, + productKey, + computeTrackingType, + trackingConsentState, + resolve + ) + }) } }) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 51b35250ae..05fbebfd2c 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -49,8 +49,9 @@ export function startSessionManager( configuration: Configuration, productKey: string, computeTrackingType: (rawTrackingType?: string) => TrackingType, - trackingConsentState: TrackingConsentState -): SessionManager { + trackingConsentState: TrackingConsentState, + onReady: (sessionManager: SessionManager) => void +) { const renewObservable = new Observable() const expireObservable = new Observable() @@ -68,41 +69,51 @@ export function startSessionManager( }) stopCallbacks.push(() => sessionContextHistory.stop()) - sessionStore.renewObservable.subscribe(() => { - sessionContextHistory.add(buildSessionContext(), relativeNow()) - renewObservable.notify() - }) - sessionStore.expireObservable.subscribe(() => { - expireObservable.notify() - sessionContextHistory.closeActive(relativeNow()) - }) - // We expand/renew session unconditionally as tracking consent is always granted when the session // manager is started. - sessionStore.expandOrRenewSession() - sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative) - if (isExperimentalFeatureEnabled(ExperimentalFeature.SHORT_SESSION_INVESTIGATION)) { - const session = sessionStore.getSession() - if (session) { - detectSessionIdChange(configuration, session) + sessionStore.expandOrRenewSession(() => { + sessionStore.renewObservable.subscribe(() => { + sessionContextHistory.add(buildSessionContext(), relativeNow()) + renewObservable.notify() + }) + sessionStore.expireObservable.subscribe(() => { + expireObservable.notify() + sessionContextHistory.closeActive(relativeNow()) + }) + + sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative) + if (isExperimentalFeatureEnabled(ExperimentalFeature.SHORT_SESSION_INVESTIGATION)) { + const session = sessionStore.getSession() + if (session) { + detectSessionIdChange(configuration, session) + } } - } - trackingConsentState.observable.subscribe(() => { - if (trackingConsentState.isGranted()) { - sessionStore.expandOrRenewSession() - } else { - sessionStore.expire(false) - } - }) + trackingConsentState.observable.subscribe(() => { + if (trackingConsentState.isGranted()) { + sessionStore.expandOrRenewSession() + } else { + sessionStore.expire() + } + }) - trackActivity(configuration, () => { - if (trackingConsentState.isGranted()) { - sessionStore.expandOrRenewSession() - } + trackActivity(configuration, () => { + if (trackingConsentState.isGranted()) { + sessionStore.expandOrRenewSession() + } + }) + trackVisibility(configuration, () => sessionStore.expandSession()) + trackResume(configuration, () => sessionStore.restartSession()) + + onReady({ + findSession: (startTime, options) => sessionContextHistory.find(startTime, options), + renewObservable, + expireObservable, + sessionStateUpdateObservable: sessionStore.sessionStateUpdateObservable, + expire: sessionStore.expire, + updateSessionState: sessionStore.updateSessionState, + }) }) - trackVisibility(configuration, () => sessionStore.expandSession()) - trackResume(configuration, () => sessionStore.restartSession()) function buildSessionContext() { const session = sessionStore.getSession() @@ -125,15 +136,6 @@ export function startSessionManager( anonymousId: session.anonymousId, } } - - return { - findSession: (startTime, options) => sessionContextHistory.find(startTime, options), - renewObservable, - expireObservable, - sessionStateUpdateObservable: sessionStore.sessionStateUpdateObservable, - expire: sessionStore.expire, - updateSessionState: sessionStore.updateSessionState, - } } export function stopSessionManager() { diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index cdec96d906..83493f79ed 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -19,7 +19,7 @@ import { processSessionStoreOperations } from './sessionStoreOperations' import { SESSION_NOT_TRACKED, SessionPersistence } from './sessionConstants' export interface SessionStore { - expandOrRenewSession: () => void + expandOrRenewSession: (callback?: () => void) => void expandSession: () => void getSession: () => SessionState restartSession: () => void @@ -94,30 +94,34 @@ export function startSessionStore( const watchSessionTimeoutId = setInterval(watchSession, STORAGE_POLL_DELAY) let sessionCache: SessionState - startSession() - - const { throttled: throttledExpandOrRenewSession, cancel: cancelExpandOrRenewSession } = throttle(() => { - processSessionStoreOperations( - { - process: (sessionState) => { - if (isSessionInNotStartedState(sessionState)) { - return - } - - const synchronizedSession = synchronizeSession(sessionState) - expandOrRenewSessionState(synchronizedSession) - return synchronizedSession - }, - after: (sessionState) => { - if (isSessionStarted(sessionState) && !hasSessionInCache()) { - renewSessionInCache(sessionState) - } - sessionCache = sessionState + const { throttled: throttledExpandOrRenewSession, cancel: cancelExpandOrRenewSession } = throttle( + (callback?: () => void) => { + processSessionStoreOperations( + { + process: (sessionState) => { + if (isSessionInNotStartedState(sessionState)) { + return + } + + const synchronizedSession = synchronizeSession(sessionState) + expandOrRenewSessionState(synchronizedSession) + return synchronizedSession + }, + after: (sessionState) => { + if (isSessionStarted(sessionState) && !hasSessionInCache()) { + renewSessionInCache(sessionState) + } + sessionCache = sessionState + callback?.() + }, }, - }, - sessionStoreStrategy - ) - }, STORAGE_POLL_DELAY) + sessionStoreStrategy + ) + }, + STORAGE_POLL_DELAY + ) + + startSession() function expandSession() { processSessionStoreOperations( @@ -165,7 +169,7 @@ export function startSessionStore( return sessionState } - function startSession() { + function startSession(callback?: () => void) { processSessionStoreOperations( { process: (sessionState) => { @@ -176,6 +180,7 @@ export function startSessionStore( }, after: (sessionState) => { sessionCache = sessionState + callback?.() }, }, sessionStoreStrategy diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index d3eeedff0a..d041d4ad5e 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -24,24 +24,26 @@ export function startLogsSessionManager( trackingConsentState: TrackingConsentState, onReady: (sessionManager: LogsSessionManager) => void ) { - const sessionManager = startSessionManager( + startSessionManager( configuration, LOGS_SESSION_KEY, (rawTrackingType) => computeTrackingType(configuration, rawTrackingType), - trackingConsentState + trackingConsentState, + (sessionManager) => { + onReady({ + findTrackedSession: (startTime?: RelativeTime, options = { returnInactive: false }) => { + const session = sessionManager.findSession(startTime, options) + return session && session.trackingType === LoggerTrackingType.TRACKED + ? { + id: session.id, + anonymousId: session.anonymousId, + } + : undefined + }, + expireObservable: sessionManager.expireObservable, + }) + } ) - onReady({ - findTrackedSession: (startTime?: RelativeTime, options = { returnInactive: false }) => { - const session = sessionManager.findSession(startTime, options) - return session && session.trackingType === LoggerTrackingType.TRACKED - ? { - id: session.id, - anonymousId: session.anonymousId, - } - : undefined - }, - expireObservable: sessionManager.expireObservable, - }) } export function startLogsSessionManagerStub(configuration: LogsConfiguration): LogsSessionManager { diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index 3e8b06e733..6b9b59c4ff 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -49,44 +49,45 @@ export function startRumSessionManager( trackingConsentState: TrackingConsentState, onReady: (sessionManager: RumSessionManager) => void ) { - const sessionManager = startSessionManager( + startSessionManager( configuration, RUM_SESSION_KEY, (rawTrackingType) => computeTrackingType(configuration, rawTrackingType), - trackingConsentState - ) + trackingConsentState, + (sessionManager) => { + sessionManager.sessionStateUpdateObservable.subscribe(({ previousState, newState }) => { + if (!previousState.forcedReplay && newState.forcedReplay) { + const sessionEntity = sessionManager.findSession() + if (sessionEntity) { + sessionEntity.isReplayForced = true + } + } + }) - sessionManager.sessionStateUpdateObservable.subscribe(({ previousState, newState }) => { - if (!previousState.forcedReplay && newState.forcedReplay) { - const sessionEntity = sessionManager.findSession() - if (sessionEntity) { - sessionEntity.isReplayForced = true - } + onReady({ + findTrackedSession: (startTime) => { + const session = sessionManager.findSession(startTime) + if (!session || session.trackingType === RumTrackingType.NOT_TRACKED) { + return + } + return { + id: session.id, + sessionReplay: + session.trackingType === RumTrackingType.TRACKED_WITH_SESSION_REPLAY + ? SessionReplayState.SAMPLED + : session.isReplayForced + ? SessionReplayState.FORCED + : SessionReplayState.OFF, + anonymousId: session.anonymousId, + } + }, + expire: sessionManager.expire, + expireObservable: sessionManager.expireObservable, + renewObservable: sessionManager.renewObservable, + setForcedReplay: () => sessionManager.updateSessionState({ forcedReplay: '1' }), + }) } - }) - - onReady({ - findTrackedSession: (startTime) => { - const session = sessionManager.findSession(startTime) - if (!session || session.trackingType === RumTrackingType.NOT_TRACKED) { - return - } - return { - id: session.id, - sessionReplay: - session.trackingType === RumTrackingType.TRACKED_WITH_SESSION_REPLAY - ? SessionReplayState.SAMPLED - : session.isReplayForced - ? SessionReplayState.FORCED - : SessionReplayState.OFF, - anonymousId: session.anonymousId, - } - }, - expire: sessionManager.expire, - expireObservable: sessionManager.expireObservable, - renewObservable: sessionManager.renewObservable, - setForcedReplay: () => sessionManager.updateSessionState({ forcedReplay: '1' }), - }) + ) } /** From 7ecaca8a09f84f37171e18c6d13aab4b7cfcd621 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 9 Jan 2026 12:41:06 +0100 Subject: [PATCH 04/41] test: adding async tests --- .../src/domain/session/sessionManager.spec.ts | 30 +++++++++++- .../src/domain/session/sessionStore.spec.ts | 46 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 0e68553f53..0175de7576 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -641,9 +641,9 @@ describe('startSessionManager', () => { expect(sessionManager.findSession()!.id).not.toBe(initialSessionId) }) - it('Remove anonymousId when tracking consent is withdrawn', () => { + it('Remove anonymousId when tracking consent is withdrawn', async () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) + const sessionManager = await startSessionManagerWithDefaults({ trackingConsentState }) const session = sessionManager.findSession()! trackingConsentState.update(TrackingConsent.NOT_GRANTED) @@ -692,6 +692,32 @@ describe('startSessionManager', () => { expect(getSessionState(SESSION_STORE_KEY).id).toBe('abcde') expect(getSessionState(SESSION_STORE_KEY)[FIRST_PRODUCT_KEY]).toEqual(FakeTrackingType.TRACKED) }) + + it('should call onReady callback with session manager after lock is released', () => { + if (!isChromium()) { + pending('the lock is only enabled in Chromium') + } + + setCookie(SESSION_STORE_KEY, `lock=${createLock()}`, DURATION) + const onReadySpy = jasmine.createSpy<(sessionManager: SessionManager) => void>('onReady') + + startSessionManager( + { sessionStoreStrategyType: STORE_TYPE } as Configuration, + FIRST_PRODUCT_KEY, + () => FakeTrackingType.TRACKED, + createTrackingConsentState(TrackingConsent.GRANTED), + onReadySpy + ) + + expect(onReadySpy).not.toHaveBeenCalled() + + // Remove lock + setCookie(SESSION_STORE_KEY, 'id=abc123', DURATION) + clock.tick(LOCK_RETRY_DELAY) + + expect(onReadySpy).toHaveBeenCalledTimes(1) + expect(onReadySpy.calls.mostRecent().args[0].findSession).toBeDefined() + }) }) function startSessionManagerWithDefaults({ diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 35137cd3c3..1dedd802cd 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -11,6 +11,7 @@ import { SessionPersistence, } from './sessionConstants' import type { SessionState } from './sessionState' +import { LOCK_RETRY_DELAY, createLock } from './sessionStoreOperations' const enum FakeTrackingType { TRACKED = 'tracked', @@ -413,6 +414,51 @@ describe('session store', () => { expect(sessionStoreManager.getSession().id).toBeUndefined() expect(renewSpy).not.toHaveBeenCalled() }) + + it('should execute callback after session expansion', () => { + setupSessionStore(createSessionState(FakeTrackingType.TRACKED, FIRST_ID)) + + const callbackSpy = jasmine.createSpy('callback') + sessionStoreManager.expandOrRenewSession(callbackSpy) + + expect(callbackSpy).toHaveBeenCalledTimes(1) + }) + + it('should execute callback after lock is released', () => { + const sessionStoreStrategyType = selectSessionStoreStrategyType(DEFAULT_INIT_CONFIGURATION) + if (sessionStoreStrategyType?.type !== SessionPersistence.COOKIE) { + fail('Unable to initialize cookie storage') + return + } + + // Create a locked session state + const lockedSession: SessionState = { + ...createSessionState(FakeTrackingType.TRACKED, FIRST_ID), + lock: createLock(), + } + + sessionStoreStrategy = createFakeSessionStoreStrategy({ isLockEnabled: true, initialSession: lockedSession }) + + sessionStoreManager = startSessionStore( + sessionStoreStrategyType, + DEFAULT_CONFIGURATION, + PRODUCT_KEY, + () => FakeTrackingType.TRACKED, + sessionStoreStrategy + ) + + const callbackSpy = jasmine.createSpy('callback') + sessionStoreManager.expandOrRenewSession(callbackSpy) + + expect(callbackSpy).not.toHaveBeenCalled() + + // Remove the lock from the session + sessionStoreStrategy.planRetrieveSession(0, createSessionState(FakeTrackingType.TRACKED, FIRST_ID)) + + clock.tick(LOCK_RETRY_DELAY) + + expect(callbackSpy).toHaveBeenCalledTimes(1) + }) }) describe('expand session', () => { From fe8b84844817f1e85203cb100a3dfa6a0cde9327 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 9 Jan 2026 12:45:45 +0100 Subject: [PATCH 05/41] fix: linter warnings --- packages/logs/src/boot/startLogs.spec.ts | 3 --- packages/logs/src/boot/startLogs.ts | 3 +-- packages/logs/src/domain/logsSessionManager.spec.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 7d5d63bbbc..7f22756182 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -2,8 +2,6 @@ import type { BufferedData, Payload } from '@datadog/browser-core' import { ErrorSource, display, - getCookie, - SESSION_STORE_KEY, createTrackingConsentState, TrackingConsent, STORAGE_POLL_DELAY, @@ -15,7 +13,6 @@ import { interceptRequests, mockEndpointBuilder, mockEventBridge, - mockSyntheticsWorkerValues, registerCleanupTask, mockClock, DEFAULT_FETCH_MOCK, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index d883cc37af..2d4f89799c 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -3,7 +3,6 @@ import { Observable, sendToExtension, createPageMayExitObservable, - willSyntheticsInjectRum, canUseEventBridge, startAccountContext, startGlobalContext, @@ -13,7 +12,7 @@ import { startUserContext, isWorkerEnvironment, } from '@datadog/browser-core' -import { LogsSessionManager, startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' +import type { LogsSessionManager } from '../domain/logsSessionManager' import type { LogsConfiguration } from '../domain/configuration' import { startLogsAssembly } from '../domain/assembly' import { startConsoleCollection } from '../domain/console/consoleCollection' diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 9230e851b5..99521e2be4 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -15,10 +15,10 @@ import type { Clock } from '@datadog/browser-core/test' import { createNewEvent, expireCookie, getSessionState, mockClock } from '@datadog/browser-core/test' import type { LogsConfiguration } from './configuration' +import type { LogsSessionManager } from './logsSessionManager' import { LOGS_SESSION_KEY, LoggerTrackingType, - LogsSessionManager, startLogsSessionManager, startLogsSessionManagerStub, } from './logsSessionManager' From 77740082b6f3cb460578d62faa6b825ba1767df5 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Mon, 12 Jan 2026 14:20:10 +0100 Subject: [PATCH 06/41] fix: tracking consent notify --- packages/core/src/domain/session/sessionManager.ts | 2 +- packages/rum-core/src/boot/startRum.ts | 1 + rum-events-format | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 05fbebfd2c..bf576f0188 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -93,7 +93,7 @@ export function startSessionManager( if (trackingConsentState.isGranted()) { sessionStore.expandOrRenewSession() } else { - sessionStore.expire() + sessionStore.expire(false) } }) diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 9885a7e49b..47d40c5474 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -83,6 +83,7 @@ export function startRum( lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (event) => sendToExtension('rum', event)) sessionManager.expireObservable.subscribe(() => lifeCycle.notify(LifeCycleEventType.SESSION_EXPIRED)) + sessionManager.renewObservable.subscribe(() => lifeCycle.notify(LifeCycleEventType.SESSION_RENEWED)) const reportError = (error: RawError) => { lifeCycle.notify(LifeCycleEventType.RAW_ERROR_COLLECTED, { error }) diff --git a/rum-events-format b/rum-events-format index d555ad8888..bf49abeaa5 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit d555ad8888ebdabf2b453d3df439c42373d5e999 +Subproject commit bf49abeaa5414d337c346ce618044dde662a9c1f From 10d191dcfd0070f7559b8b8ca2758d06fc69cc4c Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Wed, 14 Jan 2026 11:46:16 +0100 Subject: [PATCH 07/41] fix: create waitFor to await for falsy expressions --- packages/core/test/wait.ts | 46 ++++++++++ packages/logs/src/boot/logsPublicApi.spec.ts | 7 +- packages/logs/src/boot/preStartLogs.spec.ts | 29 +++++-- .../rum-core/src/boot/rumPublicApi.spec.ts | 86 ++++++++++++------- 4 files changed, 128 insertions(+), 40 deletions(-) diff --git a/packages/core/test/wait.ts b/packages/core/test/wait.ts index 1c77f3842f..d6ba2b78bf 100644 --- a/packages/core/test/wait.ts +++ b/packages/core/test/wait.ts @@ -1,3 +1,5 @@ +import {} from 'jasmine' + export function wait(durationMs: number = 0): Promise { return new Promise((resolve) => { setTimeout(resolve, durationMs) @@ -7,3 +9,47 @@ export function wait(durationMs: number = 0): Promise { export function waitNextMicrotask(): Promise { return Promise.resolve() } + +export function waitFor( + callback: () => T | Promise, + options: { timeout?: number; interval?: number } = {} +): Promise { + const { timeout = 1000, interval = 50 } = options + + return new Promise((resolve, reject) => { + const startTime = Date.now() + + function check() { + try { + const result = callback() + if (result && typeof (result as any).then === 'function') { + ;(result as Promise).then(handleResult, handleError) + } else { + handleResult(result as T) + } + } catch (error) { + handleError(error as Error) + } + } + + function handleResult(result: T) { + if (result) { + resolve(result) + } else if (Date.now() - startTime >= timeout) { + reject(new Error(`waitFor timed out after ${timeout}ms`)) + } else { + setTimeout(check, interval) + } + } + + function handleError(error: Error) { + if (Date.now() - startTime >= timeout) { + reject(error) + } else { + setTimeout(check, interval) + } + } + + check() + }) +} diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 7270bbb33a..b43b97f1c7 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,5 +1,6 @@ import type { ContextManager, TimeStamp } from '@datadog/browser-core' import { monitor, display, createContextManager, stopSessionManager } from '@datadog/browser-core' +import { waitFor } from '@datadog/browser-core/test' import type { Logger, LogsMessage } from '../domain/logger' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' @@ -72,9 +73,10 @@ describe('logs entry', () => { describe('common context', () => { let LOGS: LogsPublicApi - beforeEach(() => { + beforeEach(async () => { LOGS = makeLogsPublicApi(startLogs) LOGS.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => startLogs.calls.count() > 0) }) it('should have the current date, view and global context', () => { @@ -93,9 +95,10 @@ describe('logs entry', () => { describe('post start API usages', () => { let LOGS: LogsPublicApi - beforeEach(() => { + beforeEach(async () => { LOGS = makeLogsPublicApi(startLogs) LOGS.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => startLogs.calls.count() > 0) }) it('main logger logs a message', () => { diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 897981a7fe..b81897046b 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -4,6 +4,8 @@ import { mockClock, mockEventBridge, mockSyntheticsWorkerValues, + waitNextMicrotask, + waitFor, } from '@datadog/browser-core/test' import type { TimeStamp, TrackingConsentState } from '@datadog/browser-core' import { @@ -53,7 +55,6 @@ describe('preStartLogs', () => { } as unknown as StartLogsResult) getCommonContextSpy = jasmine.createSpy() strategy = createPreStartStrategy(getCommonContextSpy, createTrackingConsentState(), doStartLogsSpy) - clock = mockClock() }) afterEach(() => { @@ -68,9 +69,10 @@ describe('preStartLogs', () => { displaySpy = spyOn(display, 'error') }) - it('should start when the configuration is valid', () => { + it('should start when the configuration is valid', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION) expect(displaySpy).not.toHaveBeenCalled() + await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) expect(doStartLogsSpy).toHaveBeenCalled() }) @@ -130,7 +132,7 @@ describe('preStartLogs', () => { }) }) - it('allows sending logs', () => { + it('allows sending logs', async () => { strategy.handleLog( { status: StatusType.info, @@ -141,6 +143,7 @@ describe('preStartLogs', () => { expect(handleLogSpy).not.toHaveBeenCalled() strategy.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => handleLogSpy.calls.count() > 0, { timeout: 2000 }) expect(handleLogSpy.calls.all().length).toBe(1) expect(getLoggedMessage(0).message.message).toBe('message') @@ -152,6 +155,8 @@ describe('preStartLogs', () => { describe('save context when submitting a log', () => { it('saves the date', () => { + mockEventBridge() + clock = mockClock() strategy.handleLog( { status: StatusType.info, @@ -162,10 +167,11 @@ describe('preStartLogs', () => { clock.tick(ONE_SECOND) strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(handleLogSpy.calls.count()).toBe(1) expect(getLoggedMessage(0).savedDate).toEqual((Date.now() - ONE_SECOND) as TimeStamp) }) - it('saves the URL', () => { + it('saves the URL', async () => { getCommonContextSpy.and.returnValue({ view: { url: 'url' } } as unknown as CommonContext) strategy.handleLog( { @@ -176,10 +182,11 @@ describe('preStartLogs', () => { ) strategy.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => handleLogSpy.calls.count() > 0, { timeout: 2000 }) expect(getLoggedMessage(0).savedCommonContext!.view?.url).toEqual('url') }) - it('saves the log context', () => { + it('saves the log context', async () => { const context = { foo: 'bar' } strategy.handleLog( { @@ -192,6 +199,7 @@ describe('preStartLogs', () => { context.foo = 'baz' strategy.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => handleLogSpy.calls.count() > 0, { timeout: 2000 }) expect(getLoggedMessage(0).message.context!.foo).toEqual('bar') }) @@ -236,12 +244,13 @@ describe('preStartLogs', () => { expect(doStartLogsSpy).not.toHaveBeenCalled() }) - it('starts logs if tracking consent is granted before init', () => { + it('starts logs if tracking consent is granted before init', async () => { trackingConsentState.update(TrackingConsent.GRANTED) strategy.init({ ...DEFAULT_INIT_CONFIGURATION, trackingConsent: TrackingConsent.NOT_GRANTED, }) + await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) expect(doStartLogsSpy).toHaveBeenCalledTimes(1) }) @@ -254,11 +263,13 @@ describe('preStartLogs', () => { expect(doStartLogsSpy).not.toHaveBeenCalled() }) - it('do not call startLogs when tracking consent state is updated after init', () => { + it('do not call startLogs when tracking consent state is updated after init', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) doStartLogsSpy.calls.reset() trackingConsentState.update(TrackingConsent.GRANTED) + await waitNextMicrotask() expect(doStartLogsSpy).not.toHaveBeenCalled() }) @@ -283,8 +294,10 @@ describe('preStartLogs', () => { }) describe('logs session creation', () => { - it('creates a session on normal conditions', () => { + it('creates a session on normal conditions', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION) + + await waitFor(() => getCookie(SESSION_STORE_KEY) !== undefined, { timeout: 2000 }) expect(getCookie(SESSION_STORE_KEY)).toBeDefined() }) diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index d44b1c34f9..d7eb13d72a 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1,7 +1,7 @@ import type { RelativeTime, DeflateWorker, TimeStamp } from '@datadog/browser-core' import { ONE_SECOND, display, DefaultPrivacyLevel, timeStampToClocks, stopSessionManager } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { mockClock } from '@datadog/browser-core/test' +import { waitFor, mockClock, mockEventBridge } from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' import { ActionType, VitalType } from '../rawRumEvent.types' import type { DurationVitalReference } from '../domain/vital/vitalCollection' @@ -72,11 +72,12 @@ describe('rum public api', () => { ) }) - it('pass the worker to the recorder API', () => { + it('pass the worker to the recorder API', async () => { rumPublicApi.init({ ...DEFAULT_INIT_CONFIGURATION, compressIntakeRequests: true, }) + await waitFor(() => recorderApiOnRumStartSpy.calls.count() > 0) expect(recorderApiOnRumStartSpy.calls.mostRecent().args[4]).toBe(FAKE_WORKER) }) }) @@ -101,15 +102,17 @@ describe('rum public api', () => { ) }) - it('returns the internal context after init', () => { + it('returns the internal context after init', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => rumPublicApi.getInternalContext() !== undefined) expect(rumPublicApi.getInternalContext()).toEqual({ application_id: '123', session_id: '123' }) expect(getInternalContextSpy).toHaveBeenCalled() }) - it('uses the startTime if specified', () => { + it('uses the startTime if specified', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => rumPublicApi.getInternalContext() !== undefined) const startTime = 234832890 expect(rumPublicApi.getInternalContext(startTime)).toEqual({ application_id: '123', session_id: '123' }) @@ -133,6 +136,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { + mockEventBridge() addActionSpy = jasmine.createSpy() rumPublicApi = makeRumPublicApi( () => ({ @@ -142,7 +146,6 @@ describe('rum public api', () => { noopRecorderApi, noopProfilerApi ) - mockClock() }) it('allows sending actions before init', () => { @@ -184,6 +187,8 @@ describe('rum public api', () => { let clock: Clock beforeEach(() => { + mockEventBridge() + clock = mockClock() addErrorSpy = jasmine.createSpy() rumPublicApi = makeRumPublicApi( () => ({ @@ -193,7 +198,6 @@ describe('rum public api', () => { noopRecorderApi, noopProfilerApi ) - clock = mockClock() }) it('allows capturing an error before init', () => { @@ -552,20 +556,20 @@ describe('rum public api', () => { ) }) - it('should add custom timings', () => { + it('should add custom timings', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - rumPublicApi.addTiming('foo') + await waitFor(() => addTimingSpy.calls.count() > 0) expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('foo') expect(addTimingSpy.calls.argsFor(0)[1]).toBeUndefined() expect(displaySpy).not.toHaveBeenCalled() }) - it('adds custom timing with provided time', () => { + it('adds custom timing with provided time', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - rumPublicApi.addTiming('foo', 12) + await waitFor(() => addTimingSpy.calls.count() > 0) expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('foo') expect(addTimingSpy.calls.argsFor(0)[1]).toBe(12 as RelativeTime) @@ -591,10 +595,10 @@ describe('rum public api', () => { ) }) - it('should add feature flag evaluation when ff feature_flags enabled', () => { + it('should add feature flag evaluation when ff feature_flags enabled', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - rumPublicApi.addFeatureFlagEvaluation('feature', 'foo') + await waitFor(() => addFeatureFlagEvaluationSpy.calls.count() > 0) expect(addFeatureFlagEvaluationSpy.calls.argsFor(0)).toEqual(['feature', 'foo']) expect(displaySpy).not.toHaveBeenCalled() @@ -602,7 +606,7 @@ describe('rum public api', () => { }) describe('stopSession', () => { - it('calls stopSession on the startRum result', () => { + it('calls stopSession on the startRum result', async () => { const stopSessionSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ ...noopStartRum(), stopSession: stopSessionSpy }), @@ -611,12 +615,13 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.stopSession() + await waitFor(() => stopSessionSpy.calls.count() > 0) expect(stopSessionSpy).toHaveBeenCalled() }) }) describe('startView', () => { - it('should call RUM results startView with the view name', () => { + it('should call RUM results startView with the view name', async () => { const startViewSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ ...noopStartRum(), startView: startViewSpy }), @@ -625,10 +630,11 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView('foo') + await waitFor(() => startViewSpy.calls.count() > 0) expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo' }) }) - it('should call RUM results startView with the view options', () => { + it('should call RUM results startView with the view options', async () => { const startViewSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ ...noopStartRum(), startView: startViewSpy }), @@ -637,6 +643,7 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView({ name: 'foo', service: 'bar', version: 'baz', context: { foo: 'bar' } }) + await waitFor(() => startViewSpy.calls.count() > 0) expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo', service: 'bar', @@ -657,8 +664,9 @@ describe('rum public api', () => { rumPublicApi = makeRumPublicApi(noopStartRum, recorderApi, noopProfilerApi) }) - it('is started with the default defaultPrivacyLevel', () => { + it('is started with the default defaultPrivacyLevel', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => recorderApiOnRumStartSpy.calls.count() > 0) expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) }) @@ -686,22 +694,24 @@ describe('rum public api', () => { expect(recorderApi.getSessionReplayLink).toHaveBeenCalledTimes(1) }) - it('is started with the default startSessionReplayRecordingManually', () => { + it('is started with the default startSessionReplayRecordingManually', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => recorderApiOnRumStartSpy.calls.count() > 0) expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(true) }) - it('is started with the configured startSessionReplayRecordingManually', () => { + it('is started with the configured startSessionReplayRecordingManually', async () => { rumPublicApi.init({ ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: false, }) + await waitFor(() => recorderApiOnRumStartSpy.calls.count() > 0) expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(false) }) }) describe('startDurationVital', () => { - it('should call startDurationVital on the startRum result', () => { + it('should call startDurationVital on the startRum result', async () => { const startDurationVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ @@ -713,6 +723,7 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) + await waitFor(() => startDurationVitalSpy.calls.count() > 0) expect(startDurationVitalSpy).toHaveBeenCalledWith('foo', { description: 'description-value', context: { foo: 'bar' }, @@ -721,7 +732,7 @@ describe('rum public api', () => { }) describe('stopDurationVital', () => { - it('should call stopDurationVital with a name on the startRum result', () => { + it('should call stopDurationVital with a name on the startRum result', async () => { const stopDurationVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ @@ -734,13 +745,14 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) rumPublicApi.stopDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) + await waitFor(() => stopDurationVitalSpy.calls.count() > 0) expect(stopDurationVitalSpy).toHaveBeenCalledWith('foo', { description: 'description-value', context: { foo: 'bar' }, }) }) - it('should call stopDurationVital with a reference on the startRum result', () => { + it('should call stopDurationVital with a reference on the startRum result', async () => { const stopDurationVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ @@ -753,6 +765,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) const ref = rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) rumPublicApi.stopDurationVital(ref, { context: { foo: 'bar' }, description: 'description-value' }) + await waitFor(() => stopDurationVitalSpy.calls.count() > 0) expect(stopDurationVitalSpy).toHaveBeenCalledWith(ref, { description: 'description-value', context: { foo: 'bar' }, @@ -761,7 +774,7 @@ describe('rum public api', () => { }) describe('addDurationVital', () => { - it('should call addDurationVital on the startRum result', () => { + it('should call addDurationVital on the startRum result', async () => { const addDurationVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ @@ -779,6 +792,7 @@ describe('rum public api', () => { context: { foo: 'bar' }, description: 'description-value', }) + await waitFor(() => addDurationVitalSpy.calls.count() > 0) expect(addDurationVitalSpy).toHaveBeenCalledWith({ name: 'foo', startClocks: timeStampToClocks(startTime), @@ -791,7 +805,7 @@ describe('rum public api', () => { }) describe('startFeatureOperation', () => { - it('should call addOperationStepVital on the startRum result with start status', () => { + it('should call addOperationStepVital on the startRum result with start status', async () => { const addOperationStepVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }), @@ -800,6 +814,7 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) + await waitFor(() => addOperationStepVitalSpy.calls.count() > 0) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'start', { operationKey: '00000000-0000-0000-0000-000000000000', }) @@ -807,7 +822,7 @@ describe('rum public api', () => { }) describe('succeedFeatureOperation', () => { - it('should call addOperationStepVital on the startRum result with end status', () => { + it('should call addOperationStepVital on the startRum result with end status', async () => { const addOperationStepVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }), @@ -816,6 +831,7 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.succeedFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) + await waitFor(() => addOperationStepVitalSpy.calls.count() > 0) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'end', { operationKey: '00000000-0000-0000-0000-000000000000', }) @@ -823,7 +839,7 @@ describe('rum public api', () => { }) describe('failFeatureOperation', () => { - it('should call addOperationStepVital on the startRum result with end status and failure reason', () => { + it('should call addOperationStepVital on the startRum result with end status and failure reason', async () => { const addOperationStepVitalSpy = jasmine.createSpy() const rumPublicApi = makeRumPublicApi( () => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }), @@ -832,6 +848,7 @@ describe('rum public api', () => { ) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.failFeatureOperation('foo', 'error', { operationKey: '00000000-0000-0000-0000-000000000000' }) + await waitFor(() => addOperationStepVitalSpy.calls.count() > 0) expect(addOperationStepVitalSpy).toHaveBeenCalledWith( 'foo', 'end', @@ -862,9 +879,10 @@ describe('rum public api', () => { ) }) - it('should set the view name', () => { + it('should set the view name', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.setViewName('foo') + await waitFor(() => setViewNameSpy.calls.count() > 0) expect(setViewNameSpy).toHaveBeenCalledWith('foo') }) @@ -874,6 +892,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi let setViewContextSpy: jasmine.Spy['setViewContext']> let setViewContextPropertySpy: jasmine.Spy['setViewContextProperty']> + beforeEach(() => { setViewContextSpy = jasmine.createSpy() setViewContextPropertySpy = jasmine.createSpy() @@ -888,18 +907,20 @@ describe('rum public api', () => { ) }) - it('should set view specific context with setViewContext', () => { + it('should set view specific context with setViewContext', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) /* eslint-disable @typescript-eslint/no-unsafe-call */ ;(rumPublicApi as any).setViewContext({ foo: 'bar' }) + await waitFor(() => setViewContextSpy.calls.count() > 0) expect(setViewContextSpy).toHaveBeenCalledWith({ foo: 'bar' }) }) - it('should set view specific context with setViewContextProperty', () => { + it('should set view specific context with setViewContextProperty', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) /* eslint-disable @typescript-eslint/no-unsafe-call */ ;(rumPublicApi as any).setViewContextProperty('foo', 'bar') + await waitFor(() => setViewContextPropertySpy.calls.count() > 0) expect(setViewContextPropertySpy).toHaveBeenCalledWith('foo', 'bar') }) @@ -923,8 +944,12 @@ describe('rum public api', () => { ) }) - it('should return the view context after init', () => { + it('should return the view context after init', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => { + const ctx = rumPublicApi.getViewContext() + return ctx && Object.keys(ctx).length > 0 + }) expect(rumPublicApi.getViewContext()).toEqual({ foo: 'bar' }) expect(getViewContextSpy).toHaveBeenCalled() @@ -943,12 +968,13 @@ describe('rum public api', () => { startRumSpy = jasmine.createSpy().and.callFake(noopStartRum) }) - it('should return the sdk name', () => { + it('should return the sdk name', async () => { const rumPublicApi = makeRumPublicApi(startRumSpy, noopRecorderApi, noopProfilerApi, { sdkName: 'rum-slim', }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + await waitFor(() => startRumSpy.calls.count() > 0) const sdkName = startRumSpy.calls.argsFor(0)[9] expect(sdkName).toBe('rum-slim') }) From 6e0e1f7234643cd2af7854c53e5ffe881cc8c0e7 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Wed, 14 Jan 2026 12:18:54 +0100 Subject: [PATCH 08/41] fix: linter warnings --- packages/core/test/wait.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/test/wait.ts b/packages/core/test/wait.ts index d6ba2b78bf..d3b6263ad2 100644 --- a/packages/core/test/wait.ts +++ b/packages/core/test/wait.ts @@ -1,5 +1,3 @@ -import {} from 'jasmine' - export function wait(durationMs: number = 0): Promise { return new Promise((resolve) => { setTimeout(resolve, durationMs) From 79dbce0f3e53b2a4cd00322ecfbec29151231700 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 16 Jan 2026 16:15:57 +0100 Subject: [PATCH 09/41] fix: schemas --- packages/core/src/domain/telemetry/telemetryEvent.types.ts | 6 +++--- packages/rum-core/src/rumEvent.types.ts | 6 +++--- packages/rum/src/types/sessionReplay.ts | 4 ++-- rum-events-format | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 6e0dfcfd53..2a9f0e5e43 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -641,9 +641,9 @@ export interface CommonTelemetryProperties { */ model?: string /** - * Number of device processors + * Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system. */ - readonly processor_count?: number + readonly logical_cpu_count?: number /** * Total RAM in megabytes */ @@ -651,7 +651,7 @@ export interface CommonTelemetryProperties { /** * Whether the device is considered a low RAM device (Android) */ - readonly is_low_ram_device?: boolean + readonly is_low_ram?: boolean [k: string]: unknown } /** diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index bec99ef31c..594e889283 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -1685,9 +1685,9 @@ export interface CommonProperties { */ readonly brightness_level?: number /** - * Number of device processors + * Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system. */ - readonly processor_count?: number + readonly logical_cpu_count?: number /** * Total RAM in megabytes */ @@ -1695,7 +1695,7 @@ export interface CommonProperties { /** * Whether the device is considered a low RAM device (Android) */ - readonly is_low_ram_device?: boolean + readonly is_low_ram?: boolean [k: string]: unknown } /** diff --git a/packages/rum/src/types/sessionReplay.ts b/packages/rum/src/types/sessionReplay.ts index 8cb22ec203..bceb0a14bf 100644 --- a/packages/rum/src/types/sessionReplay.ts +++ b/packages/rum/src/types/sessionReplay.ts @@ -419,7 +419,7 @@ export type AddDocTypeNodeChange = [ '#doctype' | StringReference, StringOrStringReference, StringOrStringReference, - StringOrStringReference, + StringOrStringReference ] /** * Browser-specific. Schema representing a string, either expressed as a literal or as an index into the string table. @@ -560,7 +560,7 @@ export type VisualViewportChange = [ VisualViewportPageTop, VisualViewportWidth, VisualViewportHeight, - VisualViewportScale, + VisualViewportScale ] /** * The offset of the left edge of the visual viewport from the left edge of the layout viewport in CSS pixels. diff --git a/rum-events-format b/rum-events-format index bf49abeaa5..32918d9997 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit bf49abeaa5414d337c346ce618044dde662a9c1f +Subproject commit 32918d999701fb7bfd876369e27ced77d6de1809 From 7b34e5576bf748846397a183ca066c44a313b764 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 16 Jan 2026 16:55:12 +0100 Subject: [PATCH 10/41] fix: monitor functions before the initialization --- .../domain/session/sessionStoreOperations.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index d00fbbce34..9d27a4eab5 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -2,6 +2,7 @@ import { setTimeout } from '../../tools/timer' import { generateUUID } from '../../tools/utils/stringUtils' import type { TimeStamp } from '../../tools/utils/timeUtils' import { elapsed, ONE_SECOND, timeStampNow } from '../../tools/utils/timeUtils' +import { addTelemetryError } from '../telemetry' import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' import { expandSessionState, isSessionInExpiredState } from './sessionState' @@ -22,6 +23,14 @@ const LOCK_SEPARATOR = '--' const bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined +function safePersist(persistFn: () => void) { + try { + persistFn() + } catch (e) { + addTelemetryError(e) + } +} + export function processSessionStoreOperations( operations: Operations, sessionStoreStrategy: SessionStoreStrategy, @@ -58,7 +67,7 @@ export function processSessionStoreOperations( } // acquire lock currentLock = createLock() - persistWithLock(currentStore.session) + safePersist(() => persistWithLock(currentStore.session)) // if lock is not acquired, retry later currentStore = retrieveStore() if (currentStore.lock !== currentLock) { @@ -77,13 +86,13 @@ export function processSessionStoreOperations( } if (processedSession) { if (isSessionInExpiredState(processedSession)) { - expireSession(processedSession) + safePersist(() => expireSession(processedSession!)) } else { expandSessionState(processedSession) if (isLockEnabled) { - persistWithLock(processedSession) + safePersist(() => persistWithLock(processedSession!)) } else { - persistSession(processedSession) + safePersist(() => persistSession(processedSession!)) } } } @@ -97,7 +106,7 @@ export function processSessionStoreOperations( retryLater(operations, sessionStoreStrategy, numberOfRetries) return } - persistSession(currentStore.session) + safePersist(() => persistSession(currentStore.session)) processedSession = currentStore.session } } From 70449f271320de9dd32f2f779f5c34ac4c74263f Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 16 Jan 2026 17:03:31 +0100 Subject: [PATCH 11/41] fix: schema format --- packages/rum/src/types/sessionReplay.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rum/src/types/sessionReplay.ts b/packages/rum/src/types/sessionReplay.ts index bceb0a14bf..8cb22ec203 100644 --- a/packages/rum/src/types/sessionReplay.ts +++ b/packages/rum/src/types/sessionReplay.ts @@ -419,7 +419,7 @@ export type AddDocTypeNodeChange = [ '#doctype' | StringReference, StringOrStringReference, StringOrStringReference, - StringOrStringReference + StringOrStringReference, ] /** * Browser-specific. Schema representing a string, either expressed as a literal or as an index into the string table. @@ -560,7 +560,7 @@ export type VisualViewportChange = [ VisualViewportPageTop, VisualViewportWidth, VisualViewportHeight, - VisualViewportScale + VisualViewportScale, ] /** * The offset of the left edge of the visual viewport from the left edge of the layout viewport in CSS pixels. From f65e8bab600d54a04e1888319d8f28b43eb00fbb Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Wed, 21 Jan 2026 11:09:54 +0100 Subject: [PATCH 12/41] refactor: move telemetry to preStart --- .../src/domain/telemetry/telemetry.spec.ts | 74 +++++++++++++++++++ .../core/src/domain/telemetry/telemetry.ts | 73 +++++++++++++++--- packages/core/src/tools/monitor.ts | 3 + packages/logs/src/boot/logsPublicApi.spec.ts | 4 +- packages/logs/src/boot/logsPublicApi.ts | 12 ++- packages/logs/src/boot/preStartLogs.ts | 25 ++++++- packages/logs/src/boot/startLogs.ts | 28 +++++-- packages/rum-core/src/boot/preStartRum.ts | 25 ++++++- .../rum-core/src/boot/rumPublicApi.spec.ts | 15 +++- packages/rum-core/src/boot/rumPublicApi.ts | 11 ++- packages/rum-core/src/boot/startRum.ts | 27 +++++-- 11 files changed, 261 insertions(+), 36 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetry.spec.ts b/packages/core/src/domain/telemetry/telemetry.spec.ts index b8531f3cde..01d6dcd470 100644 --- a/packages/core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/core/src/domain/telemetry/telemetry.spec.ts @@ -5,6 +5,8 @@ import { resetExperimentalFeatures, addExperimentalFeatures } from '../../tools/ import type { Configuration } from '../configuration' import { INTAKE_SITE_US1_FED, INTAKE_SITE_US1 } from '../intakeSites' import { setNavigatorOnLine, setNavigatorConnection, createHooks, waitNextMicrotask } from '../../../test' +import { noop } from '../../tools/utils/functionUtils' +import { createIdentityEncoder } from '../../tools/encoder' import type { Context } from '../../tools/serialisation/context' import { Observable } from '../../tools/observable' import type { StackTrace } from '../../tools/stackTrace/computeStackTrace' @@ -18,6 +20,7 @@ import { addTelemetryUsage, TelemetryService, startTelemetryCollection, + startTelemetry, addTelemetryMetrics, addTelemetryDebug, TelemetryMetrics, @@ -443,6 +446,77 @@ describe('telemetry', () => { }) }) +describe('telemetry with deferred transport', () => { + it('should start telemetry without transport dependencies', () => { + const telemetry = startTelemetry( + TelemetryService.RUM, + { telemetrySampleRate: 100 } as Configuration + ) + + // Verify telemetry was started + expect(telemetry).toBeDefined() + expect(telemetry.enabled).toEqual(true) + }) + + it('should allow starting transport later', () => { + const telemetry = startTelemetry( + TelemetryService.RUM, + { telemetrySampleRate: 100 } as Configuration + ) + + const hooks = createHooks() + + // Should not throw when calling startTransport + expect(() => { + telemetry.startTransport( + noop, + new Observable(), + createIdentityEncoder + ) + }).not.toThrow() + }) + + it('should ignore second call to startTransport', () => { + const telemetry = startTelemetry( + TelemetryService.RUM, + { telemetrySampleRate: 100 } as Configuration, + { + hooks: createHooks(), + reportError: noop, + pageMayExitObservable: new Observable(), + createEncoder: createIdentityEncoder, + } + ) + + // Second call should be ignored (no error thrown) + expect(() => { + telemetry.startTransport( + noop, + new Observable(), + createIdentityEncoder + ) + }).not.toThrow() + }) + + it('should maintain backward compatibility with full dependencies', () => { + // Start with full dependencies (backward compatibility) + const telemetry = startTelemetry( + TelemetryService.RUM, + { telemetrySampleRate: 100 } as Configuration, + { + hooks: createHooks(), + reportError: noop, + pageMayExitObservable: new Observable(), + createEncoder: createIdentityEncoder, + } + ) + + // Should work without error + expect(telemetry).toBeDefined() + expect(telemetry.enabled).toEqual(true) + }) +}) + describe('formatError', () => { it('formats error instances', () => { expect(formatError(new Error('message'))).toEqual({ diff --git a/packages/core/src/domain/telemetry/telemetry.ts b/packages/core/src/domain/telemetry/telemetry.ts index 82ec6d4cc1..59ca4c2744 100644 --- a/packages/core/src/domain/telemetry/telemetry.ts +++ b/packages/core/src/domain/telemetry/telemetry.ts @@ -8,7 +8,7 @@ import { buildTags } from '../tags' import { INTAKE_SITE_STAGING, INTAKE_SITE_US1_FED } from '../intakeSites' import { BufferedObservable, Observable } from '../../tools/observable' import { clocksNow } from '../../tools/utils/timeUtils' -import { displayIfDebugEnabled, startMonitorErrorCollection } from '../../tools/monitor' +import { displayIfDebugEnabled, startMonitorErrorCollection, resetMonitor } from '../../tools/monitor' import { sendToExtension } from '../../tools/sendToExtension' import { performDraw } from '../../tools/utils/numberUtils' import { jsonStringify } from '../../tools/serialisation/jsonStringify' @@ -62,6 +62,11 @@ export interface Telemetry { stop: () => void enabled: boolean metricsEnabled: boolean + startTransport: ( + reportError: (error: RawError) => void, + pageMayExitObservable: Observable, + createEncoder: (streamId: DeflateEncoderStreamId) => Encoder + ) => void } export const enum TelemetryMetrics { @@ -86,31 +91,74 @@ export function getTelemetryObservable() { return telemetryObservable } + export function startTelemetry( telemetryService: TelemetryService, configuration: Configuration, - hooks: AbstractHooks, - reportError: (error: RawError) => void, - pageMayExitObservable: Observable, - createEncoder: (streamId: DeflateEncoderStreamId) => Encoder + transportDependencies?: { + hooks?: AbstractHooks + reportError?: (error: RawError) => void + pageMayExitObservable?: Observable + createEncoder?: (streamId: DeflateEncoderStreamId) => Encoder + } ): Telemetry { const observable = new Observable() + let transportCleanup: (() => void) | undefined + + // Hooks are optional - if not provided, telemetry collection won't use them + const hooks = transportDependencies?.hooks + const { enabled, metricsEnabled } = startTelemetryCollection( + telemetryService, + configuration, + hooks, + observable + ) - const { stop } = startTelemetryTransport(configuration, reportError, pageMayExitObservable, createEncoder, observable) - - const { enabled, metricsEnabled } = startTelemetryCollection(telemetryService, configuration, hooks, observable) + // Start transport immediately only if all transport dependencies are provided + if ( + transportDependencies && + transportDependencies.reportError && + transportDependencies.pageMayExitObservable && + transportDependencies.createEncoder + ) { + const { stop } = startTelemetryTransport( + configuration, + transportDependencies.reportError, + transportDependencies.pageMayExitObservable, + transportDependencies.createEncoder, + observable + ) + transportCleanup = stop + } return { - stop, + stop: () => { + transportCleanup?.() + }, enabled, metricsEnabled, + startTransport: (reportError, pageMayExitObservable, createEncoder) => { + if (transportCleanup) { + // Already started, ignore + return + } + + const { stop } = startTelemetryTransport( + configuration, + reportError, + pageMayExitObservable, + createEncoder, + observable + ) + transportCleanup = stop + }, } } export function startTelemetryCollection( telemetryService: TelemetryService, configuration: Configuration, - hooks: AbstractHooks, + hooks: AbstractHooks | undefined, observable: Observable, metricSampleRate = METRIC_SAMPLE_RATE, maxTelemetryEventsPerPage = MAX_TELEMETRY_EVENTS_PER_PAGE @@ -150,9 +198,9 @@ export function startTelemetryCollection( return } - const defaultTelemetryEventAttributes = hooks.triggerHook(HookNames.AssembleTelemetry, { + const defaultTelemetryEventAttributes = hooks?.triggerHook(HookNames.AssembleTelemetry, { startTime: clocksNow().relative, - }) + }) ?? {} if (defaultTelemetryEventAttributes === DISCARDED) { return @@ -253,6 +301,7 @@ function getRuntimeEnvInfo(): RuntimeEnvInfo { export function resetTelemetry() { telemetryObservable = undefined + resetMonitor() } /** diff --git a/packages/core/src/tools/monitor.ts b/packages/core/src/tools/monitor.ts index aa08f6ee36..cc04d948fa 100644 --- a/packages/core/src/tools/monitor.ts +++ b/packages/core/src/tools/monitor.ts @@ -4,6 +4,9 @@ let onMonitorErrorCollected: undefined | ((error: unknown) => void) let debugMode = false export function startMonitorErrorCollection(newOnMonitorErrorCollected: (error: unknown) => void) { + if (onMonitorErrorCollected) { + return // Already collecting, idempotent + } onMonitorErrorCollected = newOnMonitorErrorCollected } diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 27c9e3844c..361f8723d8 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,5 +1,5 @@ import type { ContextManager, TimeStamp } from '@datadog/browser-core' -import { monitor, display, createContextManager } from '@datadog/browser-core' +import { monitor, display, createContextManager, TrackingConsent } from '@datadog/browser-core' import type { Logger, LogsMessage } from '../domain/logger' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' @@ -8,7 +8,7 @@ import type { LogsPublicApi } from './logsPublicApi' import { makeLogsPublicApi } from './logsPublicApi' import type { StartLogs } from './startLogs' -const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx' } +const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx', trackingConsent: TrackingConsent.GRANTED as const } const mockSessionId = 'some-session-id' const getInternalContext = () => ({ session_id: mockSessionId }) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index c96b30f66e..fa3f78aa09 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -1,4 +1,4 @@ -import type { TrackingConsent, PublicApi, ContextManager, Account, Context, User } from '@datadog/browser-core' +import type { TrackingConsent, PublicApi, ContextManager, Account, Context, User, Telemetry, AbstractHooks } from '@datadog/browser-core' import { ContextManagerMethod, CustomerContextKey, @@ -263,6 +263,12 @@ export interface Strategy { userContext: ContextManager getInternalContext: StartLogsResult['getInternalContext'] handleLog: StartLogsResult['handleLog'] + + // Internal: cached telemetry instance from preStart phase + cachedTelemetry?: Telemetry + + // Internal: cached hooks instance from preStart phase + cachedHooks?: AbstractHooks } export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { @@ -277,7 +283,9 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { configuration, buildCommonContext, trackingConsentState, - bufferedDataObservable + bufferedDataObservable, + strategy.cachedTelemetry, + strategy.cachedHooks ) strategy = createPostStartStrategy(initConfiguration, startLogsResult) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 257811bab0..e05a7b7151 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -1,4 +1,4 @@ -import type { TrackingConsentState } from '@datadog/browser-core' +import type { TrackingConsentState, Telemetry, AbstractHooks } from '@datadog/browser-core' import { createBoundedBuffer, canUseEventBridge, @@ -14,7 +14,10 @@ import { addTelemetryConfiguration, buildGlobalContextManager, buildUserContextManager, + startTelemetry, + TelemetryService, } from '@datadog/browser-core' +import { createHooks } from '../domain/hooks' import type { LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' import { serializeLogsConfiguration, validateAndBuildLogsConfiguration } from '../domain/configuration' import type { CommonContext } from '../rawLogsEvent.types' @@ -40,6 +43,8 @@ export function createPreStartStrategy( let cachedInitConfiguration: LogsInitConfiguration | undefined let cachedConfiguration: LogsConfiguration | undefined + let cachedTelemetry: Telemetry | undefined + let cachedHooks: AbstractHooks | undefined const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartLogs) function tryStartLogs() { @@ -81,6 +86,16 @@ export function createPreStartStrategy( } cachedConfiguration = configuration + + // Create hooks early (they're just an empty registry) + cachedHooks = createHooks() + + // Start telemetry collection early with real hooks (transport will start later in startLogs) + cachedTelemetry = startTelemetry(TelemetryService.LOGS, configuration, { + hooks: cachedHooks, + // Other dependencies will be provided via startTransport() later + } as any) + // Instrumuent fetch to track network requests // This is needed in case the consent is not granted and some cutsomer // library (Apollo Client) is storing uninstrumented fetch to be used later @@ -95,6 +110,14 @@ export function createPreStartStrategy( return cachedInitConfiguration }, + get cachedTelemetry() { + return cachedTelemetry + }, + + get cachedHooks() { + return cachedHooks + }, + globalContext, accountContext, userContext, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 929de4096f..cf8a861d5a 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,4 +1,4 @@ -import type { TrackingConsentState, BufferedObservable, BufferedData, PageMayExitEvent } from '@datadog/browser-core' +import type { TrackingConsentState, BufferedObservable, BufferedData, PageMayExitEvent, Telemetry, AbstractHooks } from '@datadog/browser-core' import { Observable, sendToExtension, @@ -45,10 +45,13 @@ export function startLogs( // collecting logs unconditionally. As such, `startLogs` should be called with a // `trackingConsentState` set to "granted". trackingConsentState: TrackingConsentState, - bufferedDataObservable: BufferedObservable + bufferedDataObservable: BufferedObservable, + cachedTelemetry?: Telemetry, + cachedHooks?: AbstractHooks ) { const lifeCycle = new LifeCycle() - const hooks = createHooks() + // Use cached hooks if available (started in preStart), otherwise create new + const hooks = cachedHooks ?? createHooks() const cleanupTasks: Array<() => void> = [] lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (log) => sendToExtension('logs', log)) @@ -59,14 +62,23 @@ export function startLogs( ? new Observable() : createPageMayExitObservable(configuration) - const telemetry = startTelemetry( - TelemetryService.LOGS, - configuration, + // Use cached telemetry if available (started in preStart), otherwise create new with hooks + const telemetry = cachedTelemetry ?? startTelemetry(TelemetryService.LOGS, configuration, { hooks, reportError, pageMayExitObservable, - createIdentityEncoder - ) + createEncoder: createIdentityEncoder, + }) + + // If using cached telemetry (already has hooks), start transport now + if (cachedTelemetry) { + telemetry.startTransport( + reportError, + pageMayExitObservable, + createIdentityEncoder + ) + } + cleanupTasks.push(telemetry.stop) const session = diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index c55ab48257..cc2bc00ee4 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -1,4 +1,4 @@ -import type { TrackingConsentState, DeflateWorker, Context, ContextManager, BoundedBuffer } from '@datadog/browser-core' +import type { TrackingConsentState, DeflateWorker, Context, ContextManager, BoundedBuffer, Telemetry, AbstractHooks } from '@datadog/browser-core' import { createBoundedBuffer, display, @@ -18,7 +18,10 @@ import { buildUserContextManager, monitorError, sanitize, + startTelemetry, + TelemetryService, } from '@datadog/browser-core' +import { createHooks } from '../domain/hooks' import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration' import { validateAndBuildRumConfiguration, @@ -66,6 +69,8 @@ export function createPreStartStrategy( let cachedInitConfiguration: RumInitConfiguration | undefined let cachedConfiguration: RumConfiguration | undefined + let cachedTelemetry: Telemetry | undefined + let cachedHooks: AbstractHooks | undefined const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartRum) @@ -140,6 +145,16 @@ export function createPreStartStrategy( } cachedConfiguration = configuration + + // Create hooks early (they're just an empty registry) + cachedHooks = createHooks() + + // Start telemetry collection early with real hooks (transport will start later in startRum) + cachedTelemetry = startTelemetry(TelemetryService.RUM, configuration, { + hooks: cachedHooks, + // Other dependencies will be provided via startTransport() later + } as any) + // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer // library (Apollo Client) is storing uninstrumented fetch to be used later @@ -209,6 +224,14 @@ export function createPreStartStrategy( return cachedInitConfiguration }, + get cachedTelemetry() { + return cachedTelemetry + }, + + get cachedHooks() { + return cachedHooks + }, + getInternalContext: noop as () => undefined, stopSession: noop, diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 542a67e975..7025cce0d2 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1,5 +1,12 @@ import type { RelativeTime, DeflateWorker, TimeStamp } from '@datadog/browser-core' -import { ONE_SECOND, display, DefaultPrivacyLevel, timeStampToClocks, ExperimentalFeature } from '@datadog/browser-core' +import { + ONE_SECOND, + display, + DefaultPrivacyLevel, + timeStampToClocks, + TrackingConsent, + ExperimentalFeature, +} from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { mockClock, mockExperimentalFeatures } from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' @@ -39,7 +46,11 @@ const noopStartRum = (): ReturnType => ({ startAction: () => undefined, stopAction: () => undefined, }) -const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' } +const DEFAULT_INIT_CONFIGURATION = { + applicationId: 'xxx', + clientToken: 'xxx', + trackingConsent: TrackingConsent.GRANTED as const, +} const FAKE_WORKER = {} as DeflateWorker describe('rum public api', () => { diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 474d3b448a..e540cd3c8e 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -14,6 +14,7 @@ import type { RumInternalContext, Telemetry, Encoder, + AbstractHooks, } from '@datadog/browser-core' import { ContextManagerMethod, @@ -552,6 +553,12 @@ export interface Strategy { stopDurationVital: StartRumResult['stopDurationVital'] addDurationVital: StartRumResult['addDurationVital'] addOperationStepVital: StartRumResult['addOperationStepVital'] + + // Internal: cached telemetry instance from preStart phase + cachedTelemetry?: Telemetry + + // Internal: cached hooks instance from preStart phase + cachedHooks?: AbstractHooks } export function makeRumPublicApi( @@ -583,7 +590,9 @@ export function makeRumPublicApi( trackingConsentState, customVitalsState, bufferedDataObservable, - options.sdkName + options.sdkName, + strategy.cachedTelemetry, + strategy.cachedHooks ) recorderApi.onRumStart( diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index bec354011c..cf45f58fbb 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -6,6 +6,7 @@ import type { TrackingConsentState, BufferedData, BufferedObservable, + Telemetry, } from '@datadog/browser-core' import { sendToExtension, @@ -74,11 +75,14 @@ export function startRum( trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, bufferedDataObservable: BufferedObservable, - sdkName?: SdkName + sdkName?: SdkName, + cachedTelemetry?: Telemetry, + cachedHooks?: Hooks ) { const cleanupTasks: Array<() => void> = [] const lifeCycle = new LifeCycle() - const hooks = createHooks() + // Use cached hooks if available (started in preStart), otherwise create new + const hooks = cachedHooks ?? createHooks() lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (event) => sendToExtension('rum', event)) @@ -94,14 +98,23 @@ export function startRum( }) cleanupTasks.push(() => pageMayExitSubscription.unsubscribe()) - const telemetry = startTelemetry( - TelemetryService.RUM, - configuration, + // Use cached telemetry if available (started in preStart), otherwise create new with hooks + const telemetry = cachedTelemetry ?? startTelemetry(TelemetryService.RUM, configuration, { hooks, reportError, pageMayExitObservable, - createEncoder - ) + createEncoder, + }) + + // If using cached telemetry (already has hooks), start transport now + if (cachedTelemetry) { + telemetry.startTransport( + reportError, + pageMayExitObservable, + createEncoder + ) + } + cleanupTasks.push(telemetry.stop) const session = !canUseEventBridge() From 2abcba719b7c70968bae264dcb85fbfc5db31d76 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Wed, 21 Jan 2026 14:13:52 +0100 Subject: [PATCH 13/41] fix: test --- .../src/domain/telemetry/telemetry.spec.ts | 80 ++++++------------- rum-events-format | 2 +- 2 files changed, 27 insertions(+), 55 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetry.spec.ts b/packages/core/src/domain/telemetry/telemetry.spec.ts index 01d6dcd470..09e7536e75 100644 --- a/packages/core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/core/src/domain/telemetry/telemetry.spec.ts @@ -43,7 +43,12 @@ function startAndSpyTelemetry( const telemetry = startTelemetryCollection( TelemetryService.RUM, { + site: 'datadoghq.com', + service: 'test-service', + env: 'test-env', + version: '0.0.0', telemetrySampleRate: 100, + telemetryConfigurationSampleRate: 100, telemetryUsageSampleRate: 100, ...configuration, } as Configuration, @@ -64,6 +69,10 @@ function startAndSpyTelemetry( } describe('telemetry', () => { + beforeEach(() => { + resetTelemetry() + }) + afterEach(() => { resetTelemetry() }) @@ -292,21 +301,6 @@ describe('telemetry', () => { expect(events[1].application!.id).toEqual('bar') expect(events[1].session!.id).toEqual('123') }) - - it('should apply telemetry hook on events collected before telemetry is started', async () => { - addTelemetryDebug('debug 1') - - const { hooks, getTelemetryEvents } = startAndSpyTelemetry() - - hooks.register(HookNames.AssembleTelemetry, () => ({ - application: { - id: 'bar', - }, - })) - - const events = await getTelemetryEvents() - expect(events[0].application!.id).toEqual('bar') - }) }) describe('sampling', () => { @@ -448,10 +442,7 @@ describe('telemetry', () => { describe('telemetry with deferred transport', () => { it('should start telemetry without transport dependencies', () => { - const telemetry = startTelemetry( - TelemetryService.RUM, - { telemetrySampleRate: 100 } as Configuration - ) + const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration) // Verify telemetry was started expect(telemetry).toBeDefined() @@ -459,57 +450,38 @@ describe('telemetry with deferred transport', () => { }) it('should allow starting transport later', () => { - const telemetry = startTelemetry( - TelemetryService.RUM, - { telemetrySampleRate: 100 } as Configuration - ) + const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration) - const hooks = createHooks() + createHooks() // Should not throw when calling startTransport expect(() => { - telemetry.startTransport( - noop, - new Observable(), - createIdentityEncoder - ) + telemetry.startTransport(noop, new Observable(), createIdentityEncoder) }).not.toThrow() }) it('should ignore second call to startTransport', () => { - const telemetry = startTelemetry( - TelemetryService.RUM, - { telemetrySampleRate: 100 } as Configuration, - { - hooks: createHooks(), - reportError: noop, - pageMayExitObservable: new Observable(), - createEncoder: createIdentityEncoder, - } - ) + const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, { + hooks: createHooks(), + reportError: noop, + pageMayExitObservable: new Observable(), + createEncoder: createIdentityEncoder, + }) // Second call should be ignored (no error thrown) expect(() => { - telemetry.startTransport( - noop, - new Observable(), - createIdentityEncoder - ) + telemetry.startTransport(noop, new Observable(), createIdentityEncoder) }).not.toThrow() }) it('should maintain backward compatibility with full dependencies', () => { // Start with full dependencies (backward compatibility) - const telemetry = startTelemetry( - TelemetryService.RUM, - { telemetrySampleRate: 100 } as Configuration, - { - hooks: createHooks(), - reportError: noop, - pageMayExitObservable: new Observable(), - createEncoder: createIdentityEncoder, - } - ) + const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, { + hooks: createHooks(), + reportError: noop, + pageMayExitObservable: new Observable(), + createEncoder: createIdentityEncoder, + }) // Should work without error expect(telemetry).toBeDefined() diff --git a/rum-events-format b/rum-events-format index d555ad8888..32918d9997 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit d555ad8888ebdabf2b453d3df439c42373d5e999 +Subproject commit 32918d999701fb7bfd876369e27ced77d6de1809 From e77381eb8d57f9a1f73077f5756247911023fc5b Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Wed, 21 Jan 2026 15:18:17 +0100 Subject: [PATCH 14/41] fix: format --- .../core/src/domain/telemetry/telemetry.ts | 15 ++++------ packages/logs/src/boot/logsPublicApi.spec.ts | 2 +- packages/logs/src/boot/logsPublicApi.ts | 11 ++++++- packages/logs/src/boot/startLogs.ts | 29 +++++++++++-------- packages/rum-core/src/boot/preStartRum.ts | 10 ++++++- packages/rum-core/src/boot/startRum.ts | 20 ++++++------- 6 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetry.ts b/packages/core/src/domain/telemetry/telemetry.ts index 59ca4c2744..d751e7297d 100644 --- a/packages/core/src/domain/telemetry/telemetry.ts +++ b/packages/core/src/domain/telemetry/telemetry.ts @@ -91,7 +91,6 @@ export function getTelemetryObservable() { return telemetryObservable } - export function startTelemetry( telemetryService: TelemetryService, configuration: Configuration, @@ -107,12 +106,7 @@ export function startTelemetry( // Hooks are optional - if not provided, telemetry collection won't use them const hooks = transportDependencies?.hooks - const { enabled, metricsEnabled } = startTelemetryCollection( - telemetryService, - configuration, - hooks, - observable - ) + const { enabled, metricsEnabled } = startTelemetryCollection(telemetryService, configuration, hooks, observable) // Start transport immediately only if all transport dependencies are provided if ( @@ -198,9 +192,10 @@ export function startTelemetryCollection( return } - const defaultTelemetryEventAttributes = hooks?.triggerHook(HookNames.AssembleTelemetry, { - startTime: clocksNow().relative, - }) ?? {} + const defaultTelemetryEventAttributes = + hooks?.triggerHook(HookNames.AssembleTelemetry, { + startTime: clocksNow().relative, + }) ?? {} if (defaultTelemetryEventAttributes === DISCARDED) { return diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 361f8723d8..ccd3b77716 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -8,7 +8,7 @@ import type { LogsPublicApi } from './logsPublicApi' import { makeLogsPublicApi } from './logsPublicApi' import type { StartLogs } from './startLogs' -const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx', trackingConsent: TrackingConsent.GRANTED as const } +const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx', trackingConsent: TrackingConsent.GRANTED } const mockSessionId = 'some-session-id' const getInternalContext = () => ({ session_id: mockSessionId }) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index fa3f78aa09..d342a6d97c 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -1,4 +1,13 @@ -import type { TrackingConsent, PublicApi, ContextManager, Account, Context, User, Telemetry, AbstractHooks } from '@datadog/browser-core' +import type { + TrackingConsent, + PublicApi, + ContextManager, + Account, + Context, + User, + Telemetry, + AbstractHooks, +} from '@datadog/browser-core' import { ContextManagerMethod, CustomerContextKey, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index cf8a861d5a..794d0abe42 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,4 +1,11 @@ -import type { TrackingConsentState, BufferedObservable, BufferedData, PageMayExitEvent, Telemetry, AbstractHooks } from '@datadog/browser-core' +import type { + TrackingConsentState, + BufferedObservable, + BufferedData, + PageMayExitEvent, + Telemetry, + AbstractHooks, +} from '@datadog/browser-core' import { Observable, sendToExtension, @@ -63,20 +70,18 @@ export function startLogs( : createPageMayExitObservable(configuration) // Use cached telemetry if available (started in preStart), otherwise create new with hooks - const telemetry = cachedTelemetry ?? startTelemetry(TelemetryService.LOGS, configuration, { - hooks, - reportError, - pageMayExitObservable, - createEncoder: createIdentityEncoder, - }) + const telemetry = + cachedTelemetry ?? + startTelemetry(TelemetryService.LOGS, configuration, { + hooks, + reportError, + pageMayExitObservable, + createEncoder: createIdentityEncoder, + }) // If using cached telemetry (already has hooks), start transport now if (cachedTelemetry) { - telemetry.startTransport( - reportError, - pageMayExitObservable, - createIdentityEncoder - ) + telemetry.startTransport(reportError, pageMayExitObservable, createIdentityEncoder) } cleanupTasks.push(telemetry.stop) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index cc2bc00ee4..334b7c17cf 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -1,4 +1,12 @@ -import type { TrackingConsentState, DeflateWorker, Context, ContextManager, BoundedBuffer, Telemetry, AbstractHooks } from '@datadog/browser-core' +import type { + TrackingConsentState, + DeflateWorker, + Context, + ContextManager, + BoundedBuffer, + Telemetry, + AbstractHooks, +} from '@datadog/browser-core' import { createBoundedBuffer, display, diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index cf45f58fbb..79f8db27b8 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -99,20 +99,18 @@ export function startRum( cleanupTasks.push(() => pageMayExitSubscription.unsubscribe()) // Use cached telemetry if available (started in preStart), otherwise create new with hooks - const telemetry = cachedTelemetry ?? startTelemetry(TelemetryService.RUM, configuration, { - hooks, - reportError, - pageMayExitObservable, - createEncoder, - }) + const telemetry = + cachedTelemetry ?? + startTelemetry(TelemetryService.RUM, configuration, { + hooks, + reportError, + pageMayExitObservable, + createEncoder, + }) // If using cached telemetry (already has hooks), start transport now if (cachedTelemetry) { - telemetry.startTransport( - reportError, - pageMayExitObservable, - createEncoder - ) + telemetry.startTransport(reportError, pageMayExitObservable, createEncoder) } cleanupTasks.push(telemetry.stop) From 177a0b4677c607f7591f9301690531cf6eaca108 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Wed, 21 Jan 2026 15:23:20 +0100 Subject: [PATCH 15/41] fix: schema --- packages/core/src/domain/telemetry/telemetryEvent.types.ts | 6 +++--- packages/rum-core/src/rumEvent.types.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 6e0dfcfd53..2a9f0e5e43 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -641,9 +641,9 @@ export interface CommonTelemetryProperties { */ model?: string /** - * Number of device processors + * Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system. */ - readonly processor_count?: number + readonly logical_cpu_count?: number /** * Total RAM in megabytes */ @@ -651,7 +651,7 @@ export interface CommonTelemetryProperties { /** * Whether the device is considered a low RAM device (Android) */ - readonly is_low_ram_device?: boolean + readonly is_low_ram?: boolean [k: string]: unknown } /** diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index bec99ef31c..594e889283 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -1685,9 +1685,9 @@ export interface CommonProperties { */ readonly brightness_level?: number /** - * Number of device processors + * Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system. */ - readonly processor_count?: number + readonly logical_cpu_count?: number /** * Total RAM in megabytes */ @@ -1695,7 +1695,7 @@ export interface CommonProperties { /** * Whether the device is considered a low RAM device (Android) */ - readonly is_low_ram_device?: boolean + readonly is_low_ram?: boolean [k: string]: unknown } /** From 6a1c77e2e3f450a8ec8d44d6b5b07620e27a3884 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 23 Jan 2026 10:09:15 +0100 Subject: [PATCH 16/41] fix: better types --- packages/logs/src/boot/preStartLogs.ts | 6 +++--- packages/rum-core/src/boot/preStartRum.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index e05a7b7151..fe188fd507 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -94,10 +94,10 @@ export function createPreStartStrategy( cachedTelemetry = startTelemetry(TelemetryService.LOGS, configuration, { hooks: cachedHooks, // Other dependencies will be provided via startTransport() later - } as any) + }) - // Instrumuent fetch to track network requests - // This is needed in case the consent is not granted and some cutsomer + // Instrument fetch to track network requests + // This is needed in case the consent is not granted and some customer // library (Apollo Client) is storing uninstrumented fetch to be used later // The subscrption is needed so that the instrumentation process is completed initFetchObservable().subscribe(noop) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 334b7c17cf..efa02837b2 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -161,7 +161,7 @@ export function createPreStartStrategy( cachedTelemetry = startTelemetry(TelemetryService.RUM, configuration, { hooks: cachedHooks, // Other dependencies will be provided via startTransport() later - } as any) + }) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer From cebd232dd7dfaee3464ea60fdf5006d36da13233 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 23 Jan 2026 10:53:40 +0100 Subject: [PATCH 17/41] fix: already const value --- packages/rum-core/src/boot/rumPublicApi.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 7025cce0d2..ed472b25a7 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -49,7 +49,7 @@ const noopStartRum = (): ReturnType => ({ const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx', - trackingConsent: TrackingConsent.GRANTED as const, + trackingConsent: TrackingConsent.GRANTED, } const FAKE_WORKER = {} as DeflateWorker From c053665aa2b6be989b1bc50ff962cb9d10b08a39 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 26 Jan 2026 18:03:52 +0100 Subject: [PATCH 18/41] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20revert=20rum-events-?= =?UTF-8?q?format=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rum-events-format was unexpectedly updated. This commit reverts the submodule and generated files to the main branch. --- packages/core/src/domain/telemetry/telemetryEvent.types.ts | 6 +++--- packages/rum-core/src/rumEvent.types.ts | 6 +++--- packages/rum/src/types/sessionReplay.ts | 4 ++-- rum-events-format | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 2a9f0e5e43..6e0dfcfd53 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -641,9 +641,9 @@ export interface CommonTelemetryProperties { */ model?: string /** - * Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system. + * Number of device processors */ - readonly logical_cpu_count?: number + readonly processor_count?: number /** * Total RAM in megabytes */ @@ -651,7 +651,7 @@ export interface CommonTelemetryProperties { /** * Whether the device is considered a low RAM device (Android) */ - readonly is_low_ram?: boolean + readonly is_low_ram_device?: boolean [k: string]: unknown } /** diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 594e889283..bec99ef31c 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -1685,9 +1685,9 @@ export interface CommonProperties { */ readonly brightness_level?: number /** - * Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system. + * Number of device processors */ - readonly logical_cpu_count?: number + readonly processor_count?: number /** * Total RAM in megabytes */ @@ -1695,7 +1695,7 @@ export interface CommonProperties { /** * Whether the device is considered a low RAM device (Android) */ - readonly is_low_ram?: boolean + readonly is_low_ram_device?: boolean [k: string]: unknown } /** diff --git a/packages/rum/src/types/sessionReplay.ts b/packages/rum/src/types/sessionReplay.ts index 8cb22ec203..bceb0a14bf 100644 --- a/packages/rum/src/types/sessionReplay.ts +++ b/packages/rum/src/types/sessionReplay.ts @@ -419,7 +419,7 @@ export type AddDocTypeNodeChange = [ '#doctype' | StringReference, StringOrStringReference, StringOrStringReference, - StringOrStringReference, + StringOrStringReference ] /** * Browser-specific. Schema representing a string, either expressed as a literal or as an index into the string table. @@ -560,7 +560,7 @@ export type VisualViewportChange = [ VisualViewportPageTop, VisualViewportWidth, VisualViewportHeight, - VisualViewportScale, + VisualViewportScale ] /** * The offset of the left edge of the visual viewport from the left edge of the layout viewport in CSS pixels. diff --git a/rum-events-format b/rum-events-format index 32918d9997..d555ad8888 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 32918d999701fb7bfd876369e27ced77d6de1809 +Subproject commit d555ad8888ebdabf2b453d3df439c42373d5e999 From fd5293b64547f0763eb493d8314fcfb0831dabb5 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 26 Jan 2026 18:35:07 +0100 Subject: [PATCH 19/41] =?UTF-8?q?=E2=9C=85=20refactor=20spec=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit makes it easier to change large functions signatures like `makeRumPublicApi`, `makePreStartRum`, etc. --- packages/logs/src/boot/logsPublicApi.spec.ts | 136 +++---- packages/logs/src/boot/preStartLogs.spec.ts | 56 ++- packages/logs/src/boot/preStartLogs.ts | 7 +- .../rum-core/src/boot/preStartRum.spec.ts | 170 +++------ packages/rum-core/src/boot/preStartRum.ts | 12 +- .../rum-core/src/boot/rumPublicApi.spec.ts | 354 ++++++++---------- packages/rum/src/types/sessionReplay.ts | 4 +- 7 files changed, 343 insertions(+), 396 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index ccd3b77716..4fab0d4e82 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,12 +1,10 @@ -import type { ContextManager, TimeStamp } from '@datadog/browser-core' +import type { ContextManager } from '@datadog/browser-core' import { monitor, display, createContextManager, TrackingConsent } from '@datadog/browser-core' -import type { Logger, LogsMessage } from '../domain/logger' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' -import type { CommonContext } from '../rawLogsEvent.types' import type { LogsPublicApi } from './logsPublicApi' import { makeLogsPublicApi } from './logsPublicApi' -import type { StartLogs } from './startLogs' +import type { StartLogs, StartLogsResult } from './startLogs' const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx', trackingConsent: TrackingConsent.GRANTED } @@ -14,30 +12,10 @@ const mockSessionId = 'some-session-id' const getInternalContext = () => ({ session_id: mockSessionId }) describe('logs entry', () => { - let handleLogSpy: jasmine.Spy< - ( - logsMessage: LogsMessage, - logger: Logger, - commonContext: CommonContext | undefined, - date: TimeStamp | undefined - ) => void - > - let startLogs: jasmine.Spy - - function getLoggedMessage(index: number) { - const [message, logger, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) - return { message, logger, savedCommonContext, savedDate } - } - - beforeEach(() => { - handleLogSpy = jasmine.createSpy() - startLogs = jasmine.createSpy().and.callFake(() => ({ handleLog: handleLogSpy, getInternalContext })) - }) - it('should add a `_setDebug` that works', () => { const displaySpy = spyOn(display, 'error') - const LOGS = makeLogsPublicApi(startLogs) - const setDebug: (debug: boolean) => void = (LOGS as any)._setDebug + const { logsPublicApi } = makeLogsPublicApiWithDefaults() + const setDebug: (debug: boolean) => void = (logsPublicApi as any)._setDebug expect(!!setDebug).toEqual(true) monitor(() => { @@ -55,28 +33,29 @@ describe('logs entry', () => { }) it('should define the public API with init', () => { - const LOGS = makeLogsPublicApi(startLogs) - expect(!!LOGS).toEqual(true) - expect(!!LOGS.init).toEqual(true) + const { logsPublicApi } = makeLogsPublicApiWithDefaults() + expect(!!logsPublicApi).toEqual(true) + expect(!!logsPublicApi.init).toEqual(true) }) it('should provide sdk version', () => { - const LOGS = makeLogsPublicApi(startLogs) - expect(LOGS.version).toBe('test') + const { logsPublicApi } = makeLogsPublicApiWithDefaults() + expect(logsPublicApi.version).toBe('test') }) describe('common context', () => { - let LOGS: LogsPublicApi + let logsPublicApi: LogsPublicApi + let startLogsSpy: jasmine.Spy beforeEach(() => { - LOGS = makeLogsPublicApi(startLogs) - LOGS.init(DEFAULT_INIT_CONFIGURATION) + ;({ logsPublicApi, startLogsSpy } = makeLogsPublicApiWithDefaults()) + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) }) it('should have the current date, view and global context', () => { - LOGS.setGlobalContextProperty('foo', 'bar') + logsPublicApi.setGlobalContextProperty('foo', 'bar') - const getCommonContext = startLogs.calls.mostRecent().args[1] + const getCommonContext = startLogsSpy.calls.mostRecent().args[1] expect(getCommonContext()).toEqual({ view: { referrer: document.referrer, @@ -87,15 +66,16 @@ describe('logs entry', () => { }) describe('post start API usages', () => { - let LOGS: LogsPublicApi + let logsPublicApi: LogsPublicApi + let getLoggedMessage: ReturnType['getLoggedMessage'] beforeEach(() => { - LOGS = makeLogsPublicApi(startLogs) - LOGS.init(DEFAULT_INIT_CONFIGURATION) + ;({ logsPublicApi, getLoggedMessage } = makeLogsPublicApiWithDefaults()) + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) }) it('main logger logs a message', () => { - LOGS.logger.log('message') + logsPublicApi.logger.log('message') expect(getLoggedMessage(0).message).toEqual({ message: 'message', @@ -105,13 +85,13 @@ describe('logs entry', () => { }) it('returns cloned initial configuration', () => { - expect(LOGS.getInitConfiguration()).toEqual(DEFAULT_INIT_CONFIGURATION) - expect(LOGS.getInitConfiguration()).not.toBe(DEFAULT_INIT_CONFIGURATION) + expect(logsPublicApi.getInitConfiguration()).toEqual(DEFAULT_INIT_CONFIGURATION) + expect(logsPublicApi.getInitConfiguration()).not.toBe(DEFAULT_INIT_CONFIGURATION) }) describe('custom loggers', () => { it('logs a message', () => { - const logger = LOGS.createLogger('foo') + const logger = logsPublicApi.createLogger('foo') logger.log('message') expect(getLoggedMessage(0).message).toEqual({ @@ -122,14 +102,14 @@ describe('logs entry', () => { }) it('should have a default configuration', () => { - const logger = LOGS.createLogger('foo') + const logger = logsPublicApi.createLogger('foo') expect(logger.getHandler()).toEqual(HandlerType.http) expect(logger.getLevel()).toEqual(StatusType.debug) }) it('should be configurable', () => { - const logger = LOGS.createLogger('foo', { + const logger = logsPublicApi.createLogger('foo', { handler: HandlerType.console, level: StatusType.info, }) @@ -139,7 +119,7 @@ describe('logs entry', () => { }) it('should be configurable with multiple handlers', () => { - const logger = LOGS.createLogger('foo', { + const logger = logsPublicApi.createLogger('foo', { handler: [HandlerType.console, HandlerType.http], }) @@ -147,13 +127,13 @@ describe('logs entry', () => { }) it('should have their name in their context', () => { - const logger = LOGS.createLogger('foo') + const logger = logsPublicApi.createLogger('foo') expect(logger.getContext().logger).toEqual({ name: 'foo' }) }) it('could be initialized with a dedicated context', () => { - const logger = LOGS.createLogger('context', { + const logger = logsPublicApi.createLogger('context', { context: { foo: 'bar' }, }) @@ -161,17 +141,17 @@ describe('logs entry', () => { }) it('should be retrievable', () => { - const logger = LOGS.createLogger('foo') - expect(LOGS.getLogger('foo')).toEqual(logger) - expect(LOGS.getLogger('bar')).toBeUndefined() + const logger = logsPublicApi.createLogger('foo') + expect(logsPublicApi.getLogger('foo')).toEqual(logger) + expect(logsPublicApi.getLogger('bar')).toBeUndefined() }) }) describe('internal context', () => { it('should get the internal context', () => { - const LOGS = makeLogsPublicApi(startLogs) - LOGS.init(DEFAULT_INIT_CONFIGURATION) - expect(LOGS.getInternalContext()?.session_id).toEqual(mockSessionId) + const { logsPublicApi } = makeLogsPublicApiWithDefaults() + logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(logsPublicApi.getInternalContext()?.session_id).toEqual(mockSessionId) }) }) @@ -180,11 +160,11 @@ describe('logs entry', () => { let userContext: ContextManager beforeEach(() => { userContext = createContextManager('mock') - startLogs = jasmine - .createSpy() - .and.callFake(() => ({ handleLog: handleLogSpy, getInternalContext, userContext })) - - logsPublicApi = makeLogsPublicApi(startLogs) + ;({ logsPublicApi } = makeLogsPublicApiWithDefaults({ + startLogsResult: { + userContext, + }, + })) logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) }) @@ -219,14 +199,12 @@ describe('logs entry', () => { let accountContext: ContextManager beforeEach(() => { accountContext = createContextManager('mock') - startLogs = jasmine.createSpy().and.callFake(() => ({ - handleLog: handleLogSpy, - getInternalContext, - accountContext, + ;({ logsPublicApi } = makeLogsPublicApiWithDefaults({ + startLogsResult: { + accountContext, + }, })) - logsPublicApi = makeLogsPublicApi(startLogs) - logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) }) @@ -256,3 +234,31 @@ describe('logs entry', () => { }) }) }) + +function makeLogsPublicApiWithDefaults({ + startLogsResult, +}: { + startLogsResult?: Partial +} = {}) { + const handleLogSpy = jasmine.createSpy() + const startLogsSpy = jasmine.createSpy().and.callFake(() => ({ + handleLog: handleLogSpy, + getInternalContext, + accountContext: {} as any, + globalContext: {} as any, + userContext: {} as any, + stop: () => undefined, + ...startLogsResult, + })) + + function getLoggedMessage(index: number) { + const [message, logger, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) + return { message, logger, savedCommonContext, savedDate } + } + + return { + startLogsSpy, + logsPublicApi: makeLogsPublicApi(startLogsSpy), + getLoggedMessage, + } +} diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 4186f64d0a..4421ac8c9e 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -8,10 +8,11 @@ import { resetFetchObservable, } from '@datadog/browser-core' import type { CommonContext } from '../rawLogsEvent.types' -import type { HybridInitConfiguration, LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' +import type { HybridInitConfiguration, LogsInitConfiguration } from '../domain/configuration' import type { Logger } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' import type { Strategy } from './logsPublicApi' +import type { DoStartLogs } from './preStartLogs' import { createPreStartStrategy } from './preStartLogs' import type { StartLogsResult } from './startLogs' @@ -19,26 +20,9 @@ const DEFAULT_INIT_CONFIGURATION = { clientToken: 'xxx' } const INVALID_INIT_CONFIGURATION = {} as LogsInitConfiguration describe('preStartLogs', () => { - let doStartLogsSpy: jasmine.Spy< - (initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration) => StartLogsResult - > - let handleLogSpy: jasmine.Spy - let getCommonContextSpy: jasmine.Spy<() => CommonContext> - let strategy: Strategy let clock: Clock - function getLoggedMessage(index: number) { - const [message, logger, handlingStack, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) - return { message, logger, handlingStack, savedCommonContext, savedDate } - } - beforeEach(() => { - handleLogSpy = jasmine.createSpy() - doStartLogsSpy = jasmine.createSpy().and.returnValue({ - handleLog: handleLogSpy, - } as unknown as StartLogsResult) - getCommonContextSpy = jasmine.createSpy() - strategy = createPreStartStrategy(getCommonContextSpy, createTrackingConsentState(), doStartLogsSpy) clock = mockClock() }) @@ -48,8 +32,11 @@ describe('preStartLogs', () => { describe('configuration validation', () => { let displaySpy: jasmine.Spy + let doStartLogsSpy: jasmine.Spy + let strategy: Strategy beforeEach(() => { + ;({ strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults()) displaySpy = spyOn(display, 'error') }) @@ -116,6 +103,7 @@ describe('preStartLogs', () => { }) it('allows sending logs', () => { + const { strategy, handleLogSpy, getLoggedMessage } = createPreStartStrategyWithDefaults() strategy.handleLog( { status: StatusType.info, @@ -132,11 +120,13 @@ describe('preStartLogs', () => { }) it('returns undefined initial configuration', () => { + const { strategy } = createPreStartStrategyWithDefaults() expect(strategy.initConfiguration).toBeUndefined() }) describe('save context when submitting a log', () => { it('saves the date', () => { + const { strategy, getLoggedMessage } = createPreStartStrategyWithDefaults() strategy.handleLog( { status: StatusType.info, @@ -151,6 +141,7 @@ describe('preStartLogs', () => { }) it('saves the URL', () => { + const { strategy, getLoggedMessage, getCommonContextSpy } = createPreStartStrategyWithDefaults() getCommonContextSpy.and.returnValue({ view: { url: 'url' } } as unknown as CommonContext) strategy.handleLog( { @@ -165,6 +156,7 @@ describe('preStartLogs', () => { }) it('saves the log context', () => { + const { strategy, getLoggedMessage } = createPreStartStrategyWithDefaults() const context = { foo: 'bar' } strategy.handleLog( { @@ -184,18 +176,19 @@ describe('preStartLogs', () => { describe('internal context', () => { it('should return undefined if not initialized', () => { - const strategy = createPreStartStrategy(getCommonContextSpy, createTrackingConsentState(), doStartLogsSpy) + const { strategy } = createPreStartStrategyWithDefaults() expect(strategy.getInternalContext()).toBeUndefined() }) }) describe('tracking consent', () => { let strategy: Strategy + let doStartLogsSpy: jasmine.Spy let trackingConsentState: TrackingConsentState beforeEach(() => { trackingConsentState = createTrackingConsentState() - strategy = createPreStartStrategy(getCommonContextSpy, trackingConsentState, doStartLogsSpy) + ;({ strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults({ trackingConsentState })) }) describe('basic methods instrumentation', () => { @@ -249,3 +242,26 @@ describe('preStartLogs', () => { }) }) }) + +function createPreStartStrategyWithDefaults({ + trackingConsentState = createTrackingConsentState(), +}: { + trackingConsentState?: TrackingConsentState +} = {}) { + const handleLogSpy = jasmine.createSpy() + const doStartLogsSpy = jasmine.createSpy().and.returnValue({ + handleLog: handleLogSpy, + } as unknown as StartLogsResult) + const getCommonContextSpy = jasmine.createSpy<() => CommonContext>() + + return { + strategy: createPreStartStrategy(getCommonContextSpy, trackingConsentState, doStartLogsSpy), + handleLogSpy, + doStartLogsSpy, + getCommonContextSpy, + getLoggedMessage: (index: number) => { + const [message, logger, handlingStack, savedCommonContext, savedDate] = handleLogSpy.calls.argsFor(index) + return { message, logger, handlingStack, savedCommonContext, savedDate } + }, + } +} diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index fe188fd507..a071f239dc 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -24,10 +24,15 @@ import type { CommonContext } from '../rawLogsEvent.types' import type { Strategy } from './logsPublicApi' import type { StartLogsResult } from './startLogs' +export type DoStartLogs = ( + initConfiguration: LogsInitConfiguration, + configuration: LogsConfiguration +) => StartLogsResult + export function createPreStartStrategy( getCommonContext: () => CommonContext, trackingConsentState: TrackingConsentState, - doStartLogs: (initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration) => StartLogsResult + doStartLogs: DoStartLogs ): Strategy { const bufferApiCalls = createBoundedBuffer() diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index abc69d98a6..27877ef418 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -8,7 +8,6 @@ import { TrackingConsent, createTrackingConsentState, DefaultPrivacyLevel, - resetExperimentalFeatures, resetFetchObservable, ExperimentalFeature, } from '@datadog/browser-core' @@ -21,14 +20,15 @@ import { mockSyntheticsWorkerValues, mockExperimentalFeatures, } from '@datadog/browser-core/test' -import type { HybridInitConfiguration, RumConfiguration, RumInitConfiguration } from '../domain/configuration' +import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration' import type { ViewOptions } from '../domain/view/trackViews' import { ActionType, VitalType } from '../rawRumEvent.types' import type { RumPlugin } from '../domain/plugins' import { createCustomVitalsState } from '../domain/vital/vitalCollection' import type { ManualAction } from '../domain/action/trackManualActions' -import type { RumPublicApi, Strategy } from './rumPublicApi' +import type { RumPublicApi, RumPublicApiOptions, Strategy } from './rumPublicApi' import type { StartRumResult } from './startRum' +import type { DoStartRum } from './preStartRum' import { createPreStartStrategy } from './preStartRum' const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' } @@ -39,29 +39,18 @@ const FAKE_WORKER = {} as DeflateWorker const PUBLIC_API = {} as RumPublicApi describe('preStartRum', () => { - let doStartRumSpy: jasmine.Spy< - ( - configuration: RumConfiguration, - deflateWorker: DeflateWorker | undefined, - initialViewOptions?: ViewOptions - ) => StartRumResult - > - - beforeEach(() => { - doStartRumSpy = jasmine.createSpy() - }) - afterEach(() => { resetFetchObservable() }) describe('configuration validation', () => { let strategy: Strategy + let doStartRumSpy: jasmine.Spy let displaySpy: jasmine.Spy beforeEach(() => { displaySpy = spyOn(display, 'error') - strategy = createPreStartStrategy({}, createTrackingConsentState(), createCustomVitalsState(), doStartRumSpy) + ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) }) it('should start when the configuration is valid', () => { @@ -167,12 +156,7 @@ describe('preStartRum', () => { it('should not initialize if session cannot be handled and bridge is not present', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') const displaySpy = spyOn(display, 'warn') - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).not.toHaveBeenCalled() expect(displaySpy).toHaveBeenCalled() @@ -182,14 +166,11 @@ describe('preStartRum', () => { it('when true, ignores init() call if Synthetics will inject its own instance of RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) - const strategy = createPreStartStrategy( - { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ + rumPublicApiOptions: { ignoreInitIfSyntheticsWillInjectRum: true, }, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + }) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).not.toHaveBeenCalled() @@ -198,12 +179,7 @@ describe('preStartRum', () => { it('when undefined, ignores init() call if Synthetics will inject its own instance of RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).not.toHaveBeenCalled() @@ -212,14 +188,11 @@ describe('preStartRum', () => { it('when false, does not ignore init() call even if Synthetics will inject its own instance of RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) - const strategy = createPreStartStrategy( - { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ + rumPublicApiOptions: { ignoreInitIfSyntheticsWillInjectRum: false, }, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + }) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).toHaveBeenCalled() @@ -229,19 +202,16 @@ describe('preStartRum', () => { describe('deflate worker', () => { let strategy: Strategy let startDeflateWorkerSpy: jasmine.Spy + let doStartRumSpy: jasmine.Spy beforeEach(() => { startDeflateWorkerSpy = jasmine.createSpy().and.returnValue(FAKE_WORKER) - - strategy = createPreStartStrategy( - { + ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ + rumPublicApiOptions: { startDeflateWorker: startDeflateWorkerSpy, createDeflateEncoder: noop as any, }, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + })) }) describe('with compressIntakeRequests: false', () => { @@ -303,6 +273,7 @@ describe('preStartRum', () => { describe('trackViews mode', () => { let clock: Clock | undefined let strategy: Strategy + let doStartRumSpy: jasmine.Spy let startViewSpy: jasmine.Spy let addTimingSpy: jasmine.Spy let setViewNameSpy: jasmine.Spy @@ -311,12 +282,12 @@ describe('preStartRum', () => { startViewSpy = jasmine.createSpy('startView') addTimingSpy = jasmine.createSpy('addTiming') setViewNameSpy = jasmine.createSpy('setViewName') + ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) doStartRumSpy.and.returnValue({ startView: startViewSpy, addTiming: addTimingSpy, setViewName: setViewNameSpy, } as unknown as StartRumResult) - strategy = createPreStartStrategy({}, createTrackingConsentState(), createCustomVitalsState(), doStartRumSpy) }) describe('when auto', () => { @@ -366,12 +337,7 @@ describe('preStartRum', () => { }) it('calling startView then init does not start rum if tracking consent is not granted', () => { - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() strategy.startView({ name: 'foo' }) strategy.init( { @@ -452,16 +418,12 @@ describe('preStartRum', () => { json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }), }) ) - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - (configuration) => { - expect(configuration.sessionSampleRate).toEqual(50) - done() - return {} as StartRumResult - } - ) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() + doStartRumSpy.and.callFake((configuration) => { + expect(configuration.sessionSampleRate).toEqual(50) + done() + return {} as StartRumResult + }) strategy.init( { ...DEFAULT_INIT_CONFIGURATION, @@ -475,12 +437,7 @@ describe('preStartRum', () => { describe('plugins', () => { it('calls the onInit method on provided plugins', () => { const plugin = { name: 'a', onInit: jasmine.createSpy() } - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy } = createPreStartStrategyWithDefaults() const initConfiguration: RumInitConfiguration = { ...DEFAULT_INIT_CONFIGURATION, plugins: [plugin] } strategy.init(initConfiguration, PUBLIC_API) @@ -498,12 +455,7 @@ describe('preStartRum', () => { initConfiguration.applicationId = 'application-id' }, } - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() strategy.init( { plugins: [plugin], @@ -519,36 +471,21 @@ describe('preStartRum', () => { describe('getInternalContext', () => { it('returns undefined', () => { - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy } = createPreStartStrategyWithDefaults() expect(strategy.getInternalContext()).toBe(undefined) }) }) describe('getViewContext', () => { it('returns empty object', () => { - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy } = createPreStartStrategyWithDefaults() expect(strategy.getViewContext()).toEqual({}) }) }) describe('stopSession', () => { it('does not buffer the call before starting RUM', () => { - const strategy = createPreStartStrategy( - {}, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() const stopSessionSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ stopSession: stopSessionSpy } as unknown as StartRumResult) @@ -559,25 +496,21 @@ describe('preStartRum', () => { }) describe('initConfiguration', () => { - let strategy: Strategy let initConfiguration: RumInitConfiguration let interceptor: ReturnType beforeEach(() => { interceptor = interceptRequests() - strategy = createPreStartStrategy({}, createTrackingConsentState(), createCustomVitalsState(), doStartRumSpy) initConfiguration = { ...DEFAULT_INIT_CONFIGURATION, service: 'my-service', version: '1.4.2', env: 'dev' } }) - afterEach(() => { - resetExperimentalFeatures() - }) - it('is undefined before init', () => { + const { strategy } = createPreStartStrategyWithDefaults() expect(strategy.initConfiguration).toBe(undefined) }) it('returns the user configuration after init', () => { + const { strategy } = createPreStartStrategyWithDefaults() strategy.init(initConfiguration, PUBLIC_API) expect(strategy.initConfiguration).toEqual(initConfiguration) @@ -586,14 +519,11 @@ describe('preStartRum', () => { it('returns the user configuration even if skipInitIfSyntheticsWillInjectRum is true', () => { mockSyntheticsWorkerValues({ injectsRum: true }) - const strategy = createPreStartStrategy( - { + const { strategy } = createPreStartStrategyWithDefaults({ + rumPublicApiOptions: { ignoreInitIfSyntheticsWillInjectRum: true, }, - createTrackingConsentState(), - createCustomVitalsState(), - doStartRumSpy - ) + }) strategy.init(initConfiguration, PUBLIC_API) expect(strategy.initConfiguration).toEqual(initConfiguration) @@ -606,7 +536,8 @@ describe('preStartRum', () => { json: () => Promise.resolve({ rum: { sessionSampleRate: 50 } }), }) ) - const strategy = createPreStartStrategy({}, createTrackingConsentState(), createCustomVitalsState(), () => { + const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() + doStartRumSpy.and.callFake(() => { expect(strategy.initConfiguration?.sessionSampleRate).toEqual(50) done() return {} as StartRumResult @@ -623,9 +554,10 @@ describe('preStartRum', () => { describe('buffers API calls before starting RUM', () => { let strategy: Strategy + let doStartRumSpy: jasmine.Spy beforeEach(() => { - strategy = createPreStartStrategy({}, createTrackingConsentState(), createCustomVitalsState(), doStartRumSpy) + ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) }) it('addAction', () => { @@ -794,11 +726,12 @@ describe('preStartRum', () => { describe('tracking consent', () => { let strategy: Strategy + let doStartRumSpy: jasmine.Spy let trackingConsentState: TrackingConsentState beforeEach(() => { trackingConsentState = createTrackingConsentState() - strategy = createPreStartStrategy({}, trackingConsentState, createCustomVitalsState(), doStartRumSpy) + ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ trackingConsentState })) }) describe('basic methods instrumentation', () => { @@ -876,3 +809,22 @@ describe('preStartRum', () => { }) }) }) + +function createPreStartStrategyWithDefaults({ + rumPublicApiOptions = {}, + trackingConsentState = createTrackingConsentState(), +}: { + rumPublicApiOptions?: RumPublicApiOptions + trackingConsentState?: TrackingConsentState +} = {}) { + const doStartRumSpy = jasmine.createSpy() + return { + strategy: createPreStartStrategy( + rumPublicApiOptions, + trackingConsentState, + createCustomVitalsState(), + doStartRumSpy + ), + doStartRumSpy, + } +} diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index efa02837b2..5bdbd6bde9 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -48,15 +48,17 @@ import { callPluginsMethod } from '../domain/plugins' import type { StartRumResult } from './startRum' import type { RumPublicApiOptions, Strategy } from './rumPublicApi' +export type DoStartRum = ( + configuration: RumConfiguration, + deflateWorker: DeflateWorker | undefined, + initialViewOptions: ViewOptions | undefined +) => StartRumResult + export function createPreStartStrategy( { ignoreInitIfSyntheticsWillInjectRum = true, startDeflateWorker }: RumPublicApiOptions, trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, - doStartRum: ( - configuration: RumConfiguration, - deflateWorker: DeflateWorker | undefined, - initialViewOptions?: ViewOptions - ) => StartRumResult + doStartRum: DoStartRum ): Strategy { const bufferApiCalls = createBoundedBuffer() diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index ed472b25a7..adc275b7bb 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -12,7 +12,7 @@ import { mockClock, mockExperimentalFeatures } from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' import { ActionType, VitalType } from '../rawRumEvent.types' import type { DurationVitalReference } from '../domain/vital/vitalCollection' -import type { RumPublicApi, RecorderApi } from './rumPublicApi' +import type { RumPublicApi, RecorderApi, ProfilerApi, RumPublicApiOptions } from './rumPublicApi' import { makeRumPublicApi } from './rumPublicApi' import type { StartRum } from './startRum' @@ -55,30 +55,20 @@ const FAKE_WORKER = {} as DeflateWorker describe('rum public api', () => { describe('init', () => { - let startRumSpy: jasmine.Spy - - beforeEach(() => { - startRumSpy = jasmine.createSpy().and.callFake(noopStartRum) - }) - describe('deflate worker', () => { let rumPublicApi: RumPublicApi let recorderApiOnRumStartSpy: jasmine.Spy beforeEach(() => { recorderApiOnRumStartSpy = jasmine.createSpy() - - rumPublicApi = makeRumPublicApi( - startRumSpy, - { - ...noopRecorderApi, + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + recorderApi: { onRumStart: recorderApiOnRumStartSpy, }, - noopProfilerApi, - { + rumPublicApiOptions: { startDeflateWorker: () => FAKE_WORKER, - } - ) + }, + })) }) it('pass the worker to the recorder API', () => { @@ -100,14 +90,11 @@ describe('rum public api', () => { application_id: '123', session_id: '123', })) - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { getInternalContext: getInternalContextSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('returns the internal context after init', () => { @@ -128,7 +115,7 @@ describe('rum public api', () => { describe('getInitConfiguration', () => { it('clones the init configuration', () => { - const rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + const { rumPublicApi } = makeRumPublicApiWithDefaults() rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) @@ -143,14 +130,11 @@ describe('rum public api', () => { beforeEach(() => { addActionSpy = jasmine.createSpy() - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addAction: addActionSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) mockClock() }) @@ -194,14 +178,11 @@ describe('rum public api', () => { beforeEach(() => { addErrorSpy = jasmine.createSpy() - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addError: addErrorSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) clock = mockClock() }) @@ -256,14 +237,11 @@ describe('rum public api', () => { beforeEach(() => { addActionSpy = jasmine.createSpy() displaySpy = spyOn(display, 'error') - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addAction: addActionSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should attach valid objects', () => { @@ -314,7 +292,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { - rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + ;({ rumPublicApi } = makeRumPublicApiWithDefaults()) }) it('should return empty object if no user has been set', () => { @@ -340,7 +318,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { - rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + ;({ rumPublicApi } = makeRumPublicApiWithDefaults()) }) it('should add attribute', () => { @@ -385,7 +363,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { - rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + ;({ rumPublicApi } = makeRumPublicApiWithDefaults()) }) it('should remove property', () => { @@ -406,14 +384,11 @@ describe('rum public api', () => { beforeEach(() => { addActionSpy = jasmine.createSpy() displaySpy = spyOn(display, 'error') - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addAction: addActionSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should attach valid objects', () => { @@ -463,7 +438,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { - rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + ;({ rumPublicApi } = makeRumPublicApiWithDefaults()) }) it('should return empty object if no account has been set', () => { @@ -489,7 +464,7 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { - rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + ;({ rumPublicApi } = makeRumPublicApiWithDefaults()) }) it('should add attribute', () => { @@ -531,8 +506,9 @@ describe('rum public api', () => { let rumPublicApi: RumPublicApi beforeEach(() => { - rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + ;({ rumPublicApi } = makeRumPublicApiWithDefaults()) }) + it('should remove property', () => { const account = { id: 'foo', name: 'bar', email: 'qux', foo: { bar: 'qux' } } @@ -551,14 +527,11 @@ describe('rum public api', () => { beforeEach(() => { addTimingSpy = jasmine.createSpy() displaySpy = spyOn(display, 'error') - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addTiming: addTimingSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should add custom timings', () => { @@ -590,14 +563,11 @@ describe('rum public api', () => { beforeEach(() => { addFeatureFlagEvaluationSpy = jasmine.createSpy() displaySpy = spyOn(display, 'error') - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addFeatureFlagEvaluation: addFeatureFlagEvaluationSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should add feature flag evaluation when ff feature_flags enabled', () => { @@ -613,11 +583,11 @@ describe('rum public api', () => { describe('stopSession', () => { it('calls stopSession on the startRum result', () => { const stopSessionSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ ...noopStartRum(), stopSession: stopSessionSpy }), - noopRecorderApi, - noopProfilerApi - ) + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { + stopSession: stopSessionSpy, + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.stopSession() expect(stopSessionSpy).toHaveBeenCalled() @@ -627,11 +597,11 @@ describe('rum public api', () => { describe('startView', () => { it('should call RUM results startView with the view name', () => { const startViewSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ ...noopStartRum(), startView: startViewSpy }), - noopRecorderApi, - noopProfilerApi - ) + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { + startView: startViewSpy, + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView('foo') expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo' }) @@ -639,11 +609,11 @@ describe('rum public api', () => { it('should call RUM results startView with the view options', () => { const startViewSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ ...noopStartRum(), startView: startViewSpy }), - noopRecorderApi, - noopProfilerApi - ) + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { + startView: startViewSpy, + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView({ name: 'foo', service: 'bar', version: 'baz', context: { foo: 'bar' } }) expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ @@ -656,19 +626,27 @@ describe('rum public api', () => { }) describe('recording', () => { - let recorderApiOnRumStartSpy: jasmine.Spy let rumPublicApi: RumPublicApi - let recorderApi: RecorderApi + let recorderApi: { + onRumStart: jasmine.Spy + start: jasmine.Spy + stop: jasmine.Spy + getSessionReplayLink: jasmine.Spy + } beforeEach(() => { - recorderApiOnRumStartSpy = jasmine.createSpy('recorderApiOnRumStart') - recorderApi = { ...noopRecorderApi, onRumStart: recorderApiOnRumStartSpy } - rumPublicApi = makeRumPublicApi(noopStartRum, recorderApi, noopProfilerApi) + recorderApi = { + onRumStart: jasmine.createSpy(), + start: jasmine.createSpy(), + stop: jasmine.createSpy(), + getSessionReplayLink: jasmine.createSpy(), + } + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ recorderApi })) }) it('is started with the default defaultPrivacyLevel', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) + expect(recorderApi.onRumStart.calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) }) it('is started with the configured defaultPrivacyLevel', () => { @@ -676,16 +654,12 @@ describe('rum public api', () => { ...DEFAULT_INIT_CONFIGURATION, defaultPrivacyLevel: DefaultPrivacyLevel.MASK_USER_INPUT, }) - expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].defaultPrivacyLevel).toBe( + expect(recorderApi.onRumStart.calls.mostRecent().args[1].defaultPrivacyLevel).toBe( DefaultPrivacyLevel.MASK_USER_INPUT ) }) it('public api calls are forwarded to the recorder api', () => { - spyOn(recorderApi, 'start') - spyOn(recorderApi, 'stop') - spyOn(recorderApi, 'getSessionReplayLink') - rumPublicApi.startSessionReplayRecording() rumPublicApi.stopSessionReplayRecording() rumPublicApi.getSessionReplayLink() @@ -697,7 +671,7 @@ describe('rum public api', () => { it('is started with the default startSessionReplayRecordingManually', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(true) + expect(recorderApi.onRumStart.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(true) }) it('is started with the configured startSessionReplayRecordingManually', () => { @@ -705,21 +679,18 @@ describe('rum public api', () => { ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: false, }) - expect(recorderApiOnRumStartSpy.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(false) + expect(recorderApi.onRumStart.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(false) }) }) describe('startDurationVital', () => { it('should call startDurationVital on the startRum result', () => { const startDurationVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { startDurationVital: startDurationVitalSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) expect(startDurationVitalSpy).toHaveBeenCalledWith('foo', { @@ -732,14 +703,11 @@ describe('rum public api', () => { describe('stopDurationVital', () => { it('should call stopDurationVital with a name on the startRum result', () => { const stopDurationVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { stopDurationVital: stopDurationVitalSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) rumPublicApi.stopDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) @@ -751,14 +719,11 @@ describe('rum public api', () => { it('should call stopDurationVital with a reference on the startRum result', () => { const stopDurationVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { stopDurationVital: stopDurationVitalSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) const ref = rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) rumPublicApi.stopDurationVital(ref, { context: { foo: 'bar' }, description: 'description-value' }) @@ -775,15 +740,12 @@ describe('rum public api', () => { const startActionSpy = jasmine.createSpy() const stopActionSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { startAction: startActionSpy, stopAction: stopActionSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startAction('purchase', { @@ -813,14 +775,11 @@ describe('rum public api', () => { mockExperimentalFeatures([ExperimentalFeature.START_STOP_ACTION]) const startActionSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { startAction: startActionSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startAction('action_name', { @@ -841,15 +800,12 @@ describe('rum public api', () => { it('should not call startAction/stopAction when feature flag is disabled', () => { const startActionSpy = jasmine.createSpy() const stopActionSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { startAction: startActionSpy, stopAction: stopActionSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startAction('purchase', { type: ActionType.CUSTOM }) @@ -863,14 +819,11 @@ describe('rum public api', () => { describe('addDurationVital', () => { it('should call addDurationVital on the startRum result', () => { const addDurationVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { addDurationVital: addDurationVitalSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + }) const startTime = 1707755888000 as TimeStamp rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.addDurationVital('foo', { @@ -893,11 +846,11 @@ describe('rum public api', () => { describe('startFeatureOperation', () => { it('should call addOperationStepVital on the startRum result with start status', () => { const addOperationStepVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }), - noopRecorderApi, - noopProfilerApi - ) + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { + addOperationStepVital: addOperationStepVitalSpy, + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'start', { @@ -909,11 +862,11 @@ describe('rum public api', () => { describe('succeedFeatureOperation', () => { it('should call addOperationStepVital on the startRum result with end status', () => { const addOperationStepVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }), - noopRecorderApi, - noopProfilerApi - ) + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { + addOperationStepVital: addOperationStepVitalSpy, + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.succeedFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'end', { @@ -925,11 +878,11 @@ describe('rum public api', () => { describe('failFeatureOperation', () => { it('should call addOperationStepVital on the startRum result with end status and failure reason', () => { const addOperationStepVitalSpy = jasmine.createSpy() - const rumPublicApi = makeRumPublicApi( - () => ({ ...noopStartRum(), addOperationStepVital: addOperationStepVitalSpy }), - noopRecorderApi, - noopProfilerApi - ) + const { rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { + addOperationStepVital: addOperationStepVitalSpy, + }, + }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.failFeatureOperation('foo', 'error', { operationKey: '00000000-0000-0000-0000-000000000000' }) expect(addOperationStepVitalSpy).toHaveBeenCalledWith( @@ -942,7 +895,7 @@ describe('rum public api', () => { }) it('should provide sdk version', () => { - const rumPublicApi = makeRumPublicApi(noopStartRum, noopRecorderApi, noopProfilerApi) + const { rumPublicApi } = makeRumPublicApiWithDefaults() expect(rumPublicApi.version).toBe('test') }) @@ -952,14 +905,11 @@ describe('rum public api', () => { beforeEach(() => { setViewNameSpy = jasmine.createSpy() - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { setViewName: setViewNameSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should set the view name', () => { @@ -977,15 +927,12 @@ describe('rum public api', () => { beforeEach(() => { setViewContextSpy = jasmine.createSpy() setViewContextPropertySpy = jasmine.createSpy() - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { setViewContext: setViewContextSpy, setViewContextProperty: setViewContextPropertySpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should set view specific context with setViewContext', () => { @@ -1013,14 +960,11 @@ describe('rum public api', () => { getViewContextSpy = jasmine.createSpy().and.callFake(() => ({ foo: 'bar', })) - rumPublicApi = makeRumPublicApi( - () => ({ - ...noopStartRum(), + ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + startRumResult: { getViewContext: getViewContextSpy, - }), - noopRecorderApi, - noopProfilerApi - ) + }, + })) }) it('should return the view context after init', () => { @@ -1037,15 +981,11 @@ describe('rum public api', () => { }) describe('it should pass down the sdk name to startRum', () => { - let startRumSpy: jasmine.Spy - - beforeEach(() => { - startRumSpy = jasmine.createSpy().and.callFake(noopStartRum) - }) - it('should return the sdk name', () => { - const rumPublicApi = makeRumPublicApi(startRumSpy, noopRecorderApi, noopProfilerApi, { - sdkName: 'rum-slim', + const { rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ + rumPublicApiOptions: { + sdkName: 'rum-slim', + }, }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) @@ -1054,3 +994,29 @@ describe('rum public api', () => { }) }) }) + +function makeRumPublicApiWithDefaults({ + recorderApi, + profilerApi, + startRumResult, + rumPublicApiOptions = {}, +}: { + recorderApi?: Partial + profilerApi?: Partial + startRumResult?: Partial> + rumPublicApiOptions?: RumPublicApiOptions +} = {}) { + const startRumSpy = jasmine.createSpy().and.callFake(() => ({ + ...noopStartRum(), + ...startRumResult, + })) + return { + startRumSpy, + rumPublicApi: makeRumPublicApi( + startRumSpy, + { ...noopRecorderApi, ...recorderApi }, + { ...noopProfilerApi, ...profilerApi }, + rumPublicApiOptions + ), + } +} diff --git a/packages/rum/src/types/sessionReplay.ts b/packages/rum/src/types/sessionReplay.ts index bceb0a14bf..8cb22ec203 100644 --- a/packages/rum/src/types/sessionReplay.ts +++ b/packages/rum/src/types/sessionReplay.ts @@ -419,7 +419,7 @@ export type AddDocTypeNodeChange = [ '#doctype' | StringReference, StringOrStringReference, StringOrStringReference, - StringOrStringReference + StringOrStringReference, ] /** * Browser-specific. Schema representing a string, either expressed as a literal or as an index into the string table. @@ -560,7 +560,7 @@ export type VisualViewportChange = [ VisualViewportPageTop, VisualViewportWidth, VisualViewportHeight, - VisualViewportScale + VisualViewportScale, ] /** * The offset of the left edge of the visual viewport from the left edge of the layout viewport in CSS pixels. From 62302f59124c729a16e228a1a3279b9a9015a5ac Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 26 Jan 2026 17:26:12 +0100 Subject: [PATCH 20/41] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20make=20telemetry=20r?= =?UTF-8?q?equired=20in=20`startLogs`=20and=20`startRum`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No need to start the telemetry again in `startLogs` and `startRum`. --- packages/core/test/emulate/mockTelemetry.ts | 11 +++++++ packages/logs/src/boot/logsPublicApi.ts | 12 ++----- packages/logs/src/boot/preStartLogs.ts | 32 +++++++------------ packages/logs/src/boot/startLogs.spec.ts | 6 +++- packages/logs/src/boot/startLogs.ts | 24 +++----------- packages/rum-core/src/boot/preStartRum.ts | 31 ++++++------------ .../rum-core/src/boot/rumPublicApi.spec.ts | 2 +- packages/rum-core/src/boot/rumPublicApi.ts | 15 +++------ packages/rum-core/src/boot/startRum.spec.ts | 3 ++ packages/rum-core/src/boot/startRum.ts | 24 +++----------- 10 files changed, 56 insertions(+), 104 deletions(-) diff --git a/packages/core/test/emulate/mockTelemetry.ts b/packages/core/test/emulate/mockTelemetry.ts index 77a52e6223..145066b449 100644 --- a/packages/core/test/emulate/mockTelemetry.ts +++ b/packages/core/test/emulate/mockTelemetry.ts @@ -4,8 +4,10 @@ import { getTelemetryObservable, resetTelemetry, type RawTelemetryEvent, + type Telemetry, } from '../../src/domain/telemetry' import { registerCleanupTask } from '../registerCleanupTask' +import { noop } from '../../src/tools/utils/functionUtils' export interface MockTelemetry { getEvents: () => Promise @@ -43,3 +45,12 @@ export function startMockTelemetry() { }, } } + +export function createFakeTelemetryObject(): Telemetry { + return { + stop: noop, + enabled: false, + metricsEnabled: false, + startTransport: noop, + } +} diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index d342a6d97c..b5ddc5e253 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -272,12 +272,6 @@ export interface Strategy { userContext: ContextManager getInternalContext: StartLogsResult['getInternalContext'] handleLog: StartLogsResult['handleLog'] - - // Internal: cached telemetry instance from preStart phase - cachedTelemetry?: Telemetry - - // Internal: cached hooks instance from preStart phase - cachedHooks?: AbstractHooks } export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { @@ -287,14 +281,14 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { let strategy = createPreStartStrategy( buildCommonContext, trackingConsentState, - (initConfiguration, configuration) => { + (initConfiguration, configuration, telemetry, hooks) => { const startLogsResult = startLogsImpl( configuration, buildCommonContext, trackingConsentState, bufferedDataObservable, - strategy.cachedTelemetry, - strategy.cachedHooks + telemetry, + hooks ) strategy = createPostStartStrategy(initConfiguration, startLogsResult) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index a071f239dc..b5e50e8b15 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -1,4 +1,4 @@ -import type { TrackingConsentState, Telemetry, AbstractHooks } from '@datadog/browser-core' +import type { TrackingConsentState, Telemetry } from '@datadog/browser-core' import { createBoundedBuffer, canUseEventBridge, @@ -17,6 +17,7 @@ import { startTelemetry, TelemetryService, } from '@datadog/browser-core' +import type { Hooks } from '../domain/hooks' import { createHooks } from '../domain/hooks' import type { LogsConfiguration, LogsInitConfiguration } from '../domain/configuration' import { serializeLogsConfiguration, validateAndBuildLogsConfiguration } from '../domain/configuration' @@ -26,7 +27,9 @@ import type { StartLogsResult } from './startLogs' export type DoStartLogs = ( initConfiguration: LogsInitConfiguration, - configuration: LogsConfiguration + configuration: LogsConfiguration, + telemetry: Telemetry, + hooks: Hooks ) => StartLogsResult export function createPreStartStrategy( @@ -48,17 +51,17 @@ export function createPreStartStrategy( let cachedInitConfiguration: LogsInitConfiguration | undefined let cachedConfiguration: LogsConfiguration | undefined - let cachedTelemetry: Telemetry | undefined - let cachedHooks: AbstractHooks | undefined + let telemetry: Telemetry | undefined + const hooks = createHooks() const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartLogs) function tryStartLogs() { - if (!cachedConfiguration || !cachedInitConfiguration || !trackingConsentState.isGranted()) { + if (!cachedConfiguration || !cachedInitConfiguration || !telemetry || !trackingConsentState.isGranted()) { return } trackingConsentStateSubscription.unsubscribe() - const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration) + const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, telemetry, hooks) bufferApiCalls.drain(startLogsResult) } @@ -92,13 +95,8 @@ export function createPreStartStrategy( cachedConfiguration = configuration - // Create hooks early (they're just an empty registry) - cachedHooks = createHooks() - - // Start telemetry collection early with real hooks (transport will start later in startLogs) - cachedTelemetry = startTelemetry(TelemetryService.LOGS, configuration, { - hooks: cachedHooks, - // Other dependencies will be provided via startTransport() later + telemetry = startTelemetry(TelemetryService.LOGS, configuration, { + hooks, }) // Instrument fetch to track network requests @@ -115,14 +113,6 @@ export function createPreStartStrategy( return cachedInitConfiguration }, - get cachedTelemetry() { - return cachedTelemetry - }, - - get cachedHooks() { - return cachedHooks - }, - globalContext, accountContext, userContext, diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 82616e3615..e10b43a6ba 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -23,11 +23,13 @@ import { mockClock, expireCookie, DEFAULT_FETCH_MOCK, + createFakeTelemetryObject, } from '@datadog/browser-core/test' import type { LogsConfiguration } from '../domain/configuration' import { validateAndBuildLogsConfiguration } from '../domain/configuration' import { Logger } from '../domain/logger' +import { createHooks } from '../domain/hooks' import { StatusType } from '../domain/logger/isAuthorized' import type { LogsEvent } from '../logsEvent.types' import { startLogs } from './startLogs' @@ -65,7 +67,9 @@ function startLogsWithDefaults( }, () => COMMON_CONTEXT, trackingConsentState, - new BufferedObservable(100) + new BufferedObservable(100), + createFakeTelemetryObject(), + createHooks() ) registerCleanupTask(stop) diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 794d0abe42..9d6c638d0a 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -4,7 +4,6 @@ import type { BufferedData, PageMayExitEvent, Telemetry, - AbstractHooks, } from '@datadog/browser-core' import { Observable, @@ -14,8 +13,6 @@ import { canUseEventBridge, startAccountContext, startGlobalContext, - startTelemetry, - TelemetryService, createIdentityEncoder, startUserContext, isWorkerEnvironment, @@ -34,7 +31,7 @@ import { startLogsBridge } from '../transport/startLogsBridge' import { startInternalContext } from '../domain/contexts/internalContext' import { startReportError } from '../domain/reportError' import type { CommonContext } from '../rawLogsEvent.types' -import { createHooks } from '../domain/hooks' +import type { Hooks } from '../domain/hooks' import { startRUMInternalContext } from '../domain/contexts/rumInternalContext' import { startSessionContext } from '../domain/contexts/sessionContext' import { startTrackingConsentContext } from '../domain/contexts/trackingConsentContext' @@ -53,12 +50,10 @@ export function startLogs( // `trackingConsentState` set to "granted". trackingConsentState: TrackingConsentState, bufferedDataObservable: BufferedObservable, - cachedTelemetry?: Telemetry, - cachedHooks?: AbstractHooks + telemetry: Telemetry, + hooks: Hooks ) { const lifeCycle = new LifeCycle() - // Use cached hooks if available (started in preStart), otherwise create new - const hooks = cachedHooks ?? createHooks() const cleanupTasks: Array<() => void> = [] lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (log) => sendToExtension('logs', log)) @@ -69,18 +64,7 @@ export function startLogs( ? new Observable() : createPageMayExitObservable(configuration) - // Use cached telemetry if available (started in preStart), otherwise create new with hooks - const telemetry = - cachedTelemetry ?? - startTelemetry(TelemetryService.LOGS, configuration, { - hooks, - reportError, - pageMayExitObservable, - createEncoder: createIdentityEncoder, - }) - - // If using cached telemetry (already has hooks), start transport now - if (cachedTelemetry) { + if (telemetry) { telemetry.startTransport(reportError, pageMayExitObservable, createIdentityEncoder) } diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 5bdbd6bde9..15a5852e10 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -5,7 +5,6 @@ import type { ContextManager, BoundedBuffer, Telemetry, - AbstractHooks, } from '@datadog/browser-core' import { createBoundedBuffer, @@ -29,6 +28,7 @@ import { startTelemetry, TelemetryService, } from '@datadog/browser-core' +import type { Hooks } from '../domain/hooks' import { createHooks } from '../domain/hooks' import type { RumConfiguration, RumInitConfiguration } from '../domain/configuration' import { @@ -51,7 +51,9 @@ import type { RumPublicApiOptions, Strategy } from './rumPublicApi' export type DoStartRum = ( configuration: RumConfiguration, deflateWorker: DeflateWorker | undefined, - initialViewOptions: ViewOptions | undefined + initialViewOptions: ViewOptions | undefined, + telemetry: Telemetry, + hooks: Hooks ) => StartRumResult export function createPreStartStrategy( @@ -79,15 +81,15 @@ export function createPreStartStrategy( let cachedInitConfiguration: RumInitConfiguration | undefined let cachedConfiguration: RumConfiguration | undefined - let cachedTelemetry: Telemetry | undefined - let cachedHooks: AbstractHooks | undefined + let telemetry: Telemetry | undefined + const hooks = createHooks() const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartRum) const emptyContext: Context = {} function tryStartRum() { - if (!cachedInitConfiguration || !cachedConfiguration || !trackingConsentState.isGranted()) { + if (!cachedInitConfiguration || !cachedConfiguration || !telemetry || !trackingConsentState.isGranted()) { return } @@ -109,7 +111,7 @@ export function createPreStartStrategy( initialViewOptions = firstStartViewCall.options } - const startRumResult = doStartRum(cachedConfiguration, deflateWorker, initialViewOptions) + const startRumResult = doStartRum(cachedConfiguration, deflateWorker, initialViewOptions, telemetry, hooks) bufferApiCalls.drain(startRumResult) } @@ -156,13 +158,8 @@ export function createPreStartStrategy( cachedConfiguration = configuration - // Create hooks early (they're just an empty registry) - cachedHooks = createHooks() - - // Start telemetry collection early with real hooks (transport will start later in startRum) - cachedTelemetry = startTelemetry(TelemetryService.RUM, configuration, { - hooks: cachedHooks, - // Other dependencies will be provided via startTransport() later + telemetry = startTelemetry(TelemetryService.RUM, configuration, { + hooks, }) // Instrument fetch to track network requests @@ -234,14 +231,6 @@ export function createPreStartStrategy( return cachedInitConfiguration }, - get cachedTelemetry() { - return cachedTelemetry - }, - - get cachedHooks() { - return cachedHooks - }, - getInternalContext: noop as () => undefined, stopSession: noop, diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index adc275b7bb..3423502d6d 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -989,7 +989,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - const sdkName = startRumSpy.calls.argsFor(0)[8] + const sdkName = startRumSpy.calls.argsFor(0)[10] expect(sdkName).toBe('rum-slim') }) }) diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index e540cd3c8e..70ed50bdd5 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -14,7 +14,6 @@ import type { RumInternalContext, Telemetry, Encoder, - AbstractHooks, } from '@datadog/browser-core' import { ContextManagerMethod, @@ -553,12 +552,6 @@ export interface Strategy { stopDurationVital: StartRumResult['stopDurationVital'] addDurationVital: StartRumResult['addDurationVital'] addOperationStepVital: StartRumResult['addOperationStepVital'] - - // Internal: cached telemetry instance from preStart phase - cachedTelemetry?: Telemetry - - // Internal: cached hooks instance from preStart phase - cachedHooks?: AbstractHooks } export function makeRumPublicApi( @@ -575,7 +568,7 @@ export function makeRumPublicApi( options, trackingConsentState, customVitalsState, - (configuration, deflateWorker, initialViewOptions) => { + (configuration, deflateWorker, initialViewOptions, telemetry, hooks) => { const createEncoder = deflateWorker && options.createDeflateEncoder ? (streamId: DeflateEncoderStreamId) => options.createDeflateEncoder!(configuration, deflateWorker, streamId) @@ -590,9 +583,9 @@ export function makeRumPublicApi( trackingConsentState, customVitalsState, bufferedDataObservable, - options.sdkName, - strategy.cachedTelemetry, - strategy.cachedHooks + telemetry, + hooks, + options.sdkName ) recorderApi.onRumStart( diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index da2fabae33..b3aeca3325 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -19,6 +19,7 @@ import { mockClock, mockEventBridge, registerCleanupTask, + createFakeTelemetryObject, } from '@datadog/browser-core/test' import type { RumSessionManagerMock } from '../../test' import { createRumSessionManagerMock, mockRumConfiguration, noopProfilerApi, noopRecorderApi } from '../../test' @@ -169,6 +170,8 @@ describe('view events', () => { createTrackingConsentState(TrackingConsent.GRANTED), createCustomVitalsState(), new BufferedObservable(100), + createFakeTelemetryObject(), + createHooks(), 'rum' ) diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 79f8db27b8..1b4984248d 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -11,8 +11,6 @@ import type { import { sendToExtension, createPageMayExitObservable, - TelemetryService, - startTelemetry, canUseEventBridge, addTelemetryDebug, startAccountContext, @@ -53,7 +51,6 @@ import type { SdkName } from '../domain/contexts/defaultContext' import { startDefaultContext } from '../domain/contexts/defaultContext' import { startTrackingConsentContext } from '../domain/contexts/trackingConsentContext' import type { Hooks } from '../domain/hooks' -import { createHooks } from '../domain/hooks' import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' import { startSourceCodeContext } from '../domain/contexts/sourceCodeContext' @@ -75,14 +72,12 @@ export function startRum( trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, bufferedDataObservable: BufferedObservable, - sdkName?: SdkName, - cachedTelemetry?: Telemetry, - cachedHooks?: Hooks + telemetry: Telemetry, + hooks: Hooks, + sdkName?: SdkName ) { const cleanupTasks: Array<() => void> = [] const lifeCycle = new LifeCycle() - // Use cached hooks if available (started in preStart), otherwise create new - const hooks = cachedHooks ?? createHooks() lifeCycle.subscribe(LifeCycleEventType.RUM_EVENT_COLLECTED, (event) => sendToExtension('rum', event)) @@ -98,18 +93,7 @@ export function startRum( }) cleanupTasks.push(() => pageMayExitSubscription.unsubscribe()) - // Use cached telemetry if available (started in preStart), otherwise create new with hooks - const telemetry = - cachedTelemetry ?? - startTelemetry(TelemetryService.RUM, configuration, { - hooks, - reportError, - pageMayExitObservable, - createEncoder, - }) - - // If using cached telemetry (already has hooks), start transport now - if (cachedTelemetry) { + if (telemetry) { telemetry.startTransport(reportError, pageMayExitObservable, createEncoder) } From e52a2d555dab5df691eb07d453f24ec05f0e8c17 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 26 Jan 2026 17:47:32 +0100 Subject: [PATCH 21/41] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20make=20hooks=20requi?= =?UTF-8?q?red=20when=20starting=20telemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This simplifies the code a bit. --- .../src/domain/telemetry/telemetry.spec.ts | 38 +++++++++++-------- .../core/src/domain/telemetry/telemetry.ts | 13 +++---- packages/logs/src/boot/preStartLogs.ts | 4 +- packages/rum-core/src/boot/preStartRum.ts | 4 +- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetry.spec.ts b/packages/core/src/domain/telemetry/telemetry.spec.ts index 09e7536e75..ef4f50631d 100644 --- a/packages/core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/core/src/domain/telemetry/telemetry.spec.ts @@ -442,7 +442,7 @@ describe('telemetry', () => { describe('telemetry with deferred transport', () => { it('should start telemetry without transport dependencies', () => { - const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration) + const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, createHooks()) // Verify telemetry was started expect(telemetry).toBeDefined() @@ -450,9 +450,7 @@ describe('telemetry with deferred transport', () => { }) it('should allow starting transport later', () => { - const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration) - - createHooks() + const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, createHooks()) // Should not throw when calling startTransport expect(() => { @@ -461,12 +459,16 @@ describe('telemetry with deferred transport', () => { }) it('should ignore second call to startTransport', () => { - const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, { - hooks: createHooks(), - reportError: noop, - pageMayExitObservable: new Observable(), - createEncoder: createIdentityEncoder, - }) + const telemetry = startTelemetry( + TelemetryService.RUM, + { telemetrySampleRate: 100 } as Configuration, + createHooks(), + { + reportError: noop, + pageMayExitObservable: new Observable(), + createEncoder: createIdentityEncoder, + } + ) // Second call should be ignored (no error thrown) expect(() => { @@ -476,12 +478,16 @@ describe('telemetry with deferred transport', () => { it('should maintain backward compatibility with full dependencies', () => { // Start with full dependencies (backward compatibility) - const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, { - hooks: createHooks(), - reportError: noop, - pageMayExitObservable: new Observable(), - createEncoder: createIdentityEncoder, - }) + const telemetry = startTelemetry( + TelemetryService.RUM, + { telemetrySampleRate: 100 } as Configuration, + createHooks(), + { + reportError: noop, + pageMayExitObservable: new Observable(), + createEncoder: createIdentityEncoder, + } + ) // Should work without error expect(telemetry).toBeDefined() diff --git a/packages/core/src/domain/telemetry/telemetry.ts b/packages/core/src/domain/telemetry/telemetry.ts index d751e7297d..2027871302 100644 --- a/packages/core/src/domain/telemetry/telemetry.ts +++ b/packages/core/src/domain/telemetry/telemetry.ts @@ -94,8 +94,8 @@ export function getTelemetryObservable() { export function startTelemetry( telemetryService: TelemetryService, configuration: Configuration, + hooks: AbstractHooks, transportDependencies?: { - hooks?: AbstractHooks reportError?: (error: RawError) => void pageMayExitObservable?: Observable createEncoder?: (streamId: DeflateEncoderStreamId) => Encoder @@ -104,8 +104,6 @@ export function startTelemetry( const observable = new Observable() let transportCleanup: (() => void) | undefined - // Hooks are optional - if not provided, telemetry collection won't use them - const hooks = transportDependencies?.hooks const { enabled, metricsEnabled } = startTelemetryCollection(telemetryService, configuration, hooks, observable) // Start transport immediately only if all transport dependencies are provided @@ -152,7 +150,7 @@ export function startTelemetry( export function startTelemetryCollection( telemetryService: TelemetryService, configuration: Configuration, - hooks: AbstractHooks | undefined, + hooks: AbstractHooks, observable: Observable, metricSampleRate = METRIC_SAMPLE_RATE, maxTelemetryEventsPerPage = MAX_TELEMETRY_EVENTS_PER_PAGE @@ -192,10 +190,9 @@ export function startTelemetryCollection( return } - const defaultTelemetryEventAttributes = - hooks?.triggerHook(HookNames.AssembleTelemetry, { - startTime: clocksNow().relative, - }) ?? {} + const defaultTelemetryEventAttributes = hooks.triggerHook(HookNames.AssembleTelemetry, { + startTime: clocksNow().relative, + }) if (defaultTelemetryEventAttributes === DISCARDED) { return diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index b5e50e8b15..3d11d66f38 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -95,9 +95,7 @@ export function createPreStartStrategy( cachedConfiguration = configuration - telemetry = startTelemetry(TelemetryService.LOGS, configuration, { - hooks, - }) + telemetry = startTelemetry(TelemetryService.LOGS, configuration, hooks, {}) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 15a5852e10..25dc9feda9 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -158,9 +158,7 @@ export function createPreStartStrategy( cachedConfiguration = configuration - telemetry = startTelemetry(TelemetryService.RUM, configuration, { - hooks, - }) + telemetry = startTelemetry(TelemetryService.RUM, configuration, hooks, {}) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer From f2dbf433ee7dd87198f907707710713f8478e248 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 26 Jan 2026 18:02:10 +0100 Subject: [PATCH 22/41] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20start=20telemetry=20?= =?UTF-8?q?transport=20when=20starting=20telemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/telemetry/telemetry.spec.ts | 100 ++++++++---------- .../core/src/domain/telemetry/telemetry.ts | 74 +++---------- packages/core/test/emulate/mockTelemetry.ts | 1 - packages/logs/src/boot/logsPublicApi.ts | 14 +-- packages/logs/src/boot/preStartLogs.ts | 10 +- packages/logs/src/boot/startLogs.spec.ts | 2 - packages/logs/src/boot/startLogs.ts | 16 +-- packages/rum-core/src/boot/preStartRum.ts | 2 +- packages/rum-core/src/boot/startRum.ts | 6 -- 9 files changed, 66 insertions(+), 159 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetry.spec.ts b/packages/core/src/domain/telemetry/telemetry.spec.ts index ef4f50631d..63c4619246 100644 --- a/packages/core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/core/src/domain/telemetry/telemetry.spec.ts @@ -1,12 +1,19 @@ +import type { TimeStamp } from '@datadog/browser-rum/internal' import { NO_ERROR_STACK_PRESENT_MESSAGE } from '../error/error' import { callMonitored } from '../../tools/monitor' import type { ExperimentalFeature } from '../../tools/experimentalFeatures' import { resetExperimentalFeatures, addExperimentalFeatures } from '../../tools/experimentalFeatures' -import type { Configuration } from '../configuration' +import { validateAndBuildConfiguration, type Configuration } from '../configuration' import { INTAKE_SITE_US1_FED, INTAKE_SITE_US1 } from '../intakeSites' -import { setNavigatorOnLine, setNavigatorConnection, createHooks, waitNextMicrotask } from '../../../test' -import { noop } from '../../tools/utils/functionUtils' -import { createIdentityEncoder } from '../../tools/encoder' +import { + setNavigatorOnLine, + setNavigatorConnection, + createHooks, + waitNextMicrotask, + interceptRequests, + registerCleanupTask, + createNewEvent, +} from '../../../test' import type { Context } from '../../tools/serialisation/context' import { Observable } from '../../tools/observable' import type { StackTrace } from '../../tools/stackTrace/computeStackTrace' @@ -20,10 +27,10 @@ import { addTelemetryUsage, TelemetryService, startTelemetryCollection, - startTelemetry, addTelemetryMetrics, addTelemetryDebug, TelemetryMetrics, + startTelemetryTransport, } from './telemetry' import type { TelemetryEvent } from './telemetryEvent.types' import { StatusType, TelemetryType } from './rawTelemetryEvent.types' @@ -69,10 +76,6 @@ function startAndSpyTelemetry( } describe('telemetry', () => { - beforeEach(() => { - resetTelemetry() - }) - afterEach(() => { resetTelemetry() }) @@ -440,58 +443,43 @@ describe('telemetry', () => { }) }) -describe('telemetry with deferred transport', () => { - it('should start telemetry without transport dependencies', () => { - const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, createHooks()) - - // Verify telemetry was started - expect(telemetry).toBeDefined() - expect(telemetry.enabled).toEqual(true) - }) - - it('should allow starting transport later', () => { - const telemetry = startTelemetry(TelemetryService.RUM, { telemetrySampleRate: 100 } as Configuration, createHooks()) - - // Should not throw when calling startTransport - expect(() => { - telemetry.startTransport(noop, new Observable(), createIdentityEncoder) - }).not.toThrow() - }) +describe('startTelemetryTransport', () => { + it('should send telemetry events through transport', () => { + const interceptor = interceptRequests() + const telemetryObservable = new Observable() - it('should ignore second call to startTransport', () => { - const telemetry = startTelemetry( - TelemetryService.RUM, - { telemetrySampleRate: 100 } as Configuration, - createHooks(), - { - reportError: noop, - pageMayExitObservable: new Observable(), - createEncoder: createIdentityEncoder, - } + const { stop } = startTelemetryTransport( + validateAndBuildConfiguration({ clientToken: 'xxx' })!, + telemetryObservable ) - // Second call should be ignored (no error thrown) - expect(() => { - telemetry.startTransport(noop, new Observable(), createIdentityEncoder) - }).not.toThrow() - }) + registerCleanupTask(stop) - it('should maintain backward compatibility with full dependencies', () => { - // Start with full dependencies (backward compatibility) - const telemetry = startTelemetry( - TelemetryService.RUM, - { telemetrySampleRate: 100 } as Configuration, - createHooks(), - { - reportError: noop, - pageMayExitObservable: new Observable(), - createEncoder: createIdentityEncoder, - } - ) + // Trigger a telemetry event by notifying the observable + telemetryObservable.notify({ + type: 'telemetry', + date: 123 as TimeStamp, + service: TelemetryService.RUM, + version: '0.0.0', + source: 'browser', + telemetry: { + type: TelemetryType.LOG, + status: StatusType.error, + message: 'test error', + }, + _dd: { + format_version: 2, + }, + }) + + // Force the batch to flush by emulating a beforeunload event + window.dispatchEvent(createNewEvent('beforeunload')) - // Should work without error - expect(telemetry).toBeDefined() - expect(telemetry.enabled).toEqual(true) + expect(interceptor.requests.length).toBe(1) + const telemetryEvent = JSON.parse(interceptor.requests[0].body) + expect(telemetryEvent.type).toBe('telemetry') + expect(telemetryEvent.telemetry.type).toBe(TelemetryType.LOG) + expect(telemetryEvent.telemetry.status).toBe(StatusType.error) }) }) diff --git a/packages/core/src/domain/telemetry/telemetry.ts b/packages/core/src/domain/telemetry/telemetry.ts index 2027871302..c2d079b82b 100644 --- a/packages/core/src/domain/telemetry/telemetry.ts +++ b/packages/core/src/domain/telemetry/telemetry.ts @@ -13,7 +13,6 @@ import { sendToExtension } from '../../tools/sendToExtension' import { performDraw } from '../../tools/utils/numberUtils' import { jsonStringify } from '../../tools/serialisation/jsonStringify' import { combine } from '../../tools/mergeInto' -import type { RawError } from '../error/error.types' import { NonErrorPrefix } from '../error/error.types' import type { StackTrace } from '../../tools/stackTrace/computeStackTrace' import { computeStackTrace } from '../../tools/stackTrace/computeStackTrace' @@ -25,12 +24,12 @@ import { getEventBridge, createBatch, } from '../../transport' -import type { Encoder } from '../../tools/encoder' -import type { PageMayExitEvent } from '../../browser/pageMayExitObservable' -import { DeflateEncoderStreamId } from '../deflate' +import { createIdentityEncoder } from '../../tools/encoder' +import { createPageMayExitObservable } from '../../browser/pageMayExitObservable' import type { AbstractHooks, RecursivePartial } from '../../tools/abstractHooks' import { HookNames, DISCARDED } from '../../tools/abstractHooks' import { globalObject, isWorkerEnvironment } from '../../tools/globalObject' +import { noop } from '../../tools/utils/functionUtils' import type { TelemetryEvent } from './telemetryEvent.types' import type { RawTelemetryConfiguration, @@ -62,11 +61,6 @@ export interface Telemetry { stop: () => void enabled: boolean metricsEnabled: boolean - startTransport: ( - reportError: (error: RawError) => void, - pageMayExitObservable: Observable, - createEncoder: (streamId: DeflateEncoderStreamId) => Encoder - ) => void } export const enum TelemetryMetrics { @@ -94,56 +88,15 @@ export function getTelemetryObservable() { export function startTelemetry( telemetryService: TelemetryService, configuration: Configuration, - hooks: AbstractHooks, - transportDependencies?: { - reportError?: (error: RawError) => void - pageMayExitObservable?: Observable - createEncoder?: (streamId: DeflateEncoderStreamId) => Encoder - } + hooks: AbstractHooks ): Telemetry { const observable = new Observable() - let transportCleanup: (() => void) | undefined - const { enabled, metricsEnabled } = startTelemetryCollection(telemetryService, configuration, hooks, observable) - - // Start transport immediately only if all transport dependencies are provided - if ( - transportDependencies && - transportDependencies.reportError && - transportDependencies.pageMayExitObservable && - transportDependencies.createEncoder - ) { - const { stop } = startTelemetryTransport( - configuration, - transportDependencies.reportError, - transportDependencies.pageMayExitObservable, - transportDependencies.createEncoder, - observable - ) - transportCleanup = stop - } - + const { stop } = startTelemetryTransport(configuration, observable) return { - stop: () => { - transportCleanup?.() - }, + stop, enabled, metricsEnabled, - startTransport: (reportError, pageMayExitObservable, createEncoder) => { - if (transportCleanup) { - // Already started, ignore - return - } - - const { stop } = startTelemetryTransport( - configuration, - reportError, - pageMayExitObservable, - createEncoder, - observable - ) - transportCleanup = stop - }, } } @@ -246,11 +199,8 @@ export function startTelemetryCollection( } } -function startTelemetryTransport( +export function startTelemetryTransport( configuration: Configuration, - reportError: (error: RawError) => void, - pageMayExitObservable: Observable, - createEncoder: (streamId: DeflateEncoderStreamId) => Encoder, telemetryObservable: Observable ) { const cleanupTasks: Array<() => void> = [] @@ -264,10 +214,14 @@ function startTelemetryTransport( endpoints.push(configuration.replica.rumEndpointBuilder) } const telemetryBatch = createBatch({ - encoder: createEncoder(DeflateEncoderStreamId.TELEMETRY), - request: createHttpRequest(endpoints, reportError), + encoder: createIdentityEncoder(), + request: createHttpRequest( + endpoints, + // Ignore transport errors for telemetry + noop + ), flushController: createFlushController({ - pageMayExitObservable, + pageMayExitObservable: createPageMayExitObservable(configuration), // We don't use an actual session expire observable here, to make telemetry collection // independent of the session. This allows to start and send telemetry events earlier. diff --git a/packages/core/test/emulate/mockTelemetry.ts b/packages/core/test/emulate/mockTelemetry.ts index 145066b449..8e7a746b9d 100644 --- a/packages/core/test/emulate/mockTelemetry.ts +++ b/packages/core/test/emulate/mockTelemetry.ts @@ -51,6 +51,5 @@ export function createFakeTelemetryObject(): Telemetry { stop: noop, enabled: false, metricsEnabled: false, - startTransport: noop, } } diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index b5ddc5e253..aa3afd7a74 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -1,13 +1,4 @@ -import type { - TrackingConsent, - PublicApi, - ContextManager, - Account, - Context, - User, - Telemetry, - AbstractHooks, -} from '@datadog/browser-core' +import type { TrackingConsent, PublicApi, ContextManager, Account, Context, User } from '@datadog/browser-core' import { ContextManagerMethod, CustomerContextKey, @@ -281,13 +272,12 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { let strategy = createPreStartStrategy( buildCommonContext, trackingConsentState, - (initConfiguration, configuration, telemetry, hooks) => { + (initConfiguration, configuration, hooks) => { const startLogsResult = startLogsImpl( configuration, buildCommonContext, trackingConsentState, bufferedDataObservable, - telemetry, hooks ) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 3d11d66f38..8fcceb903f 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -1,4 +1,4 @@ -import type { TrackingConsentState, Telemetry } from '@datadog/browser-core' +import type { TrackingConsentState } from '@datadog/browser-core' import { createBoundedBuffer, canUseEventBridge, @@ -28,7 +28,6 @@ import type { StartLogsResult } from './startLogs' export type DoStartLogs = ( initConfiguration: LogsInitConfiguration, configuration: LogsConfiguration, - telemetry: Telemetry, hooks: Hooks ) => StartLogsResult @@ -51,17 +50,16 @@ export function createPreStartStrategy( let cachedInitConfiguration: LogsInitConfiguration | undefined let cachedConfiguration: LogsConfiguration | undefined - let telemetry: Telemetry | undefined const hooks = createHooks() const trackingConsentStateSubscription = trackingConsentState.observable.subscribe(tryStartLogs) function tryStartLogs() { - if (!cachedConfiguration || !cachedInitConfiguration || !telemetry || !trackingConsentState.isGranted()) { + if (!cachedConfiguration || !cachedInitConfiguration || !trackingConsentState.isGranted()) { return } trackingConsentStateSubscription.unsubscribe() - const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, telemetry, hooks) + const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, hooks) bufferApiCalls.drain(startLogsResult) } @@ -95,7 +93,7 @@ export function createPreStartStrategy( cachedConfiguration = configuration - telemetry = startTelemetry(TelemetryService.LOGS, configuration, hooks, {}) + startTelemetry(TelemetryService.LOGS, configuration, hooks) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index e10b43a6ba..6498426065 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -23,7 +23,6 @@ import { mockClock, expireCookie, DEFAULT_FETCH_MOCK, - createFakeTelemetryObject, } from '@datadog/browser-core/test' import type { LogsConfiguration } from '../domain/configuration' @@ -68,7 +67,6 @@ function startLogsWithDefaults( () => COMMON_CONTEXT, trackingConsentState, new BufferedObservable(100), - createFakeTelemetryObject(), createHooks() ) diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 9d6c638d0a..c06e3d07c3 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,10 +1,4 @@ -import type { - TrackingConsentState, - BufferedObservable, - BufferedData, - PageMayExitEvent, - Telemetry, -} from '@datadog/browser-core' +import type { TrackingConsentState, BufferedObservable, BufferedData, PageMayExitEvent } from '@datadog/browser-core' import { Observable, sendToExtension, @@ -13,7 +7,6 @@ import { canUseEventBridge, startAccountContext, startGlobalContext, - createIdentityEncoder, startUserContext, isWorkerEnvironment, } from '@datadog/browser-core' @@ -50,7 +43,6 @@ export function startLogs( // `trackingConsentState` set to "granted". trackingConsentState: TrackingConsentState, bufferedDataObservable: BufferedObservable, - telemetry: Telemetry, hooks: Hooks ) { const lifeCycle = new LifeCycle() @@ -64,12 +56,6 @@ export function startLogs( ? new Observable() : createPageMayExitObservable(configuration) - if (telemetry) { - telemetry.startTransport(reportError, pageMayExitObservable, createIdentityEncoder) - } - - cleanupTasks.push(telemetry.stop) - const session = configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum() ? startLogsSessionManager(configuration, trackingConsentState) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 25dc9feda9..3afe922ba9 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -158,7 +158,7 @@ export function createPreStartStrategy( cachedConfiguration = configuration - telemetry = startTelemetry(TelemetryService.RUM, configuration, hooks, {}) + telemetry = startTelemetry(TelemetryService.RUM, configuration, hooks) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 1b4984248d..623ca080d0 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -93,12 +93,6 @@ export function startRum( }) cleanupTasks.push(() => pageMayExitSubscription.unsubscribe()) - if (telemetry) { - telemetry.startTransport(reportError, pageMayExitObservable, createEncoder) - } - - cleanupTasks.push(telemetry.stop) - const session = !canUseEventBridge() ? startRumSessionManager(configuration, lifeCycle, trackingConsentState) : startRumSessionManagerStub() From 20d12a17539961dd2794eb7ae4b4465f196bf56e Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Mon, 26 Jan 2026 18:26:12 +0100 Subject: [PATCH 23/41] =?UTF-8?q?=E2=9C=85=20don't=20actually=20start=20te?= =?UTF-8?q?lemetry=20in=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logs/src/boot/logsPublicApi.spec.ts | 3 ++- packages/logs/src/boot/logsPublicApi.ts | 15 ++++++++++++--- packages/logs/src/boot/preStartLogs.spec.ts | 15 +++++++++++++-- packages/logs/src/boot/preStartLogs.ts | 5 +++-- packages/rum-core/src/boot/preStartRum.spec.ts | 4 +++- packages/rum-core/src/boot/preStartRum.ts | 5 +++-- packages/rum-core/src/boot/rumPublicApi.spec.ts | 5 +++-- packages/rum-core/src/boot/rumPublicApi.ts | 7 +++++-- 8 files changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 4fab0d4e82..2eabfab784 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -2,6 +2,7 @@ import type { ContextManager } from '@datadog/browser-core' import { monitor, display, createContextManager, TrackingConsent } from '@datadog/browser-core' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' +import { createFakeTelemetryObject } from '../../../core/test' import type { LogsPublicApi } from './logsPublicApi' import { makeLogsPublicApi } from './logsPublicApi' import type { StartLogs, StartLogsResult } from './startLogs' @@ -258,7 +259,7 @@ function makeLogsPublicApiWithDefaults({ return { startLogsSpy, - logsPublicApi: makeLogsPublicApi(startLogsSpy), + logsPublicApi: makeLogsPublicApi(startLogsSpy, createFakeTelemetryObject), getLoggedMessage, } } diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index aa3afd7a74..9014f3b8ab 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -1,4 +1,12 @@ -import type { TrackingConsent, PublicApi, ContextManager, Account, Context, User } from '@datadog/browser-core' +import type { + TrackingConsent, + PublicApi, + ContextManager, + Account, + Context, + User, + startTelemetry, +} from '@datadog/browser-core' import { ContextManagerMethod, CustomerContextKey, @@ -265,7 +273,7 @@ export interface Strategy { handleLog: StartLogsResult['handleLog'] } -export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { +export function makeLogsPublicApi(startLogsImpl: StartLogs, startTelemetryImpl?: typeof startTelemetry): LogsPublicApi { const trackingConsentState = createTrackingConsentState() const bufferedDataObservable = startBufferingData().observable @@ -283,7 +291,8 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs): LogsPublicApi { strategy = createPostStartStrategy(initConfiguration, startLogsResult) return startLogsResult - } + }, + startTelemetryImpl ) const getStrategy = () => strategy diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 4421ac8c9e..61fab6e5ca 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -1,4 +1,10 @@ -import { callbackAddsInstrumentation, type Clock, mockClock, mockEventBridge } from '@datadog/browser-core/test' +import { + callbackAddsInstrumentation, + type Clock, + mockClock, + mockEventBridge, + createFakeTelemetryObject, +} from '@datadog/browser-core/test' import type { TimeStamp, TrackingConsentState } from '@datadog/browser-core' import { ONE_SECOND, @@ -255,7 +261,12 @@ function createPreStartStrategyWithDefaults({ const getCommonContextSpy = jasmine.createSpy<() => CommonContext>() return { - strategy: createPreStartStrategy(getCommonContextSpy, trackingConsentState, doStartLogsSpy), + strategy: createPreStartStrategy( + getCommonContextSpy, + trackingConsentState, + doStartLogsSpy, + createFakeTelemetryObject + ), handleLogSpy, doStartLogsSpy, getCommonContextSpy, diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 8fcceb903f..b50681ae8a 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -34,7 +34,8 @@ export type DoStartLogs = ( export function createPreStartStrategy( getCommonContext: () => CommonContext, trackingConsentState: TrackingConsentState, - doStartLogs: DoStartLogs + doStartLogs: DoStartLogs, + startTelemetryImpl = startTelemetry ): Strategy { const bufferApiCalls = createBoundedBuffer() @@ -93,7 +94,7 @@ export function createPreStartStrategy( cachedConfiguration = configuration - startTelemetry(TelemetryService.LOGS, configuration, hooks) + startTelemetryImpl(TelemetryService.LOGS, configuration, hooks) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index 27877ef418..cc987250b4 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -19,6 +19,7 @@ import { mockEventBridge, mockSyntheticsWorkerValues, mockExperimentalFeatures, + createFakeTelemetryObject, } from '@datadog/browser-core/test' import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration' import type { ViewOptions } from '../domain/view/trackViews' @@ -823,7 +824,8 @@ function createPreStartStrategyWithDefaults({ rumPublicApiOptions, trackingConsentState, createCustomVitalsState(), - doStartRumSpy + doStartRumSpy, + createFakeTelemetryObject ), doStartRumSpy, } diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 3afe922ba9..df7a5dc67f 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -60,7 +60,8 @@ export function createPreStartStrategy( { ignoreInitIfSyntheticsWillInjectRum = true, startDeflateWorker }: RumPublicApiOptions, trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, - doStartRum: DoStartRum + doStartRum: DoStartRum, + startTelemetryImpl = startTelemetry ): Strategy { const bufferApiCalls = createBoundedBuffer() @@ -158,7 +159,7 @@ export function createPreStartStrategy( cachedConfiguration = configuration - telemetry = startTelemetry(TelemetryService.RUM, configuration, hooks) + telemetry = startTelemetryImpl(TelemetryService.RUM, configuration, hooks) // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 3423502d6d..4ae5218875 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -8,7 +8,7 @@ import { ExperimentalFeature, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { mockClock, mockExperimentalFeatures } from '@datadog/browser-core/test' +import { createFakeTelemetryObject, mockClock, mockExperimentalFeatures } from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' import { ActionType, VitalType } from '../rawRumEvent.types' import type { DurationVitalReference } from '../domain/vital/vitalCollection' @@ -1016,7 +1016,8 @@ function makeRumPublicApiWithDefaults({ startRumSpy, { ...noopRecorderApi, ...recorderApi }, { ...noopProfilerApi, ...profilerApi }, - rumPublicApiOptions + rumPublicApiOptions, + createFakeTelemetryObject ), } } diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 70ed50bdd5..0ac8f2639b 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -14,6 +14,7 @@ import type { RumInternalContext, Telemetry, Encoder, + startTelemetry, } from '@datadog/browser-core' import { ContextManagerMethod, @@ -558,7 +559,8 @@ export function makeRumPublicApi( startRumImpl: StartRum, recorderApi: RecorderApi, profilerApi: ProfilerApi, - options: RumPublicApiOptions = {} + options: RumPublicApiOptions = {}, + startTelemetryImpl?: typeof startTelemetry ): RumPublicApi { const trackingConsentState = createTrackingConsentState() const customVitalsState = createCustomVitalsState() @@ -615,7 +617,8 @@ export function makeRumPublicApi( }) return startRumResult - } + }, + startTelemetryImpl ) const getStrategy = () => strategy From c150bcdc4d3d615d8135b304600e5210315d5819 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Tue, 27 Jan 2026 12:13:27 +0100 Subject: [PATCH 24/41] =?UTF-8?q?=F0=9F=94=A5=20remove=20unrelated=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/telemetry/telemetry.spec.ts | 20 ++++++++++++++----- packages/core/src/tools/monitor.ts | 3 --- .../rum-core/src/boot/rumPublicApi.spec.ts | 15 ++------------ 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/core/src/domain/telemetry/telemetry.spec.ts b/packages/core/src/domain/telemetry/telemetry.spec.ts index 63c4619246..0c3b1c9d30 100644 --- a/packages/core/src/domain/telemetry/telemetry.spec.ts +++ b/packages/core/src/domain/telemetry/telemetry.spec.ts @@ -50,12 +50,7 @@ function startAndSpyTelemetry( const telemetry = startTelemetryCollection( TelemetryService.RUM, { - site: 'datadoghq.com', - service: 'test-service', - env: 'test-env', - version: '0.0.0', telemetrySampleRate: 100, - telemetryConfigurationSampleRate: 100, telemetryUsageSampleRate: 100, ...configuration, } as Configuration, @@ -304,6 +299,21 @@ describe('telemetry', () => { expect(events[1].application!.id).toEqual('bar') expect(events[1].session!.id).toEqual('123') }) + + it('should apply telemetry hook on events collected before telemetry is started', async () => { + addTelemetryDebug('debug 1') + + const { hooks, getTelemetryEvents } = startAndSpyTelemetry() + + hooks.register(HookNames.AssembleTelemetry, () => ({ + application: { + id: 'bar', + }, + })) + + const events = await getTelemetryEvents() + expect(events[0].application!.id).toEqual('bar') + }) }) describe('sampling', () => { diff --git a/packages/core/src/tools/monitor.ts b/packages/core/src/tools/monitor.ts index cc04d948fa..aa08f6ee36 100644 --- a/packages/core/src/tools/monitor.ts +++ b/packages/core/src/tools/monitor.ts @@ -4,9 +4,6 @@ let onMonitorErrorCollected: undefined | ((error: unknown) => void) let debugMode = false export function startMonitorErrorCollection(newOnMonitorErrorCollected: (error: unknown) => void) { - if (onMonitorErrorCollected) { - return // Already collecting, idempotent - } onMonitorErrorCollected = newOnMonitorErrorCollected } diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 4ae5218875..9f68e3e60a 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1,12 +1,5 @@ import type { RelativeTime, DeflateWorker, TimeStamp } from '@datadog/browser-core' -import { - ONE_SECOND, - display, - DefaultPrivacyLevel, - timeStampToClocks, - TrackingConsent, - ExperimentalFeature, -} from '@datadog/browser-core' +import { ONE_SECOND, display, DefaultPrivacyLevel, timeStampToClocks, ExperimentalFeature } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { createFakeTelemetryObject, mockClock, mockExperimentalFeatures } from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' @@ -46,11 +39,7 @@ const noopStartRum = (): ReturnType => ({ startAction: () => undefined, stopAction: () => undefined, }) -const DEFAULT_INIT_CONFIGURATION = { - applicationId: 'xxx', - clientToken: 'xxx', - trackingConsent: TrackingConsent.GRANTED, -} +const DEFAULT_INIT_CONFIGURATION = { applicationId: 'xxx', clientToken: 'xxx' } const FAKE_WORKER = {} as DeflateWorker describe('rum public api', () => { From 6f1bec7ee253d688112d8898ef0ce36ee92ebd41 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Tue, 27 Jan 2026 12:13:27 +0100 Subject: [PATCH 25/41] =?UTF-8?q?=F0=9F=90=9B=20fix=20telemetry=20crash=20?= =?UTF-8?q?in=20Workers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/browser/pageMayExitObservable.ts | 5 +++++ packages/logs/src/boot/startLogs.ts | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/browser/pageMayExitObservable.ts b/packages/core/src/browser/pageMayExitObservable.ts index 6a850e17d8..8808db3d4e 100644 --- a/packages/core/src/browser/pageMayExitObservable.ts +++ b/packages/core/src/browser/pageMayExitObservable.ts @@ -1,6 +1,7 @@ import { Observable } from '../tools/observable' import { objectValues } from '../tools/utils/polyfills' import type { Configuration } from '../domain/configuration' +import { isWorkerEnvironment } from '../tools/globalObject' import { addEventListeners, addEventListener, DOM_EVENT } from './addEventListener' export const PageExitReason = { @@ -18,6 +19,10 @@ export interface PageMayExitEvent { export function createPageMayExitObservable(configuration: Configuration): Observable { return new Observable((observable) => { + if (isWorkerEnvironment) { + // Page exit is not observable in worker environments (no window/document events) + return + } const { stop: stopListeners } = addEventListeners( configuration, window, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index c06e3d07c3..5c4e4e8afc 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,6 +1,5 @@ -import type { TrackingConsentState, BufferedObservable, BufferedData, PageMayExitEvent } from '@datadog/browser-core' +import type { TrackingConsentState, BufferedObservable, BufferedData } from '@datadog/browser-core' import { - Observable, sendToExtension, createPageMayExitObservable, willSyntheticsInjectRum, @@ -8,7 +7,6 @@ import { startAccountContext, startGlobalContext, startUserContext, - isWorkerEnvironment, } from '@datadog/browser-core' import { startLogsSessionManager, startLogsSessionManagerStub } from '../domain/logsSessionManager' import type { LogsConfiguration } from '../domain/configuration' @@ -51,10 +49,7 @@ export function startLogs( lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (log) => sendToExtension('logs', log)) const reportError = startReportError(lifeCycle) - // Page exit is not observable in worker environments (no window/document events) - const pageMayExitObservable = isWorkerEnvironment - ? new Observable() - : createPageMayExitObservable(configuration) + const pageMayExitObservable = createPageMayExitObservable(configuration) const session = configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum() From b72c3ff2d8d3edb8513c98fe67475f62107f004f Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Tue, 27 Jan 2026 12:13:27 +0100 Subject: [PATCH 26/41] =?UTF-8?q?=E2=9C=85=20telemetry=20is=20sent=20uncom?= =?UTF-8?q?pressed,=20adjust=20e2e=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/scenario/transport.scenario.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/e2e/scenario/transport.scenario.ts b/test/e2e/scenario/transport.scenario.ts index 5e162f61c2..90bb5592c6 100644 --- a/test/e2e/scenario/transport.scenario.ts +++ b/test/e2e/scenario/transport.scenario.ts @@ -14,8 +14,12 @@ test.describe('transport', () => { // The last view update should be sent without compression const plainRequests = intakeRegistry.rumRequests.filter((request) => request.encoding === null) - expect(plainRequests).toHaveLength(1) - expect(plainRequests[0].events).toEqual( + expect(plainRequests).toHaveLength(2) + const telemetryEventsRequest = plainRequests.find((request) => + request.events.some((event) => event.type === 'telemetry') + ) + const rumEventsRequest = plainRequests.find((request) => request !== telemetryEventsRequest) + expect(rumEventsRequest!.events).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'view', @@ -25,7 +29,7 @@ test.describe('transport', () => { // Other data should be sent encoded const deflateRequests = intakeRegistry.rumRequests.filter((request) => request.encoding === 'deflate') - expect(deflateRequests).toHaveLength(2) + expect(deflateRequests).toHaveLength(1) expect(deflateRequests.flatMap((request) => request.events).length).toBeGreaterThan(0) }) From 772576df229b4847952e54951b8af79cdf40fbb5 Mon Sep 17 00:00:00 2001 From: Benoit Zugmeyer Date: Tue, 27 Jan 2026 17:41:58 +0100 Subject: [PATCH 27/41] =?UTF-8?q?=F0=9F=90=9B=20make=20sure=20telemetry=20?= =?UTF-8?q?isn't=20sent=20until=20consent=20is=20given?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logs/src/boot/preStartLogs.spec.ts | 35 +++++++++++--- packages/logs/src/boot/preStartLogs.ts | 4 +- .../rum-core/src/boot/preStartRum.spec.ts | 48 ++++++++++++++++++- packages/rum-core/src/boot/preStartRum.ts | 9 ++-- 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 61fab6e5ca..e95e854418 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -247,6 +247,32 @@ describe('preStartLogs', () => { expect(doStartLogsSpy).not.toHaveBeenCalled() }) }) + + describe('telemetry', () => { + it('starts telemetry during init() by default', () => { + const { strategy, startTelemetrySpy } = createPreStartStrategyWithDefaults() + strategy.init(DEFAULT_INIT_CONFIGURATION) + expect(startTelemetrySpy).toHaveBeenCalledTimes(1) + }) + + it('does not start telemetry until consent is granted', () => { + const trackingConsentState = createTrackingConsentState() + const { strategy, startTelemetrySpy } = createPreStartStrategyWithDefaults({ + trackingConsentState, + }) + + strategy.init({ + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }) + + expect(startTelemetrySpy).not.toHaveBeenCalled() + + trackingConsentState.update(TrackingConsent.GRANTED) + + expect(startTelemetrySpy).toHaveBeenCalledTimes(1) + }) + }) }) function createPreStartStrategyWithDefaults({ @@ -259,14 +285,11 @@ function createPreStartStrategyWithDefaults({ handleLog: handleLogSpy, } as unknown as StartLogsResult) const getCommonContextSpy = jasmine.createSpy<() => CommonContext>() + const startTelemetrySpy = jasmine.createSpy().and.callFake(createFakeTelemetryObject) return { - strategy: createPreStartStrategy( - getCommonContextSpy, - trackingConsentState, - doStartLogsSpy, - createFakeTelemetryObject - ), + strategy: createPreStartStrategy(getCommonContextSpy, trackingConsentState, doStartLogsSpy, startTelemetrySpy), + startTelemetrySpy, handleLogSpy, doStartLogsSpy, getCommonContextSpy, diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index b50681ae8a..b4b2b2fb6a 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -59,6 +59,8 @@ export function createPreStartStrategy( return } + startTelemetryImpl(TelemetryService.LOGS, cachedConfiguration, hooks) + trackingConsentStateSubscription.unsubscribe() const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, hooks) @@ -94,8 +96,6 @@ export function createPreStartStrategy( cachedConfiguration = configuration - startTelemetryImpl(TelemetryService.LOGS, configuration, hooks) - // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer // library (Apollo Client) is storing uninstrumented fetch to be used later diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index cc987250b4..431853227a 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -809,6 +809,50 @@ describe('preStartRum', () => { expect(doStartRumSpy).not.toHaveBeenCalled() }) }) + + describe('telemetry', () => { + it('starts telemetry during init() by default', () => { + const { strategy, startTelemetrySpy } = createPreStartStrategyWithDefaults() + strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + expect(startTelemetrySpy).toHaveBeenCalledTimes(1) + }) + + it('does not start telemetry until consent is granted', () => { + const trackingConsentState = createTrackingConsentState() + const { strategy, startTelemetrySpy } = createPreStartStrategyWithDefaults({ + trackingConsentState, + }) + + strategy.init( + { + ...DEFAULT_INIT_CONFIGURATION, + trackingConsent: TrackingConsent.NOT_GRANTED, + }, + PUBLIC_API + ) + + expect(startTelemetrySpy).not.toHaveBeenCalled() + + trackingConsentState.update(TrackingConsent.GRANTED) + + expect(startTelemetrySpy).toHaveBeenCalledTimes(1) + }) + + it('starts telemetry only once', () => { + const trackingConsentState = createTrackingConsentState() + const { strategy, startTelemetrySpy } = createPreStartStrategyWithDefaults({ + trackingConsentState, + }) + + strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) + + expect(startTelemetrySpy).toHaveBeenCalledTimes(1) + + strategy.startView({ name: 'foo' }) + + expect(startTelemetrySpy).toHaveBeenCalledTimes(1) + }) + }) }) function createPreStartStrategyWithDefaults({ @@ -819,14 +863,16 @@ function createPreStartStrategyWithDefaults({ trackingConsentState?: TrackingConsentState } = {}) { const doStartRumSpy = jasmine.createSpy() + const startTelemetrySpy = jasmine.createSpy().and.callFake(createFakeTelemetryObject) return { strategy: createPreStartStrategy( rumPublicApiOptions, trackingConsentState, createCustomVitalsState(), doStartRumSpy, - createFakeTelemetryObject + startTelemetrySpy ), doStartRumSpy, + startTelemetrySpy, } } diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index df7a5dc67f..faa73de506 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -90,10 +90,15 @@ export function createPreStartStrategy( const emptyContext: Context = {} function tryStartRum() { - if (!cachedInitConfiguration || !cachedConfiguration || !telemetry || !trackingConsentState.isGranted()) { + if (!cachedInitConfiguration || !cachedConfiguration || !trackingConsentState.isGranted()) { return } + // Start telemetry only once, when we have consent and configuration + if (!telemetry) { + telemetry = startTelemetryImpl(TelemetryService.RUM, cachedConfiguration, hooks) + } + trackingConsentStateSubscription.unsubscribe() let initialViewOptions: ViewOptions | undefined @@ -159,8 +164,6 @@ export function createPreStartStrategy( cachedConfiguration = configuration - telemetry = startTelemetryImpl(TelemetryService.RUM, configuration, hooks) - // Instrument fetch to track network requests // This is needed in case the consent is not granted and some customer // library (Apollo Client) is storing uninstrumented fetch to be used later From 15c3871a11e01cdc54c9aca54afa9062fb1c7438 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 28 Jan 2026 12:59:30 +0100 Subject: [PATCH 28/41] =?UTF-8?q?=E2=9C=85=20fix=20preStartRum=20tests=20t?= =?UTF-8?q?o=20handle=20async=20session=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rum-core/src/boot/preStartRum.spec.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index 115be9b99f..009ee8f425 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -21,6 +21,7 @@ import { mockSyntheticsWorkerValues, mockExperimentalFeatures, createFakeTelemetryObject, + waitFor, } from '@datadog/browser-core/test' import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration' import type { ViewOptions } from '../domain/view/trackViews' @@ -56,8 +57,9 @@ describe('preStartRum', () => { ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) }) - it('should start when the configuration is valid', () => { + it('should start when the configuration is valid', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await waitFor(() => doStartRumSpy.calls.any()) expect(displaySpy).not.toHaveBeenCalled() expect(doStartRumSpy).toHaveBeenCalled() }) @@ -188,7 +190,7 @@ describe('preStartRum', () => { expect(doStartRumSpy).not.toHaveBeenCalled() }) - it('when false, does not ignore init() call even if Synthetics will inject its own instance of RUM', () => { + it('when false, does not ignore init() call even if Synthetics will inject its own instance of RUM', async () => { mockSyntheticsWorkerValues({ injectsRum: true }) const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults({ @@ -197,6 +199,7 @@ describe('preStartRum', () => { }, }) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await waitFor(() => doStartRumSpy.calls.any()) expect(doStartRumSpy).toHaveBeenCalled() }) @@ -218,8 +221,9 @@ describe('preStartRum', () => { }) describe('with compressIntakeRequests: false', () => { - it('does not create a deflate worker', () => { + it('does not create a deflate worker', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await waitFor(() => doStartRumSpy.calls.any()) expect(startDeflateWorkerSpy).not.toHaveBeenCalled() const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] @@ -228,7 +232,7 @@ describe('preStartRum', () => { }) describe('with compressIntakeRequests: true', () => { - it('creates a deflate worker instance', () => { + it('creates a deflate worker instance', async () => { strategy.init( { ...DEFAULT_INIT_CONFIGURATION, @@ -236,6 +240,7 @@ describe('preStartRum', () => { }, PUBLIC_API ) + await waitFor(() => doStartRumSpy.calls.any()) expect(startDeflateWorkerSpy).toHaveBeenCalledTimes(1) const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] @@ -300,7 +305,7 @@ describe('preStartRum', () => { expect(doStartRumSpy).toHaveBeenCalled() }) - it('before init startView should be handled after init', () => { + it('before init startView should be handled after init', async () => { clock = mockClock() clock.tick(10) @@ -310,6 +315,7 @@ describe('preStartRum', () => { clock.tick(20) strategy.init(AUTO_CONFIGURATION, PUBLIC_API) + await waitFor(() => startViewSpy.calls.any()) expect(startViewSpy).toHaveBeenCalled() expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo' }) @@ -327,12 +333,13 @@ describe('preStartRum', () => { expect(doStartRumSpy).not.toHaveBeenCalled() }) - it('calling startView then init should start rum', () => { + it('calling startView then init should start rum', async () => { strategy.startView({ name: 'foo' }) expect(doStartRumSpy).not.toHaveBeenCalled() expect(startViewSpy).not.toHaveBeenCalled() strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) + await waitFor(() => doStartRumSpy.calls.any()) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) @@ -352,7 +359,7 @@ describe('preStartRum', () => { expect(doStartRumSpy).not.toHaveBeenCalled() }) - it('calling startView twice before init should start rum and create a new view', () => { + it('calling startView twice before init should start rum and create a new view', async () => { clock = mockClock() clock.tick(10) strategy.startView({ name: 'foo' }) @@ -362,6 +369,7 @@ describe('preStartRum', () => { clock.tick(10) strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) + await waitFor(() => doStartRumSpy.calls.any()) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] @@ -369,19 +377,20 @@ describe('preStartRum', () => { expect(startViewSpy).toHaveBeenCalledOnceWith({ name: 'bar' }, relativeToClocks(clock.relative(20))) }) - it('calling init then startView should start rum', () => { + it('calling init then startView should start rum', async () => { strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) expect(doStartRumSpy).not.toHaveBeenCalled() expect(startViewSpy).not.toHaveBeenCalled() strategy.startView({ name: 'foo' }) + await waitFor(() => doStartRumSpy.calls.any()) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) expect(startViewSpy).not.toHaveBeenCalled() }) - it('API calls should be handled in order', () => { + it('API calls should be handled in order', async () => { clock = mockClock() clock.tick(10) @@ -395,6 +404,7 @@ describe('preStartRum', () => { clock.tick(10) strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) + await waitFor(() => addTimingSpy.calls.any()) expect(addTimingSpy).toHaveBeenCalledTimes(2) @@ -414,7 +424,7 @@ describe('preStartRum', () => { interceptor = interceptRequests() }) - it('should start with the remote configuration when a remoteConfigurationId is provided', (done) => { + it('should start with the remote configuration when a remoteConfigurationId is provided', async () => { interceptor.withFetch(() => Promise.resolve({ ok: true, @@ -422,11 +432,6 @@ describe('preStartRum', () => { }) ) const { strategy, doStartRumSpy } = createPreStartStrategyWithDefaults() - doStartRumSpy.and.callFake((configuration) => { - expect(configuration.sessionSampleRate).toEqual(50) - done() - return {} as StartRumResult - }) strategy.init( { ...DEFAULT_INIT_CONFIGURATION, @@ -434,6 +439,8 @@ describe('preStartRum', () => { }, PUBLIC_API ) + await waitFor(() => doStartRumSpy.calls.any()) + expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(50) }) }) From 5ded0ca86e90743a1cb652794dd2f9e27723e566 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 28 Jan 2026 16:02:20 +0100 Subject: [PATCH 29/41] =?UTF-8?q?=F0=9F=90=9B=20refactor=20telemetry=20ini?= =?UTF-8?q?tialization=20to=20ensure=20it=20starts=20only=20after=20consen?= =?UTF-8?q?t=20is=20granted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logs/src/boot/preStartLogs.ts | 3 +-- packages/rum-core/src/boot/preStartRum.ts | 18 +++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index bf259d6bb7..52d217ec5c 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -64,8 +64,6 @@ export function createPreStartStrategy( return } - startTelemetryImpl(TelemetryService.LOGS, cachedConfiguration, hooks) - trackingConsentStateSubscription.unsubscribe() const startLogsResult = doStartLogs(cachedInitConfiguration, cachedConfiguration, sessionManager, hooks) @@ -110,6 +108,7 @@ export function createPreStartStrategy( trackingConsentState.tryToInit(configuration.trackingConsent) trackingConsentState.onGrantedOnce(() => { + startTelemetryImpl(TelemetryService.LOGS, configuration, hooks) if (configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum()) { startLogsSessionManager(configuration, trackingConsentState, (newSessionManager) => { sessionManager = newSessionManager diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 40a31f7d44..660eeb1450 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -94,15 +94,10 @@ export function createPreStartStrategy( const emptyContext: Context = {} function tryStartRum() { - if (!cachedInitConfiguration || !cachedConfiguration || !sessionManager) { + if (!cachedInitConfiguration || !cachedConfiguration || !sessionManager || !telemetry) { return } - // Start telemetry only once, when we have consent and configuration - if (!telemetry) { - telemetry = startTelemetryImpl(TelemetryService.RUM, cachedConfiguration, hooks) - } - trackingConsentStateSubscription.unsubscribe() let initialViewOptions: ViewOptions | undefined @@ -121,7 +116,14 @@ export function createPreStartStrategy( initialViewOptions = firstStartViewCall.options } - const startRumResult = doStartRum(cachedConfiguration, sessionManager, deflateWorker, initialViewOptions, telemetry, hooks) + const startRumResult = doStartRum( + cachedConfiguration, + sessionManager, + deflateWorker, + initialViewOptions, + telemetry, + hooks + ) bufferApiCalls.drain(startRumResult) } @@ -177,6 +179,8 @@ export function createPreStartStrategy( trackingConsentState.tryToInit(configuration.trackingConsent) trackingConsentState.onGrantedOnce(() => { + telemetry = startTelemetryImpl(TelemetryService.RUM, configuration, hooks) + if (canUseEventBridge()) { sessionManager = startRumSessionManagerStub() tryStartRum() From 7a5263d1e0542987b3dd206da3e8304895cd00fb Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 28 Jan 2026 16:02:57 +0100 Subject: [PATCH 30/41] =?UTF-8?q?=F0=9F=90=9B=20remove=20safePersist=20fun?= =?UTF-8?q?ction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/session/sessionStoreOperations.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index 9d27a4eab5..d00fbbce34 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -2,7 +2,6 @@ import { setTimeout } from '../../tools/timer' import { generateUUID } from '../../tools/utils/stringUtils' import type { TimeStamp } from '../../tools/utils/timeUtils' import { elapsed, ONE_SECOND, timeStampNow } from '../../tools/utils/timeUtils' -import { addTelemetryError } from '../telemetry' import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' import { expandSessionState, isSessionInExpiredState } from './sessionState' @@ -23,14 +22,6 @@ const LOCK_SEPARATOR = '--' const bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined -function safePersist(persistFn: () => void) { - try { - persistFn() - } catch (e) { - addTelemetryError(e) - } -} - export function processSessionStoreOperations( operations: Operations, sessionStoreStrategy: SessionStoreStrategy, @@ -67,7 +58,7 @@ export function processSessionStoreOperations( } // acquire lock currentLock = createLock() - safePersist(() => persistWithLock(currentStore.session)) + persistWithLock(currentStore.session) // if lock is not acquired, retry later currentStore = retrieveStore() if (currentStore.lock !== currentLock) { @@ -86,13 +77,13 @@ export function processSessionStoreOperations( } if (processedSession) { if (isSessionInExpiredState(processedSession)) { - safePersist(() => expireSession(processedSession!)) + expireSession(processedSession) } else { expandSessionState(processedSession) if (isLockEnabled) { - safePersist(() => persistWithLock(processedSession!)) + persistWithLock(processedSession) } else { - safePersist(() => persistSession(processedSession!)) + persistSession(processedSession) } } } @@ -106,7 +97,7 @@ export function processSessionStoreOperations( retryLater(operations, sessionStoreStrategy, numberOfRetries) return } - safePersist(() => persistSession(currentStore.session)) + persistSession(currentStore.session) processedSession = currentStore.session } } From 4d5bb57a9ae5ba7ea821dba7ec801c45f91ed0be Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 28 Jan 2026 16:03:10 +0100 Subject: [PATCH 31/41] =?UTF-8?q?=F0=9F=94=A7=20clean=20up=20imports=20in?= =?UTF-8?q?=20logs=20and=20rum=20core=20test=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logs/src/boot/logsPublicApi.spec.ts | 3 +-- packages/logs/src/boot/preStartLogs.spec.ts | 1 - packages/rum-core/src/boot/rumPublicApi.spec.ts | 17 +++++++++++++++-- packages/rum-core/src/boot/startRum.ts | 1 - 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 85d8810345..770051722c 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,7 +1,6 @@ -import type { ContextManager, TimeStamp } from '@datadog/browser-core' +import type { ContextManager } from '@datadog/browser-core' import { monitor, display, createContextManager, stopSessionManager, TrackingConsent } from '@datadog/browser-core' import { waitFor } from '@datadog/browser-core/test' -import type { Logger, LogsMessage } from '../domain/logger' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' import { createFakeTelemetryObject } from '../../../core/test' diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 50a9797a7e..3a7f9914d5 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -23,7 +23,6 @@ import type { CommonContext } from '../rawLogsEvent.types' import type { HybridInitConfiguration, LogsInitConfiguration } from '../domain/configuration' import type { Logger } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' -import type { LogsSessionManager } from '../domain/logsSessionManager' import type { Strategy } from './logsPublicApi' import type { DoStartLogs } from './preStartLogs' import { createPreStartStrategy } from './preStartLogs' diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 1443a974f1..9d2a6c99d4 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1,7 +1,20 @@ import type { RelativeTime, DeflateWorker, TimeStamp } from '@datadog/browser-core' -import { ONE_SECOND, display, DefaultPrivacyLevel, timeStampToClocks, stopSessionManager, ExperimentalFeature } from '@datadog/browser-core' +import { + ONE_SECOND, + display, + DefaultPrivacyLevel, + timeStampToClocks, + stopSessionManager, + ExperimentalFeature, +} from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { waitFor, mockClock, mockEventBridge, createFakeTelemetryObject, mockExperimentalFeatures } from '@datadog/browser-core/test' +import { + waitFor, + mockClock, + mockEventBridge, + createFakeTelemetryObject, + mockExperimentalFeatures, +} from '@datadog/browser-core/test' import { noopRecorderApi, noopProfilerApi } from '../../test' import { ActionType, VitalType } from '../rawRumEvent.types' import type { DurationVitalReference } from '../domain/vital/vitalCollection' diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index a3302755c0..6ed6a1db73 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -96,7 +96,6 @@ export function startRum( }) cleanupTasks.push(() => pageMayExitSubscription.unsubscribe()) - if (!canUseEventBridge()) { const batch = startRumBatch( configuration, From 68408ab3c5da0e088d98009073a8cbb0eaaf9f72 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 29 Jan 2026 08:40:54 +0100 Subject: [PATCH 32/41] =?UTF-8?q?=E2=9C=85=20replace=20waitFor=20with=20co?= =?UTF-8?q?llectAsyncCalls=20in=20test=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace polling-based waitFor with the cleaner collectAsyncCalls pattern which waits for a specific number of spy calls and fails fast on extra calls. Co-Authored-By: Claude Opus 4.5 --- packages/logs/src/boot/logsPublicApi.spec.ts | 9 ++- packages/logs/src/boot/preStartLogs.spec.ts | 30 +++---- .../rum-core/src/boot/preStartRum.spec.ts | 22 ++--- .../rum-core/src/boot/rumPublicApi.spec.ts | 81 +++++++++---------- 4 files changed, 71 insertions(+), 71 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.spec.ts b/packages/logs/src/boot/logsPublicApi.spec.ts index 770051722c..1207e07be9 100644 --- a/packages/logs/src/boot/logsPublicApi.spec.ts +++ b/packages/logs/src/boot/logsPublicApi.spec.ts @@ -1,6 +1,6 @@ import type { ContextManager } from '@datadog/browser-core' import { monitor, display, createContextManager, stopSessionManager, TrackingConsent } from '@datadog/browser-core' -import { waitFor } from '@datadog/browser-core/test' +import { collectAsyncCalls } from '@datadog/browser-core/test' import { HandlerType } from '../domain/logger' import { StatusType } from '../domain/logger/isAuthorized' import { createFakeTelemetryObject } from '../../../core/test' @@ -56,7 +56,7 @@ describe('logs entry', () => { beforeEach(async () => { ;({ logsPublicApi, startLogsSpy } = makeLogsPublicApiWithDefaults()) logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => startLogsSpy.calls.count() > 0) + await collectAsyncCalls(startLogsSpy, 1) }) it('should have the current date, view and global context', () => { @@ -75,11 +75,12 @@ describe('logs entry', () => { describe('post start API usages', () => { let logsPublicApi: LogsPublicApi let getLoggedMessage: ReturnType['getLoggedMessage'] + let startLogsSpy: jasmine.Spy beforeEach(async () => { - ;({ logsPublicApi, getLoggedMessage } = makeLogsPublicApiWithDefaults()) + ;({ logsPublicApi, getLoggedMessage, startLogsSpy } = makeLogsPublicApiWithDefaults()) logsPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => getLoggedMessage(0) !== undefined || true) + await collectAsyncCalls(startLogsSpy, 1) }) it('main logger logs a message', () => { diff --git a/packages/logs/src/boot/preStartLogs.spec.ts b/packages/logs/src/boot/preStartLogs.spec.ts index 3a7f9914d5..8845d7a355 100644 --- a/packages/logs/src/boot/preStartLogs.spec.ts +++ b/packages/logs/src/boot/preStartLogs.spec.ts @@ -1,11 +1,11 @@ import { callbackAddsInstrumentation, + collectAsyncCalls, type Clock, mockClock, mockEventBridge, mockSyntheticsWorkerValues, waitNextMicrotask, - waitFor, createFakeTelemetryObject, } from '@datadog/browser-core/test' import type { TimeStamp, TrackingConsentState } from '@datadog/browser-core' @@ -56,7 +56,7 @@ describe('preStartLogs', () => { it('should start when the configuration is valid', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION) expect(displaySpy).not.toHaveBeenCalled() - await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) + await collectAsyncCalls(doStartLogsSpy, 1) expect(doStartLogsSpy).toHaveBeenCalled() }) @@ -128,7 +128,7 @@ describe('preStartLogs', () => { expect(handleLogSpy).not.toHaveBeenCalled() strategy.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => handleLogSpy.calls.count() > 0, { timeout: 2000 }) + await collectAsyncCalls(handleLogSpy, 1) expect(handleLogSpy.calls.all().length).toBe(1) expect(getLoggedMessage(0).message.message).toBe('message') @@ -157,7 +157,7 @@ describe('preStartLogs', () => { }) it('saves the URL', async () => { - const { strategy, getLoggedMessage, getCommonContextSpy } = createPreStartStrategyWithDefaults() + const { strategy, getLoggedMessage, getCommonContextSpy, handleLogSpy } = createPreStartStrategyWithDefaults() getCommonContextSpy.and.returnValue({ view: { url: 'url' } } as unknown as CommonContext) strategy.handleLog( { @@ -168,12 +168,12 @@ describe('preStartLogs', () => { ) strategy.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => getLoggedMessage(0) !== undefined) + await collectAsyncCalls(handleLogSpy, 1) expect(getLoggedMessage(0).savedCommonContext!.view?.url).toEqual('url') }) it('saves the log context', async () => { - const { strategy, getLoggedMessage } = createPreStartStrategyWithDefaults() + const { strategy, getLoggedMessage, handleLogSpy } = createPreStartStrategyWithDefaults() const context = { foo: 'bar' } strategy.handleLog( { @@ -186,7 +186,7 @@ describe('preStartLogs', () => { context.foo = 'baz' strategy.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => getLoggedMessage(0) !== undefined) + await collectAsyncCalls(handleLogSpy, 1) expect(getLoggedMessage(0).message.context!.foo).toEqual('bar') }) @@ -238,7 +238,7 @@ describe('preStartLogs', () => { ...DEFAULT_INIT_CONFIGURATION, trackingConsent: TrackingConsent.NOT_GRANTED, }) - await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) + await collectAsyncCalls(doStartLogsSpy, 1) expect(doStartLogsSpy).toHaveBeenCalledTimes(1) }) @@ -253,7 +253,7 @@ describe('preStartLogs', () => { it('do not call startLogs when tracking consent state is updated after init', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) + await collectAsyncCalls(doStartLogsSpy, 1) doStartLogsSpy.calls.reset() trackingConsentState.update(TrackingConsent.GRANTED) @@ -269,7 +269,7 @@ describe('preStartLogs', () => { const { strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults() strategy.init({ ...DEFAULT_INIT_CONFIGURATION, sessionSampleRate: 0 }) - await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) + await collectAsyncCalls(doStartLogsSpy, 1) const sessionManager = doStartLogsSpy.calls.mostRecent().args[2] expect(sessionManager.findTrackedSession()).toBeUndefined() }) @@ -279,7 +279,7 @@ describe('preStartLogs', () => { const { strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults() strategy.init({ ...DEFAULT_INIT_CONFIGURATION, sessionSampleRate: 100 }) - await waitFor(() => doStartLogsSpy.calls.count() > 0, { timeout: 2000 }) + await collectAsyncCalls(doStartLogsSpy, 1) const sessionManager = doStartLogsSpy.calls.mostRecent().args[2] expect(sessionManager.findTrackedSession()).toBeTruthy() }) @@ -287,10 +287,10 @@ describe('preStartLogs', () => { describe('logs session creation', () => { it('creates a session on normal conditions', async () => { - const { strategy } = createPreStartStrategyWithDefaults() + const { strategy, doStartLogsSpy } = createPreStartStrategyWithDefaults() strategy.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => getCookie(SESSION_STORE_KEY) !== undefined, { timeout: 2000 }) + await collectAsyncCalls(doStartLogsSpy, 1) expect(getCookie(SESSION_STORE_KEY)).toBeDefined() }) @@ -313,7 +313,7 @@ describe('preStartLogs', () => { it('starts telemetry during init() by default', async () => { const { strategy, startTelemetrySpy } = createPreStartStrategyWithDefaults() strategy.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => startTelemetrySpy.calls.count() > 0) + await collectAsyncCalls(startTelemetrySpy, 1) expect(startTelemetrySpy).toHaveBeenCalledTimes(1) }) @@ -331,7 +331,7 @@ describe('preStartLogs', () => { expect(startTelemetrySpy).not.toHaveBeenCalled() trackingConsentState.update(TrackingConsent.GRANTED) - await waitFor(() => startTelemetrySpy.calls.count() > 0) + await collectAsyncCalls(startTelemetrySpy, 1) expect(startTelemetrySpy).toHaveBeenCalledTimes(1) }) diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index 009ee8f425..d605f91668 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -15,13 +15,13 @@ import { import type { Clock } from '@datadog/browser-core/test' import { callbackAddsInstrumentation, + collectAsyncCalls, interceptRequests, mockClock, mockEventBridge, mockSyntheticsWorkerValues, mockExperimentalFeatures, createFakeTelemetryObject, - waitFor, } from '@datadog/browser-core/test' import type { HybridInitConfiguration, RumInitConfiguration } from '../domain/configuration' import type { ViewOptions } from '../domain/view/trackViews' @@ -59,7 +59,7 @@ describe('preStartRum', () => { it('should start when the configuration is valid', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(displaySpy).not.toHaveBeenCalled() expect(doStartRumSpy).toHaveBeenCalled() }) @@ -199,7 +199,7 @@ describe('preStartRum', () => { }, }) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() }) @@ -223,7 +223,7 @@ describe('preStartRum', () => { describe('with compressIntakeRequests: false', () => { it('does not create a deflate worker', async () => { strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(startDeflateWorkerSpy).not.toHaveBeenCalled() const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] @@ -240,7 +240,7 @@ describe('preStartRum', () => { }, PUBLIC_API ) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(startDeflateWorkerSpy).toHaveBeenCalledTimes(1) const worker: DeflateWorker | undefined = doStartRumSpy.calls.mostRecent().args[2] @@ -315,7 +315,7 @@ describe('preStartRum', () => { clock.tick(20) strategy.init(AUTO_CONFIGURATION, PUBLIC_API) - await waitFor(() => startViewSpy.calls.any()) + await collectAsyncCalls(startViewSpy, 1) expect(startViewSpy).toHaveBeenCalled() expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo' }) @@ -339,7 +339,7 @@ describe('preStartRum', () => { expect(startViewSpy).not.toHaveBeenCalled() strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) @@ -369,7 +369,7 @@ describe('preStartRum', () => { clock.tick(10) strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] @@ -383,7 +383,7 @@ describe('preStartRum', () => { expect(startViewSpy).not.toHaveBeenCalled() strategy.startView({ name: 'foo' }) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] expect(initialViewOptions).toEqual({ name: 'foo' }) @@ -404,7 +404,7 @@ describe('preStartRum', () => { clock.tick(10) strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) - await waitFor(() => addTimingSpy.calls.any()) + await collectAsyncCalls(addTimingSpy, 2) expect(addTimingSpy).toHaveBeenCalledTimes(2) @@ -439,7 +439,7 @@ describe('preStartRum', () => { }, PUBLIC_API ) - await waitFor(() => doStartRumSpy.calls.any()) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy.calls.mostRecent().args[0].sessionSampleRate).toEqual(50) }) }) diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 9d2a6c99d4..a715abd14f 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -9,7 +9,7 @@ import { } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' import { - waitFor, + collectAsyncCalls, mockClock, mockEventBridge, createFakeTelemetryObject, @@ -82,7 +82,7 @@ describe('rum public api', () => { ...DEFAULT_INIT_CONFIGURATION, compressIntakeRequests: true, }) - await waitFor(() => recorderApiOnRumStartSpy.calls.count() > 0) + await collectAsyncCalls(recorderApiOnRumStartSpy, 1) expect(recorderApiOnRumStartSpy.calls.mostRecent().args[4]).toBe(FAKE_WORKER) }) }) @@ -91,13 +91,14 @@ describe('rum public api', () => { describe('getInternalContext', () => { let getInternalContextSpy: jasmine.Spy['getInternalContext']> let rumPublicApi: RumPublicApi + let startRumSpy: jasmine.Spy beforeEach(() => { getInternalContextSpy = jasmine.createSpy().and.callFake(() => ({ application_id: '123', session_id: '123', })) - ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + ;({ rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ startRumResult: { getInternalContext: getInternalContextSpy, }, @@ -106,7 +107,7 @@ describe('rum public api', () => { it('returns the internal context after init', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => rumPublicApi.getInternalContext() !== undefined) + await collectAsyncCalls(startRumSpy, 1) expect(rumPublicApi.getInternalContext()).toEqual({ application_id: '123', session_id: '123' }) expect(getInternalContextSpy).toHaveBeenCalled() @@ -114,7 +115,7 @@ describe('rum public api', () => { it('uses the startTime if specified', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => rumPublicApi.getInternalContext() !== undefined) + await collectAsyncCalls(startRumSpy, 1) const startTime = 234832890 expect(rumPublicApi.getInternalContext(startTime)).toEqual({ application_id: '123', session_id: '123' }) @@ -548,20 +549,20 @@ describe('rum public api', () => { it('should add custom timings', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.addTiming('foo') - await waitFor(() => addTimingSpy.calls.count() > 0) + const calls = await collectAsyncCalls(addTimingSpy, 1) - expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('foo') - expect(addTimingSpy.calls.argsFor(0)[1]).toBeUndefined() + expect(calls.argsFor(0)[0]).toEqual('foo') + expect(calls.argsFor(0)[1]).toBeUndefined() expect(displaySpy).not.toHaveBeenCalled() }) it('adds custom timing with provided time', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.addTiming('foo', 12) - await waitFor(() => addTimingSpy.calls.count() > 0) + const calls = await collectAsyncCalls(addTimingSpy, 1) - expect(addTimingSpy.calls.argsFor(0)[0]).toEqual('foo') - expect(addTimingSpy.calls.argsFor(0)[1]).toBe(12 as RelativeTime) + expect(calls.argsFor(0)[0]).toEqual('foo') + expect(calls.argsFor(0)[1]).toBe(12 as RelativeTime) expect(displaySpy).not.toHaveBeenCalled() }) }) @@ -584,9 +585,9 @@ describe('rum public api', () => { it('should add feature flag evaluation when ff feature_flags enabled', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.addFeatureFlagEvaluation('feature', 'foo') - await waitFor(() => addFeatureFlagEvaluationSpy.calls.count() > 0) + const calls = await collectAsyncCalls(addFeatureFlagEvaluationSpy, 1) - expect(addFeatureFlagEvaluationSpy.calls.argsFor(0)).toEqual(['feature', 'foo']) + expect(calls.argsFor(0)).toEqual(['feature', 'foo']) expect(displaySpy).not.toHaveBeenCalled() }) }) @@ -601,7 +602,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.stopSession() - await waitFor(() => stopSessionSpy.calls.count() > 0) + await collectAsyncCalls(stopSessionSpy, 1) expect(stopSessionSpy).toHaveBeenCalled() }) }) @@ -616,8 +617,8 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView('foo') - await waitFor(() => startViewSpy.calls.count() > 0) - expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ name: 'foo' }) + const calls = await collectAsyncCalls(startViewSpy, 1) + expect(calls.argsFor(0)[0]).toEqual({ name: 'foo' }) }) it('should call RUM results startView with the view options', async () => { @@ -629,8 +630,8 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView({ name: 'foo', service: 'bar', version: 'baz', context: { foo: 'bar' } }) - await waitFor(() => startViewSpy.calls.count() > 0) - expect(startViewSpy.calls.argsFor(0)[0]).toEqual({ + const calls = await collectAsyncCalls(startViewSpy, 1) + expect(calls.argsFor(0)[0]).toEqual({ name: 'foo', service: 'bar', version: 'baz', @@ -660,8 +661,8 @@ describe('rum public api', () => { it('is started with the default defaultPrivacyLevel', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => recorderApi.onRumStart.calls.count() > 0) - expect(recorderApi.onRumStart.calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) + const calls = await collectAsyncCalls(recorderApi.onRumStart, 1) + expect(calls.mostRecent().args[1].defaultPrivacyLevel).toBe(DefaultPrivacyLevel.MASK) }) it('is started with the configured defaultPrivacyLevel', () => { @@ -686,8 +687,8 @@ describe('rum public api', () => { it('is started with the default startSessionReplayRecordingManually', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => recorderApi.onRumStart.calls.count() > 0) - expect(recorderApi.onRumStart.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(true) + const calls = await collectAsyncCalls(recorderApi.onRumStart, 1) + expect(calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(true) }) it('is started with the configured startSessionReplayRecordingManually', async () => { @@ -695,8 +696,8 @@ describe('rum public api', () => { ...DEFAULT_INIT_CONFIGURATION, startSessionReplayRecordingManually: false, }) - await waitFor(() => recorderApi.onRumStart.calls.count() > 0) - expect(recorderApi.onRumStart.calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(false) + const calls = await collectAsyncCalls(recorderApi.onRumStart, 1) + expect(calls.mostRecent().args[1].startSessionReplayRecordingManually).toBe(false) }) }) @@ -710,7 +711,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) - await waitFor(() => startDurationVitalSpy.calls.count() > 0) + await collectAsyncCalls(startDurationVitalSpy, 1) expect(startDurationVitalSpy).toHaveBeenCalledWith('foo', { description: 'description-value', context: { foo: 'bar' }, @@ -729,7 +730,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) rumPublicApi.stopDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) - await waitFor(() => stopDurationVitalSpy.calls.count() > 0) + await collectAsyncCalls(stopDurationVitalSpy, 1) expect(stopDurationVitalSpy).toHaveBeenCalledWith('foo', { description: 'description-value', context: { foo: 'bar' }, @@ -746,7 +747,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) const ref = rumPublicApi.startDurationVital('foo', { context: { foo: 'bar' }, description: 'description-value' }) rumPublicApi.stopDurationVital(ref, { context: { foo: 'bar' }, description: 'description-value' }) - await waitFor(() => stopDurationVitalSpy.calls.count() > 0) + await collectAsyncCalls(stopDurationVitalSpy, 1) expect(stopDurationVitalSpy).toHaveBeenCalledWith(ref, { description: 'description-value', context: { foo: 'bar' }, @@ -852,7 +853,7 @@ describe('rum public api', () => { context: { foo: 'bar' }, description: 'description-value', }) - await waitFor(() => addDurationVitalSpy.calls.count() > 0) + await collectAsyncCalls(addDurationVitalSpy, 1) expect(addDurationVitalSpy).toHaveBeenCalledWith({ name: 'foo', startClocks: timeStampToClocks(startTime), @@ -874,7 +875,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) - await waitFor(() => addOperationStepVitalSpy.calls.count() > 0) + await collectAsyncCalls(addOperationStepVitalSpy, 1) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'start', { operationKey: '00000000-0000-0000-0000-000000000000', }) @@ -891,7 +892,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.succeedFeatureOperation('foo', { operationKey: '00000000-0000-0000-0000-000000000000' }) - await waitFor(() => addOperationStepVitalSpy.calls.count() > 0) + await collectAsyncCalls(addOperationStepVitalSpy, 1) expect(addOperationStepVitalSpy).toHaveBeenCalledWith('foo', 'end', { operationKey: '00000000-0000-0000-0000-000000000000', }) @@ -908,7 +909,7 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.failFeatureOperation('foo', 'error', { operationKey: '00000000-0000-0000-0000-000000000000' }) - await waitFor(() => addOperationStepVitalSpy.calls.count() > 0) + await collectAsyncCalls(addOperationStepVitalSpy, 1) expect(addOperationStepVitalSpy).toHaveBeenCalledWith( 'foo', 'end', @@ -939,7 +940,7 @@ describe('rum public api', () => { it('should set the view name', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.setViewName('foo') - await waitFor(() => setViewNameSpy.calls.count() > 0) + await collectAsyncCalls(setViewNameSpy, 1) expect(setViewNameSpy).toHaveBeenCalledWith('foo') }) @@ -965,7 +966,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) /* eslint-disable @typescript-eslint/no-unsafe-call */ ;(rumPublicApi as any).setViewContext({ foo: 'bar' }) - await waitFor(() => setViewContextSpy.calls.count() > 0) + await collectAsyncCalls(setViewContextSpy, 1) expect(setViewContextSpy).toHaveBeenCalledWith({ foo: 'bar' }) }) @@ -974,7 +975,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) /* eslint-disable @typescript-eslint/no-unsafe-call */ ;(rumPublicApi as any).setViewContextProperty('foo', 'bar') - await waitFor(() => setViewContextPropertySpy.calls.count() > 0) + await collectAsyncCalls(setViewContextPropertySpy, 1) expect(setViewContextPropertySpy).toHaveBeenCalledWith('foo', 'bar') }) @@ -983,12 +984,13 @@ describe('rum public api', () => { describe('getViewContext', () => { let getViewContextSpy: jasmine.Spy['getViewContext']> let rumPublicApi: RumPublicApi + let startRumSpy: jasmine.Spy beforeEach(() => { getViewContextSpy = jasmine.createSpy().and.callFake(() => ({ foo: 'bar', })) - ;({ rumPublicApi } = makeRumPublicApiWithDefaults({ + ;({ rumPublicApi, startRumSpy } = makeRumPublicApiWithDefaults({ startRumResult: { getViewContext: getViewContextSpy, }, @@ -997,10 +999,7 @@ describe('rum public api', () => { it('should return the view context after init', async () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => { - const ctx = rumPublicApi.getViewContext() - return ctx && Object.keys(ctx).length > 0 - }) + await collectAsyncCalls(startRumSpy, 1) expect(rumPublicApi.getViewContext()).toEqual({ foo: 'bar' }) expect(getViewContextSpy).toHaveBeenCalled() @@ -1021,8 +1020,8 @@ describe('rum public api', () => { }) rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) - await waitFor(() => startRumSpy.calls.count() > 0) - const sdkName = startRumSpy.calls.argsFor(0)[11] + const calls = await collectAsyncCalls(startRumSpy, 1) + const sdkName = calls.argsFor(0)[11] expect(sdkName).toBe('rum-slim') }) }) From 36cba1f66a6d0e583e36ff9ffefd3f7fae235d92 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 29 Jan 2026 10:40:05 +0100 Subject: [PATCH 33/41] =?UTF-8?q?=F0=9F=94=A7=20add=20async=20testing=20be?= =?UTF-8?q?st=20practices=20to=20AGENTS.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index eec126b5cf..fa0609651c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,8 @@ scripts/ # Build, deploy, release automation - Spec files co-located with implementation: `feature.ts` → `feature.spec.ts` - Use `registerCleanupTask()` for cleanup, NOT `afterEach()` - Test framework: Jasmine + Karma +- Prefer `collectAsyncCalls(spy, n)` over `waitFor(() => spy.calls.count() > 0)` for waiting on spy calls +- Don't destructure methods from `spy.calls` (e.g., `argsFor`, `mostRecent`) - use `calls.argsFor()` to avoid `@typescript-eslint/unbound-method` errors ## Commit Messages From 3b4ec110a4800dfd07d094b65ff1de4aac5adcc6 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 29 Jan 2026 16:04:24 +0100 Subject: [PATCH 34/41] =?UTF-8?q?=F0=9F=90=9B=20check=20consent=20before?= =?UTF-8?q?=20completing=20async=20session=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If consent is revoked while waiting for the session store lock (e.g., Chromium cookie locking), the session should be expired instead of proceeding with initialization. This prevents a race condition where consent revocation during the lock-wait period would be missed. --- .../src/domain/session/sessionManager.spec.ts | 22 +++++++++++++++++++ .../core/src/domain/session/sessionManager.ts | 11 ++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 0175de7576..9b9a5983b9 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -624,6 +624,28 @@ describe('startSessionManager', () => { expectSessionToBeExpired(sessionManager) }) + it('expires the session when tracking consent is withdrawn during async initialization', () => { + if (!isChromium()) { + pending('the lock is only enabled in Chromium') + } + + // Set up a locked cookie to delay initialization + setCookie(SESSION_STORE_KEY, `lock=${createLock()}`, DURATION) + + const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) + void startSessionManagerWithDefaults({ trackingConsentState }) + + // Consent is revoked while waiting for lock + trackingConsentState.update(TrackingConsent.NOT_GRANTED) + + // Release the lock + setCookie(SESSION_STORE_KEY, 'id=abc123&first=tracked', DURATION) + clock.tick(LOCK_RETRY_DELAY) + + // Session should be expired due to consent revocation + expect(getSessionState(SESSION_STORE_KEY).isExpired).toBe('1') + }) + it('renews the session when tracking consent is granted', async () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) const sessionManager = await startSessionManagerWithDefaults({ trackingConsentState }) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index bf576f0188..222d89f589 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -69,9 +69,16 @@ export function startSessionManager( }) stopCallbacks.push(() => sessionContextHistory.stop()) - // We expand/renew session unconditionally as tracking consent is always granted when the session - // manager is started. + // Tracking consent is always granted when the session manager is started, but it may be revoked + // during the async initialization (e.g., while waiting for cookie lock). We check + // consent status in the callback to handle this case. sessionStore.expandOrRenewSession(() => { + const hasConsent = trackingConsentState.isGranted() + if (!hasConsent) { + sessionStore.expire(hasConsent) + return + } + sessionStore.renewObservable.subscribe(() => { sessionContextHistory.add(buildSessionContext(), relativeNow()) renewObservable.notify() From 545b8c971e2b274d82692bd00286f0b3070e1c57 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 30 Jan 2026 08:30:41 +0100 Subject: [PATCH 35/41] =?UTF-8?q?=F0=9F=94=A5=20remove=20unused=20waitFor?= =?UTF-8?q?=20test=20utility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/test/wait.ts | 44 -------------------------------------- 1 file changed, 44 deletions(-) diff --git a/packages/core/test/wait.ts b/packages/core/test/wait.ts index d3b6263ad2..1c77f3842f 100644 --- a/packages/core/test/wait.ts +++ b/packages/core/test/wait.ts @@ -7,47 +7,3 @@ export function wait(durationMs: number = 0): Promise { export function waitNextMicrotask(): Promise { return Promise.resolve() } - -export function waitFor( - callback: () => T | Promise, - options: { timeout?: number; interval?: number } = {} -): Promise { - const { timeout = 1000, interval = 50 } = options - - return new Promise((resolve, reject) => { - const startTime = Date.now() - - function check() { - try { - const result = callback() - if (result && typeof (result as any).then === 'function') { - ;(result as Promise).then(handleResult, handleError) - } else { - handleResult(result as T) - } - } catch (error) { - handleError(error as Error) - } - } - - function handleResult(result: T) { - if (result) { - resolve(result) - } else if (Date.now() - startTime >= timeout) { - reject(new Error(`waitFor timed out after ${timeout}ms`)) - } else { - setTimeout(check, interval) - } - } - - function handleError(error: Error) { - if (Date.now() - startTime >= timeout) { - reject(error) - } else { - setTimeout(check, interval) - } - } - - check() - }) -} From 25ce6031a2c5805bb7c507bdd1c2d363d4774230 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 30 Jan 2026 10:54:10 +0100 Subject: [PATCH 36/41] =?UTF-8?q?=E2=9C=85=20fix=20flaky=20preStartRum=20t?= =?UTF-8?q?ests=20for=20async=20session=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests in 'buffers API calls before starting RUM' section expected synchronous doStartRum calls but session manager initialization is now async. Added proper async/await with collectAsyncCalls to wait for the async callbacks. Fixes race conditions on slower browsers (Chrome 63, Edge 80, Firefox 67). --- .../rum-core/src/boot/preStartRum.spec.ts | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index d605f91668..087f468fc3 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -457,7 +457,7 @@ describe('preStartRum', () => { }) }) - it('plugins can edit the init configuration prior to validation', () => { + it('plugins can edit the init configuration prior to validation', async () => { const plugin: RumPlugin = { name: 'a', onInit: ({ initConfiguration }) => { @@ -472,6 +472,7 @@ describe('preStartRum', () => { } as RumInitConfiguration, PUBLIC_API ) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() expect(doStartRumSpy.calls.mostRecent().args[0].applicationId).toBe('application-id') @@ -570,7 +571,7 @@ describe('preStartRum', () => { ;({ strategy, doStartRumSpy } = createPreStartStrategyWithDefaults()) }) - it('addAction', () => { + it('addAction', async () => { const addActionSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addAction: addActionSpy } as unknown as StartRumResult) @@ -581,10 +582,11 @@ describe('preStartRum', () => { } strategy.addAction(manualAction) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addActionSpy, 1) expect(addActionSpy).toHaveBeenCalledOnceWith(manualAction) }) - it('addError', () => { + it('addError', async () => { const addErrorSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addError: addErrorSpy } as unknown as StartRumResult) @@ -596,10 +598,11 @@ describe('preStartRum', () => { } strategy.addError(error) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addErrorSpy, 1) expect(addErrorSpy).toHaveBeenCalledOnceWith(error) }) - it('startView', () => { + it('startView', async () => { const startViewSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ startView: startViewSpy } as unknown as StartRumResult) @@ -607,10 +610,11 @@ describe('preStartRum', () => { const clockState = clocksNow() strategy.startView(options, clockState) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(startViewSpy, 1) expect(startViewSpy).toHaveBeenCalledOnceWith(options, clockState) }) - it('addTiming', () => { + it('addTiming', async () => { const addTimingSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addTiming: addTimingSpy } as unknown as StartRumResult) @@ -618,38 +622,42 @@ describe('preStartRum', () => { const time = 123 as TimeStamp strategy.addTiming(name, time) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addTimingSpy, 1) expect(addTimingSpy).toHaveBeenCalledOnceWith(name, time) }) - it('setViewContext', () => { + it('setViewContext', async () => { const setViewContextSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ setViewContext: setViewContextSpy } as unknown as StartRumResult) strategy.setViewContext({ foo: 'bar' }) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(setViewContextSpy, 1) expect(setViewContextSpy).toHaveBeenCalledOnceWith({ foo: 'bar' }) }) - it('setViewContextProperty', () => { + it('setViewContextProperty', async () => { const setViewContextPropertySpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ setViewContextProperty: setViewContextPropertySpy } as unknown as StartRumResult) strategy.setViewContextProperty('foo', 'bar') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(setViewContextPropertySpy, 1) expect(setViewContextPropertySpy).toHaveBeenCalledOnceWith('foo', 'bar') }) - it('setViewName', () => { + it('setViewName', async () => { const setViewNameSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ setViewName: setViewNameSpy } as unknown as StartRumResult) const name = 'foo' strategy.setViewName(name) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(setViewNameSpy, 1) expect(setViewNameSpy).toHaveBeenCalledOnceWith(name) }) - it('addFeatureFlagEvaluation', () => { + it('addFeatureFlagEvaluation', async () => { const addFeatureFlagEvaluationSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addFeatureFlagEvaluation: addFeatureFlagEvaluationSpy, @@ -659,10 +667,11 @@ describe('preStartRum', () => { const value = 'bar' strategy.addFeatureFlagEvaluation(key, value) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addFeatureFlagEvaluationSpy, 1) expect(addFeatureFlagEvaluationSpy).toHaveBeenCalledOnceWith(key, value) }) - it('startDurationVital', () => { + it('startDurationVital', async () => { const addDurationVitalSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addDurationVital: addDurationVitalSpy, @@ -672,10 +681,11 @@ describe('preStartRum', () => { strategy.stopDurationVital('timing') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addDurationVitalSpy, 1) expect(addDurationVitalSpy).toHaveBeenCalled() }) - it('addDurationVital', () => { + it('addDurationVital', async () => { const addDurationVitalSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addDurationVital: addDurationVitalSpy, @@ -684,10 +694,11 @@ describe('preStartRum', () => { const vitalAdd = { name: 'timing', type: VitalType.DURATION, startClocks: clocksNow(), duration: 100 as Duration } strategy.addDurationVital(vitalAdd) strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addDurationVitalSpy, 1) expect(addDurationVitalSpy).toHaveBeenCalledOnceWith(vitalAdd) }) - it('addOperationStepVital', () => { + it('addOperationStepVital', async () => { const addOperationStepVitalSpy = jasmine.createSpy() doStartRumSpy.and.returnValue({ addOperationStepVital: addOperationStepVitalSpy, @@ -695,10 +706,11 @@ describe('preStartRum', () => { strategy.addOperationStepVital('foo', 'start') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(addOperationStepVitalSpy, 1) expect(addOperationStepVitalSpy).toHaveBeenCalledOnceWith('foo', 'start', undefined, undefined) }) - it('startAction / stopAction', () => { + it('startAction / stopAction', async () => { mockExperimentalFeatures([ExperimentalFeature.START_STOP_ACTION]) const startActionSpy = jasmine.createSpy() @@ -712,6 +724,7 @@ describe('preStartRum', () => { strategy.stopAction('user_login') strategy.init(DEFAULT_INIT_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(startActionSpy, 1) expect(startActionSpy).toHaveBeenCalledWith( 'user_login', @@ -773,7 +786,7 @@ describe('preStartRum', () => { expect(doStartRumSpy).not.toHaveBeenCalled() }) - it('starts rum if tracking consent is granted before init', () => { + it('starts rum if tracking consent is granted before init', async () => { trackingConsentState.update(TrackingConsent.GRANTED) strategy.init( { @@ -782,6 +795,7 @@ describe('preStartRum', () => { }, PUBLIC_API ) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalledTimes(1) }) From e0264d7ece87cd0c2addbdcb198a28ef2abec21d Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 30 Jan 2026 14:23:14 +0100 Subject: [PATCH 37/41] =?UTF-8?q?=E2=9C=85=20fix=20remaining=20flaky=20pre?= =?UTF-8?q?StartRum=20tests=20for=20async=20session=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests in 'trackViews mode' section expected synchronous or partially async doStartRum calls but session manager initialization is now fully async. Added proper async/await with collectAsyncCalls to wait for all async callbacks to complete. Fixes race conditions on slower browsers (Edge 80). --- packages/rum-core/src/boot/preStartRum.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index 087f468fc3..768aa9de87 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -299,8 +299,9 @@ describe('preStartRum', () => { }) describe('when auto', () => { - it('should start rum at init', () => { + it('should start rum at init', async () => { strategy.init(AUTO_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(doStartRumSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() }) @@ -315,6 +316,7 @@ describe('preStartRum', () => { clock.tick(20) strategy.init(AUTO_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(doStartRumSpy, 1) await collectAsyncCalls(startViewSpy, 1) expect(startViewSpy).toHaveBeenCalled() @@ -370,6 +372,7 @@ describe('preStartRum', () => { clock.tick(10) strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) await collectAsyncCalls(doStartRumSpy, 1) + await collectAsyncCalls(startViewSpy, 1) expect(doStartRumSpy).toHaveBeenCalled() const initialViewOptions: ViewOptions | undefined = doStartRumSpy.calls.argsFor(0)[3] @@ -404,6 +407,7 @@ describe('preStartRum', () => { clock.tick(10) strategy.init(MANUAL_CONFIGURATION, PUBLIC_API) + await collectAsyncCalls(doStartRumSpy, 1) await collectAsyncCalls(addTimingSpy, 2) expect(addTimingSpy).toHaveBeenCalledTimes(2) From f43dcdce9825e0724308161bbed9e95963534e3c Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Tue, 3 Feb 2026 08:03:51 +0100 Subject: [PATCH 38/41] =?UTF-8?q?=E2=9C=A8=20add=20resetSessionStoreOperat?= =?UTF-8?q?ions=20function=20to=20clear=20session=20store=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a new function to reset buffered and ongoing session store operations, ensuring a clean state when stopping the session manager. This change enhances session management unt test reliability. --- packages/core/src/domain/session/sessionManager.ts | 2 ++ packages/core/src/domain/session/sessionStoreOperations.ts | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 222d89f589..ca5b63f7b0 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -21,6 +21,7 @@ import { toSessionState } from './sessionState' import { retrieveSessionCookie } from './storeStrategies/sessionInCookie' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' import { retrieveSessionFromLocalStorage } from './storeStrategies/sessionInLocalStorage' +import { resetSessionStoreOperations } from './sessionStoreOperations' export interface SessionManager { findSession: ( @@ -148,6 +149,7 @@ export function startSessionManager( export function stopSessionManager() { stopCallbacks.forEach((e) => e()) stopCallbacks = [] + resetSessionStoreOperations() } function trackActivity(configuration: Configuration, expandOrRenewSession: () => void) { diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index d00fbbce34..2b1c1db6b2 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -19,9 +19,14 @@ export const LOCK_MAX_TRIES = 100 export const LOCK_EXPIRATION_DELAY = ONE_SECOND const LOCK_SEPARATOR = '--' -const bufferedOperations: Operations[] = [] +let bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined +export function resetSessionStoreOperations() { + bufferedOperations = [] + ongoingOperations = undefined +} + export function processSessionStoreOperations( operations: Operations, sessionStoreStrategy: SessionStoreStrategy, From 8d5792e69bc73c4175caa0f2b3dbd8a663cc7734 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Tue, 3 Feb 2026 11:40:42 +0100 Subject: [PATCH 39/41] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20trackingConse?= =?UTF-8?q?ntContext=20initialization=20to=20pre-start=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move startTrackingConsentContext from startRum to preStartRum to align with the pattern of initializing consent-dependent contexts earlier in the startup sequence. This removes the trackingConsentState parameter from startRum since it's no longer needed there. --- packages/rum-core/src/boot/preStartRum.ts | 2 ++ packages/rum-core/src/boot/rumPublicApi.spec.ts | 2 +- packages/rum-core/src/boot/rumPublicApi.ts | 1 - packages/rum-core/src/boot/startRum.spec.ts | 3 --- packages/rum-core/src/boot/startRum.ts | 8 -------- 5 files changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 660eeb1450..6d6fbb982e 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -47,6 +47,7 @@ import { startDurationVital, stopDurationVital } from '../domain/vital/vitalColl import type { RumSessionManager } from '../domain/rumSessionManager' import { startRumSessionManager, startRumSessionManagerStub } from '../domain/rumSessionManager' import { callPluginsMethod } from '../domain/plugins' +import { startTrackingConsentContext } from '../domain/contexts/trackingConsentContext' import type { StartRumResult } from './startRum' import type { RumPublicApiOptions, Strategy } from './rumPublicApi' @@ -179,6 +180,7 @@ export function createPreStartStrategy( trackingConsentState.tryToInit(configuration.trackingConsent) trackingConsentState.onGrantedOnce(() => { + startTrackingConsentContext(hooks, trackingConsentState) telemetry = startTelemetryImpl(TelemetryService.RUM, configuration, hooks) if (canUseEventBridge()) { diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index a715abd14f..c6e56a0062 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -1021,7 +1021,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) const calls = await collectAsyncCalls(startRumSpy, 1) - const sdkName = calls.argsFor(0)[11] + const sdkName = calls.argsFor(0)[10] expect(sdkName).toBe('rum-slim') }) }) diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 4f4d307fd0..80a67c75eb 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -583,7 +583,6 @@ export function makeRumPublicApi( profilerApi, initialViewOptions, createEncoder, - trackingConsentState, customVitalsState, bufferedDataObservable, telemetry, diff --git a/packages/rum-core/src/boot/startRum.spec.ts b/packages/rum-core/src/boot/startRum.spec.ts index bf074e8104..481f1d8186 100644 --- a/packages/rum-core/src/boot/startRum.spec.ts +++ b/packages/rum-core/src/boot/startRum.spec.ts @@ -7,8 +7,6 @@ import { noop, relativeNow, createIdentityEncoder, - createTrackingConsentState, - TrackingConsent, BufferedObservable, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' @@ -167,7 +165,6 @@ describe('view events', () => { noopProfilerApi, undefined, createIdentityEncoder, - createTrackingConsentState(TrackingConsent.GRANTED), createCustomVitalsState(), new BufferedObservable(100), createFakeTelemetryObject(), diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index 6ed6a1db73..de0a0c5dce 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -3,7 +3,6 @@ import type { RawError, DeflateEncoderStreamId, Encoder, - TrackingConsentState, BufferedData, BufferedObservable, Telemetry, @@ -47,7 +46,6 @@ import { startSessionContext } from '../domain/contexts/sessionContext' import { startConnectivityContext } from '../domain/contexts/connectivityContext' import type { SdkName } from '../domain/contexts/defaultContext' import { startDefaultContext } from '../domain/contexts/defaultContext' -import { startTrackingConsentContext } from '../domain/contexts/trackingConsentContext' import type { Hooks } from '../domain/hooks' import { startEventCollection } from '../domain/event/eventCollection' import { startInitialViewMetricsTelemetry } from '../domain/view/viewMetrics/startInitialViewMetricsTelemetry' @@ -65,11 +63,6 @@ export function startRum( profilerApi: ProfilerApi, initialViewOptions: ViewOptions | undefined, createEncoder: (streamId: DeflateEncoderStreamId) => Encoder, - - // `startRum` and its subcomponents assume tracking consent is granted initially and starts - // collecting logs unconditionally. As such, `startRum` should be called with a - // `trackingConsentState` set to "granted". - trackingConsentState: TrackingConsentState, customVitalsState: CustomVitalsState, bufferedDataObservable: BufferedObservable, telemetry: Telemetry, @@ -111,7 +104,6 @@ export function startRum( startRumEventBridge(lifeCycle) } - startTrackingConsentContext(hooks, trackingConsentState) const { stop: stopInitialViewMetricsTelemetry } = startInitialViewMetricsTelemetry(lifeCycle, telemetry) cleanupTasks.push(stopInitialViewMetricsTelemetry) From 1183c6f61a1de714e1add397ad6d63085b09ffa2 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Tue, 3 Feb 2026 11:57:41 +0100 Subject: [PATCH 40/41] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20trackingConse?= =?UTF-8?q?ntContext=20initialization=20to=20pre-start=20step=20for=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move startTrackingConsentContext from startLogs to preStartLogs to align with the pattern of initializing consent-dependent contexts earlier in the startup sequence. This removes the trackingConsentState parameter from startLogs since it's no longer needed there. --- packages/logs/src/boot/logsPublicApi.ts | 1 - packages/logs/src/boot/preStartLogs.ts | 2 ++ packages/logs/src/boot/startLogs.spec.ts | 34 +------------------ packages/logs/src/boot/startLogs.ts | 9 +---- packages/rum-core/src/boot/startRum.ts | 1 - test/e2e/scenario/trackingConsent.scenario.ts | 14 ++++++++ 6 files changed, 18 insertions(+), 43 deletions(-) diff --git a/packages/logs/src/boot/logsPublicApi.ts b/packages/logs/src/boot/logsPublicApi.ts index b6107580b9..80585ef88a 100644 --- a/packages/logs/src/boot/logsPublicApi.ts +++ b/packages/logs/src/boot/logsPublicApi.ts @@ -285,7 +285,6 @@ export function makeLogsPublicApi(startLogsImpl: StartLogs, startTelemetryImpl?: configuration, logsSessionManager, buildCommonContext, - trackingConsentState, bufferedDataObservable, hooks ) diff --git a/packages/logs/src/boot/preStartLogs.ts b/packages/logs/src/boot/preStartLogs.ts index 52d217ec5c..a1c17dfa55 100644 --- a/packages/logs/src/boot/preStartLogs.ts +++ b/packages/logs/src/boot/preStartLogs.ts @@ -25,6 +25,7 @@ import { serializeLogsConfiguration, validateAndBuildLogsConfiguration } from '. import type { CommonContext } from '../rawLogsEvent.types' import type { LogsSessionManager } from '../domain/logsSessionManager' import { startLogsSessionManagerStub, startLogsSessionManager } from '../domain/logsSessionManager' +import { startTrackingConsentContext } from '../domain/contexts/trackingConsentContext' import type { Strategy } from './logsPublicApi' import type { StartLogsResult } from './startLogs' @@ -108,6 +109,7 @@ export function createPreStartStrategy( trackingConsentState.tryToInit(configuration.trackingConsent) trackingConsentState.onGrantedOnce(() => { + startTrackingConsentContext(hooks, trackingConsentState) startTelemetryImpl(TelemetryService.LOGS, configuration, hooks) if (configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum()) { startLogsSessionManager(configuration, trackingConsentState, (newSessionManager) => { diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 414127d2d0..fe95861e76 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -2,8 +2,6 @@ import type { BufferedData, Payload } from '@datadog/browser-core' import { ErrorSource, display, - createTrackingConsentState, - TrackingConsent, STORAGE_POLL_DELAY, BufferedObservable, FLUSH_DURATION_LIMIT, @@ -47,10 +45,7 @@ const COMMON_CONTEXT = { } const DEFAULT_PAYLOAD = {} as Payload -function startLogsWithDefaults( - { configuration }: { configuration?: Partial } = {}, - trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) -) { +function startLogsWithDefaults({ configuration }: { configuration?: Partial } = {}) { const endpointBuilder = mockEndpointBuilder('https://localhost/v1/input/log') const sessionManager = createLogsSessionManagerMock() const { handleLog, stop, globalContext, accountContext, userContext } = startLogs( @@ -61,7 +56,6 @@ function startLogsWithDefaults( }, sessionManager, () => COMMON_CONTEXT, - trackingConsentState, new BufferedObservable(100), createHooks() ) @@ -253,30 +247,4 @@ describe('logs', () => { expect(firstRequest.view.url).toEqual('from-rum-context') }) }) - - describe('tracking consent', () => { - it('should not send logs after tracking consent is revoked', async () => { - const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) - const { handleLog, logger } = startLogsWithDefaults({}, trackingConsentState) - - // Log a message with consent granted - should be sent - handleLog({ status: StatusType.info, message: 'message before revocation' }, logger) - - clock.tick(FLUSH_DURATION_LIMIT) - await interceptor.waitForAllFetchCalls() - expect(requests.length).toEqual(1) - expect(getLoggedMessage(requests, 0).message).toBe('message before revocation') - - // Revoke consent - trackingConsentState.update(TrackingConsent.NOT_GRANTED) - - // Log another message - should not be sent - handleLog({ status: StatusType.info, message: 'message after revocation' }, logger) - - clock.tick(FLUSH_DURATION_LIMIT) - await interceptor.waitForAllFetchCalls() - // Should still only have the first request - expect(requests.length).toEqual(1) - }) - }) }) diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 9481e8e2d6..6a31252d6a 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -1,4 +1,4 @@ -import type { TrackingConsentState, BufferedObservable, BufferedData } from '@datadog/browser-core' +import type { BufferedObservable, BufferedData } from '@datadog/browser-core' import { sendToExtension, createPageMayExitObservable, @@ -24,7 +24,6 @@ import type { CommonContext } from '../rawLogsEvent.types' import type { Hooks } from '../domain/hooks' import { startRUMInternalContext } from '../domain/contexts/rumInternalContext' import { startSessionContext } from '../domain/contexts/sessionContext' -import { startTrackingConsentContext } from '../domain/contexts/trackingConsentContext' const LOGS_STORAGE_KEY = 'logs' @@ -35,11 +34,6 @@ export function startLogs( configuration: LogsConfiguration, sessionManager: LogsSessionManager, getCommonContext: () => CommonContext, - - // `startLogs` and its subcomponents assume tracking consent is granted initially and starts - // collecting logs unconditionally. As such, `startLogs` should be called with a - // `trackingConsentState` set to "granted". - trackingConsentState: TrackingConsentState, bufferedDataObservable: BufferedObservable, hooks: Hooks ) { @@ -51,7 +45,6 @@ export function startLogs( const reportError = startReportError(lifeCycle) const pageMayExitObservable = createPageMayExitObservable(configuration) - startTrackingConsentContext(hooks, trackingConsentState) // Start user and account context first to allow overrides from global context startSessionContext(hooks, configuration, sessionManager) const accountContext = startAccountContext(hooks, configuration, LOGS_STORAGE_KEY) diff --git a/packages/rum-core/src/boot/startRum.ts b/packages/rum-core/src/boot/startRum.ts index de0a0c5dce..1f0b2fbbd2 100644 --- a/packages/rum-core/src/boot/startRum.ts +++ b/packages/rum-core/src/boot/startRum.ts @@ -104,7 +104,6 @@ export function startRum( startRumEventBridge(lifeCycle) } - const { stop: stopInitialViewMetricsTelemetry } = startInitialViewMetricsTelemetry(lifeCycle, telemetry) cleanupTasks.push(stopInitialViewMetricsTelemetry) diff --git a/test/e2e/scenario/trackingConsent.scenario.ts b/test/e2e/scenario/trackingConsent.scenario.ts index 8254b52f1c..265bd9ba49 100644 --- a/test/e2e/scenario/trackingConsent.scenario.ts +++ b/test/e2e/scenario/trackingConsent.scenario.ts @@ -97,5 +97,19 @@ test.describe('tracking consent', () => { expect(intakeRegistry.isEmpty).toBe(false) expect(await findSessionCookie(browserContext)).toBeDefined() }) + + createTest('stops sending events if tracking consent is revoked @only') + .withLogs() + .run(async ({ intakeRegistry, flushEvents, browserContext, page }) => { + await page.evaluate(() => { + window.DD_LOGS!.setTrackingConsent('not-granted') + window.DD_LOGS!.logger.log('should not be sent') + }) + + await flushEvents() + + expect(intakeRegistry.logsEvents).toHaveLength(0) + expect((await findSessionCookie(browserContext))?.isExpired).toEqual('1') + }) }) }) From 4bb4c10692415bcf3ea99165a7ff315026c8e04a Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 6 Feb 2026 09:16:38 +0100 Subject: [PATCH 41/41] fix formating --- packages/rum-core/src/boot/rumPublicApi.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index eea8aeb19a..29126a7fe1 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -618,7 +618,7 @@ describe('rum public api', () => { rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) rumPublicApi.startView('foo') const calls = await collectAsyncCalls(startViewSpy, 1) - expect(calls.argsFor(0)[0]).toEqual({ name: 'foo', handlingStack: jasmine.any(String) }) + expect(calls.argsFor(0)[0]).toEqual({ name: 'foo', handlingStack: jasmine.any(String) }) }) it('should call RUM results startView with the view options', async () => {