diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 121304cae0..39880f463f 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -1,4 +1,4 @@ -import { createNewEvent, mockClock, restorePageVisibility, setPageVisibility } from '../../../test' +import { createNewEvent, expireCookie, mockClock, restorePageVisibility, setPageVisibility } from '../../../test' import type { Clock } from '../../../test' import { getCookie, setCookie } from '../../browser/cookie' import type { RelativeTime } from '../../tools/utils/timeUtils' @@ -38,7 +38,7 @@ describe('startSessionManager', () => { let clock: Clock function expireSessionCookie() { - setCookie(SESSION_STORE_KEY, 'isExpired=1', DURATION) + expireCookie() clock.tick(STORAGE_POLL_DELAY) } @@ -48,25 +48,25 @@ describe('startSessionManager', () => { } function expectSessionIdToBe(sessionManager: SessionManager, sessionId: string) { - expect(sessionManager.findActiveSession()!.id).toBe(sessionId) + expect(sessionManager.findSession()!.id).toBe(sessionId) expect(getCookie(SESSION_STORE_KEY)).toContain(`id=${sessionId}`) } function expectSessionIdToBeDefined(sessionManager: SessionManager) { - expect(sessionManager.findActiveSession()!.id).toMatch(/^[a-f0-9-]+$/) - expect(sessionManager.findActiveSession()?.isExpired).toBeUndefined() + expect(sessionManager.findSession()!.id).toMatch(/^[a-f0-9-]+$/) + expect(sessionManager.findSession()?.isExpired).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) expect(getCookie(SESSION_STORE_KEY)).not.toContain('isExpired=1') } function expectSessionToBeExpired(sessionManager: SessionManager) { - expect(sessionManager.findActiveSession()).toBeUndefined() + expect(sessionManager.findSession()).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).toContain('isExpired=1') } function expectSessionIdToNotBeDefined(sessionManager: SessionManager) { - expect(sessionManager.findActiveSession()!.id).toBeUndefined() + expect(sessionManager.findSession()!.id).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') } @@ -75,12 +75,12 @@ describe('startSessionManager', () => { productKey: string, trackingType: FakeTrackingType ) { - expect(sessionManager.findActiveSession()!.trackingType).toEqual(trackingType) + expect(sessionManager.findSession()!.trackingType).toEqual(trackingType) expect(getCookie(SESSION_STORE_KEY)).toContain(`${productKey}=${trackingType}`) } function expectTrackingTypeToNotBeDefined(sessionManager: SessionManager, productKey: string) { - expect(sessionManager.findActiveSession()?.trackingType).toBeUndefined() + expect(sessionManager.findSession()?.trackingType).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).not.toContain(`${productKey}=`) } @@ -115,7 +115,7 @@ describe('startSessionManager', () => { deleteSessionCookie() - expect(sessionManager.findActiveSession()).toBeUndefined() + expect(sessionManager.findSession()).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() window.dispatchEvent(createNewEvent(DOM_EVENT.RESUME)) @@ -231,13 +231,13 @@ describe('startSessionManager', () => { expect(renewSessionSpy).not.toHaveBeenCalled() - expect(sessionManager.findActiveSession()).toBeUndefined() + expect(sessionManager.findSession()).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) expect(renewSessionSpy).not.toHaveBeenCalled() - expect(sessionManager.findActiveSession()).toBeUndefined() + expect(sessionManager.findSession()).toBeUndefined() expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) }) @@ -245,10 +245,10 @@ describe('startSessionManager', () => { describe('multiple startSessionManager calls', () => { it('should re-use the same session id', () => { const firstSessionManager = startSessionManagerWithDefaults({ productKey: FIRST_PRODUCT_KEY }) - const idA = firstSessionManager.findActiveSession()!.id + const idA = firstSessionManager.findSession()!.id const secondSessionManager = startSessionManagerWithDefaults({ productKey: SECOND_PRODUCT_KEY }) - const idB = secondSessionManager.findActiveSession()!.id + const idB = secondSessionManager.findSession()!.id expect(idA).toBe(idB) }) @@ -287,8 +287,8 @@ describe('startSessionManager', () => { computeSessionState: () => NOT_TRACKED_SESSION_STATE, }) - expect(firstSessionManager.findActiveSession()!.trackingType).toEqual(FakeTrackingType.TRACKED) - expect(secondSessionManager.findActiveSession()!.trackingType).toEqual(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', () => { @@ -324,7 +324,7 @@ describe('startSessionManager', () => { const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) - expect(sessionManager.findActiveSession()).toBeDefined() + expect(sessionManager.findSession()).toBeDefined() expect(getCookie(SESSION_STORE_KEY)).toBeDefined() clock.tick(SESSION_TIME_OUT_DELAY) @@ -339,7 +339,7 @@ describe('startSessionManager', () => { const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) - expect(sessionManager.findActiveSession()!.id).not.toBe('abcde') + expect(sessionManager.findSession()!.id).not.toBe('abcde') expect(getCookie(SESSION_STORE_KEY)).toContain(`created=${Date.now()}`) expect(expireSessionSpy).not.toHaveBeenCalled() // the session has not been active from the start }) @@ -349,7 +349,7 @@ describe('startSessionManager', () => { const sessionManager = startSessionManagerWithDefaults() - expect(sessionManager.findActiveSession()!.id).toBe('abcde') + expect(sessionManager.findSession()!.id).toBe('abcde') expect(getCookie(SESSION_STORE_KEY)).not.toContain('created=') }) }) @@ -509,14 +509,14 @@ describe('startSessionManager', () => { const sessionManager = startSessionManagerWithDefaults() expireSessionCookie() - expect(sessionManager.findActiveSession()).toBeUndefined() + expect(sessionManager.findSession()).toBeUndefined() }) it('should return the current session context when there is no start time', () => { const sessionManager = startSessionManagerWithDefaults() - expect(sessionManager.findActiveSession()!.id).toBeDefined() - expect(sessionManager.findActiveSession()!.trackingType).toBeDefined() + expect(sessionManager.findSession()!.id).toBeDefined() + expect(sessionManager.findSession()!.trackingType).toBeDefined() }) it('should return the session context corresponding to startTime', () => { @@ -524,8 +524,8 @@ describe('startSessionManager', () => { // 0s to 10s: first session clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY) - const firstSessionId = sessionManager.findActiveSession()!.id - const firstSessionTrackingType = sessionManager.findActiveSession()!.trackingType + const firstSessionId = sessionManager.findSession()!.id + const firstSessionTrackingType = sessionManager.findSession()!.trackingType expireSessionCookie() // 10s to 20s: no session @@ -534,24 +534,40 @@ describe('startSessionManager', () => { // 20s to end: second session document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) clock.tick(10 * ONE_SECOND) - const secondSessionId = sessionManager.findActiveSession()!.id - const secondSessionTrackingType = sessionManager.findActiveSession()!.trackingType - - expect(sessionManager.findActiveSession((5 * ONE_SECOND) as RelativeTime)!.id).toBe(firstSessionId) - expect(sessionManager.findActiveSession((5 * ONE_SECOND) as RelativeTime)!.trackingType).toBe( - firstSessionTrackingType - ) - expect(sessionManager.findActiveSession((15 * ONE_SECOND) as RelativeTime)).toBeUndefined() - expect(sessionManager.findActiveSession((25 * ONE_SECOND) as RelativeTime)!.id).toBe(secondSessionId) - expect(sessionManager.findActiveSession((25 * ONE_SECOND) as RelativeTime)!.trackingType).toBe( + const secondSessionId = sessionManager.findSession()!.id + const secondSessionTrackingType = sessionManager.findSession()!.trackingType + + expect(sessionManager.findSession((5 * ONE_SECOND) as RelativeTime)!.id).toBe(firstSessionId) + expect(sessionManager.findSession((5 * ONE_SECOND) as RelativeTime)!.trackingType).toBe(firstSessionTrackingType) + expect(sessionManager.findSession((15 * ONE_SECOND) as RelativeTime)).toBeUndefined() + expect(sessionManager.findSession((25 * ONE_SECOND) as RelativeTime)!.id).toBe(secondSessionId) + expect(sessionManager.findSession((25 * ONE_SECOND) as RelativeTime)!.trackingType).toBe( secondSessionTrackingType ) }) + describe('option `returnInactive` is true', () => { + it('should return the session context even when the session is expired', () => { + const sessionManager = startSessionManagerWithDefaults() + + // 0s to 10s: first session + clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY) + + expireSessionCookie() + + // 10s to 20s: no session + clock.tick(10 * ONE_SECOND) + + expect(sessionManager.findSession((15 * ONE_SECOND) as RelativeTime, { returnInactive: true })).toBeDefined() + + expect(sessionManager.findSession((15 * ONE_SECOND) as RelativeTime, { returnInactive: false })).toBeUndefined() + }) + }) + it('should return the current session context in the renewObservable callback', () => { const sessionManager = startSessionManagerWithDefaults() let currentSession - sessionManager.renewObservable.subscribe(() => (currentSession = sessionManager.findActiveSession())) + sessionManager.renewObservable.subscribe(() => (currentSession = sessionManager.findSession())) // new session expireSessionCookie() @@ -564,7 +580,7 @@ describe('startSessionManager', () => { it('should return the current session context in the expireObservable callback', () => { const sessionManager = startSessionManagerWithDefaults() let currentSession - sessionManager.expireObservable.subscribe(() => (currentSession = sessionManager.findActiveSession())) + sessionManager.expireObservable.subscribe(() => (currentSession = sessionManager.findSession())) // new session expireSessionCookie() @@ -599,7 +615,7 @@ describe('startSessionManager', () => { it('renews the session when tracking consent is granted', () => { const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED) const sessionManager = startSessionManagerWithDefaults({ trackingConsentState }) - const initialSessionId = sessionManager.findActiveSession()!.id + const initialSessionId = sessionManager.findSession()!.id trackingConsentState.update(TrackingConsent.NOT_GRANTED) @@ -610,7 +626,7 @@ describe('startSessionManager', () => { clock.tick(STORAGE_POLL_DELAY) expectSessionIdToBeDefined(sessionManager) - expect(sessionManager.findActiveSession()!.id).not.toBe(initialSessionId) + expect(sessionManager.findSession()!.id).not.toBe(initialSessionId) }) }) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 97e9f77403..3bdbfe3d0d 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -11,7 +11,10 @@ import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { startSessionStore } from './sessionStore' export interface SessionManager { - findActiveSession: (startTime?: RelativeTime) => SessionContext | undefined + findSession: ( + startTime?: RelativeTime, + options?: { returnInactive: boolean } + ) => SessionContext | undefined renewObservable: Observable expireObservable: Observable expire: () => void @@ -80,7 +83,7 @@ export function startSessionManager( } return { - findActiveSession: (startTime) => sessionContextHistory.find(startTime), + findSession: (startTime, options) => sessionContextHistory.find(startTime, options), renewObservable, expireObservable, expire: sessionStore.expire, diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index a202f65feb..a245dac603 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,5 +1,5 @@ import type { Clock } from '../../../test' -import { mockClock } from '../../../test' +import { expireCookie, mockClock } from '../../../test' import { getCookie, setCookie } from '../../browser/cookie' import type { SessionStore } from './sessionStore' import { STORAGE_POLL_DELAY, startSessionStore, selectSessionStoreStrategyType } from './sessionStore' @@ -52,7 +52,7 @@ function getStoreExpiration() { } function resetSessionInStore() { - setCookie(SESSION_STORE_KEY, 'isExpired=1', DURATION) + expireCookie() } describe('session store', () => { @@ -172,13 +172,13 @@ describe('session store', () => { expect(sessionStoreManager.getSession().id).toBeDefined() expectTrackedSessionToBeInStore() expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) it( 'when session not in cache, session not in store and new session not tracked, ' + - 'should store not tracked session', + 'should store not tracked session and trigger renew session', () => { setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) @@ -187,7 +187,7 @@ describe('session store', () => { expect(sessionStoreManager.getSession().id).toBeUndefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) @@ -200,7 +200,7 @@ describe('session store', () => { expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) expectTrackedSessionToBeInStore(FIRST_ID) expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) }) it( @@ -218,13 +218,13 @@ describe('session store', () => { expect(sessionId).not.toBe(FIRST_ID) expectTrackedSessionToBeInStore(sessionId) expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) it( 'when session in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', + 'should expire session, store not tracked session and trigger renew session', () => { setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) @@ -236,13 +236,13 @@ describe('session store', () => { expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) it( 'when session not tracked in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', + 'should expire session, store not tracked session and trigger renew session', () => { setSessionInStore(FakeTrackingType.NOT_TRACKED) setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) @@ -254,7 +254,7 @@ describe('session store', () => { expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) @@ -285,13 +285,13 @@ describe('session store', () => { expect(sessionStoreManager.getSession().id).toBe(SECOND_ID) expectTrackedSessionToBeInStore(SECOND_ID) expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) it( 'when session in cache is different session than in store and store session is not tracked, ' + - 'should expire session and store not tracked session', + 'should expire session, store not tracked session and trigger renew', () => { setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) setupSessionStore((rawTrackingType) => ({ @@ -305,7 +305,7 @@ describe('session store', () => { expect(sessionStoreManager.getSession().id).toBeUndefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalledTimes(1) } ) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index f8e1a7efa9..6bcf730c68 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -6,7 +6,12 @@ import { generateUUID } from '../../tools/utils/stringUtils' import type { InitConfiguration } from '../configuration' import { selectCookieStrategy, initCookieStrategy } from './storeStrategies/sessionInCookie' import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' -import { getExpiredSessionState, isSessionInExpiredState, isSessionInNotStartedState } from './sessionState' +import { + getExpiredSessionState, + isSessionInExpiredState, + isSessionInNotStartedState, + isSessionStarted, +} from './sessionState' import type { SessionState } from './sessionState' import { initLocalStorageStrategy, selectLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' import { processSessionStoreOperations } from './sessionStoreOperations' @@ -69,7 +74,6 @@ export function startSessionStore( startSession() const { throttled: throttledExpandOrRenewSession, cancel: cancelExpandOrRenewSession } = throttle(() => { - let isTracked: boolean processSessionStoreOperations( { process: (sessionState) => { @@ -78,11 +82,11 @@ export function startSessionStore( } const synchronizedSession = synchronizeSession(sessionState) - isTracked = expandOrRenewSessionState(synchronizedSession) + expandOrRenewSessionState(synchronizedSession) return synchronizedSession }, after: (sessionState) => { - if (isTracked && !hasSessionInCache()) { + if (isSessionStarted(sessionState) && !hasSessionInCache()) { renewSessionInCache(sessionState) } sessionCache = sessionState @@ -158,7 +162,6 @@ export function startSessionStore( sessionState.id = generateUUID() sessionState.created = String(dateNow()) } - return isTracked } function hasSessionInCache() { diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 96b6045c24..f3d20cf44b 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -353,6 +353,10 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & { * The version of the tracer API used by the SDK. Eg. '0.1.0' */ tracer_api_version?: string + /** + * Whether logs are sent after the session expiration + */ + send_logs_after_session_expiration?: boolean [k: string]: unknown } [k: string]: unknown diff --git a/packages/core/src/tools/valueHistory.spec.ts b/packages/core/src/tools/valueHistory.spec.ts index 02a8f9f81d..9f368b7eab 100644 --- a/packages/core/src/tools/valueHistory.spec.ts +++ b/packages/core/src/tools/valueHistory.spec.ts @@ -54,6 +54,14 @@ describe('valueHistory', () => { expect(valueHistory.find(15 as RelativeTime)).toBeUndefined() }) + + describe('with `option.returnInactive` true', () => { + it('should return the value of the closest entry regardless of the being closed', () => { + valueHistory.add('foo', 0 as RelativeTime).close(10 as RelativeTime) + valueHistory.add('bar', 20 as RelativeTime) + expect(valueHistory.find(15 as RelativeTime, { returnInactive: true })).toEqual('foo') + }) + }) }) describe('findAll', () => { diff --git a/packages/core/src/tools/valueHistory.ts b/packages/core/src/tools/valueHistory.ts index 709058c4a9..1f14deaab6 100644 --- a/packages/core/src/tools/valueHistory.ts +++ b/packages/core/src/tools/valueHistory.ts @@ -60,11 +60,16 @@ export class ValueHistory { /** * Return the latest value that was active during `startTime`, or the currently active value * if no `startTime` is provided. This method assumes that entries are not overlapping. + * + * If `option.returnInactive` is true, returns the value at `startTime` (active or not). */ - find(startTime: RelativeTime = END_OF_TIMES): Value | undefined { + find( + startTime: RelativeTime = END_OF_TIMES, + options: { returnInactive: boolean } = { returnInactive: false } + ): Value | undefined { for (const entry of this.entries) { if (entry.startTime <= startTime) { - if (startTime <= entry.endTime) { + if (options.returnInactive || startTime <= entry.endTime) { return entry.value } break diff --git a/packages/core/test/cookie.ts b/packages/core/test/cookie.ts new file mode 100644 index 0000000000..a791cc3e24 --- /dev/null +++ b/packages/core/test/cookie.ts @@ -0,0 +1,7 @@ +import { setCookie } from '../src/browser/cookie' +import { SESSION_STORE_KEY } from '../src/domain/session/storeStrategies/sessionStoreStrategy' +import { ONE_MINUTE } from '../src/tools/utils/timeUtils' + +export function expireCookie() { + setCookie(SESSION_STORE_KEY, 'isExpired=1', ONE_MINUTE) +} diff --git a/packages/core/test/index.ts b/packages/core/test/index.ts index c1ad3e201b..9ef178cec5 100644 --- a/packages/core/test/index.ts +++ b/packages/core/test/index.ts @@ -1,6 +1,7 @@ export * from './browserChecks' export * from './buildEnv' export * from './collectAsyncCalls' +export * from './cookie' export * from './registerCleanupTask' export * from './requests' export * from './emulate/createNewEvent' diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 8d6d9dbb0f..73dac10083 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -9,8 +9,11 @@ import { noop, createTrackingConsentState, TrackingConsent, + setCookie, + STORAGE_POLL_DELAY, + ONE_MINUTE, } from '@datadog/browser-core' -import type { Request } from '@datadog/browser-core/test' +import type { Clock, Request } from '@datadog/browser-core/test' import { interceptRequests, stubEndpointBuilder, @@ -18,6 +21,8 @@ import { cleanupSyntheticsWorkerValues, mockSyntheticsWorkerValues, registerCleanupTask, + mockClock, + expireCookie, } from '@datadog/browser-core/test' import type { LogsConfiguration } from '../domain/configuration' @@ -236,4 +241,66 @@ describe('logs', () => { expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) }) + + describe('session lifecycle', () => { + let clock: Clock + beforeEach(() => { + clock = mockClock() + }) + afterEach(() => { + clock.cleanup() + }) + + it('sends logs without session id when the session expires ', () => { + setCookie(SESSION_STORE_KEY, 'id=foo&logs=1', ONE_MINUTE) + ;({ handleLog, stop: stopLogs } = startLogs( + initConfiguration, + { ...baseConfiguration, sendLogsAfterSessionExpiration: true }, + () => COMMON_CONTEXT, + createTrackingConsentState(TrackingConsent.GRANTED) + )) + registerCleanupTask(stopLogs) + + handleLog({ status: StatusType.info, message: 'message 1' }, logger) + + expireCookie() + clock.tick(STORAGE_POLL_DELAY * 2) + + handleLog({ status: StatusType.info, message: 'message 2' }, logger) + + const firstRequest = getLoggedMessage(requests, 0) + const secondRequest = getLoggedMessage(requests, 1) + + expect(requests.length).toEqual(2) + expect(firstRequest.message).toEqual('message 1') + expect(firstRequest.session_id).toEqual('foo') + + expect(secondRequest.message).toEqual('message 2') + expect(secondRequest.session_id).toBeUndefined() + }) + + it('does not send logs with session id when session is expired and sendLogsAfterSessionExpiration is false', () => { + setCookie(SESSION_STORE_KEY, 'id=foo&logs=1', ONE_MINUTE) + ;({ handleLog, stop: stopLogs } = startLogs( + initConfiguration, + baseConfiguration, + () => COMMON_CONTEXT, + createTrackingConsentState(TrackingConsent.GRANTED) + )) + registerCleanupTask(stopLogs) + + handleLog({ status: StatusType.info, message: 'message 1' }, logger) + + expireCookie() + clock.tick(STORAGE_POLL_DELAY * 2) + + handleLog({ status: StatusType.info, message: 'message 2' }, logger) + + const firstRequest = getLoggedMessage(requests, 0) + + expect(requests.length).toEqual(1) + expect(firstRequest.message).toEqual('message 1') + expect(firstRequest.session_id).toEqual('foo') + }) + }) }) diff --git a/packages/logs/src/domain/assembly.spec.ts b/packages/logs/src/domain/assembly.spec.ts index 970dd3ebc9..ad320f731d 100644 --- a/packages/logs/src/domain/assembly.spec.ts +++ b/packages/logs/src/domain/assembly.spec.ts @@ -12,6 +12,7 @@ import { mockClock } from '@datadog/browser-core/test' import type { LogsEvent } from '../logsEvent.types' import type { CommonContext } from '../rawLogsEvent.types' import { startLogsAssembly } from './assembly' +import type { LogsConfiguration } from './configuration' import { validateAndBuildLogsConfiguration } from './configuration' import { Logger, StatusType } from './logger' import type { LogsSessionManager } from './logsSessionManager' @@ -41,22 +42,28 @@ const COMMON_CONTEXT_WITH_USER: CommonContext = { describe('startLogsAssembly', () => { const sessionManager: LogsSessionManager = { - findTrackedSession: () => (sessionIsTracked ? { id: SESSION_ID } : undefined), + findTrackedSession: (_startTime, options) => + (sessionIsActive && sessionIsTracked) || options?.returnInactive + ? { id: sessionIsTracked ? SESSION_ID : undefined } + : undefined, expireObservable: new Observable(), } let beforeSend: (event: LogsEvent) => void | boolean + let sessionIsActive: boolean let sessionIsTracked: boolean let lifeCycle: LifeCycle + let configuration: LogsConfiguration let serverLogs: Array = [] let mainLogger: Logger beforeEach(() => { sessionIsTracked = true + sessionIsActive = true lifeCycle = new LifeCycle() lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (serverRumEvent) => serverLogs.push(serverRumEvent)) - const configuration = { - ...validateAndBuildLogsConfiguration(initConfiguration)!, + configuration = { + ...validateAndBuildLogsConfiguration({ ...initConfiguration })!, beforeSend: (x: LogsEvent) => beforeSend(x), } beforeSend = noop @@ -96,31 +103,62 @@ describe('startLogsAssembly', () => { expect(serverLogs.length).toEqual(0) }) - it('should not send if session is not tracked', () => { - sessionIsTracked = false - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { - rawLogsEvent: DEFAULT_MESSAGE, + describe('event generation condition', () => { + it('should not send if session is not tracked', () => { + sessionIsTracked = false + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(0) }) - expect(serverLogs.length).toEqual(0) - }) - it('should enable/disable the sending when the tracking type change', () => { - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { - rawLogsEvent: DEFAULT_MESSAGE, + it('should send log with session id if session is active', () => { + sessionIsTracked = true + sessionIsActive = true + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(1) + expect(serverLogs[0].session_id).toEqual(SESSION_ID) }) - expect(serverLogs.length).toEqual(1) - sessionIsTracked = false - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { - rawLogsEvent: DEFAULT_MESSAGE, + it('should send log without session id if session has expired', () => { + startLogsAssembly( + sessionManager, + { ...configuration, sendLogsAfterSessionExpiration: true }, + lifeCycle, + () => COMMON_CONTEXT, + noop + ) + + sessionIsTracked = true + sessionIsActive = false + + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(1) + expect(serverLogs[0].session_id).toBeUndefined() }) - expect(serverLogs.length).toEqual(1) - sessionIsTracked = true - lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { - rawLogsEvent: DEFAULT_MESSAGE, + it('should enable/disable the sending when the tracking type change', () => { + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(1) + + sessionIsTracked = false + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(1) + + sessionIsTracked = true + lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, { + rawLogsEvent: DEFAULT_MESSAGE, + }) + expect(serverLogs.length).toEqual(2) }) - expect(serverLogs.length).toEqual(2) }) describe('contexts inclusion', () => { diff --git a/packages/logs/src/domain/assembly.ts b/packages/logs/src/domain/assembly.ts index b90aad67e0..cf352491a0 100644 --- a/packages/logs/src/domain/assembly.ts +++ b/packages/logs/src/domain/assembly.ts @@ -27,7 +27,11 @@ export function startLogsAssembly( const startTime = getRelativeTime(rawLogsEvent.date) const session = sessionManager.findTrackedSession(startTime) - if (!session) { + if ( + !session && + (!configuration.sendLogsAfterSessionExpiration || + !sessionManager.findTrackedSession(startTime, { returnInactive: true })) + ) { return } @@ -35,7 +39,7 @@ export function startLogsAssembly( const log = combine( { service: configuration.service, - session_id: session.id, + session_id: session?.id, // Insert user first to allow overrides from global context usr: !isEmptyObject(commonContext.user) ? commonContext.user : undefined, view: commonContext.view, diff --git a/packages/logs/src/domain/configuration.spec.ts b/packages/logs/src/domain/configuration.spec.ts index f9d52412af..211cfbfc66 100644 --- a/packages/logs/src/domain/configuration.spec.ts +++ b/packages/logs/src/domain/configuration.spec.ts @@ -138,6 +138,7 @@ describe('serializeLogsConfiguration', () => { forwardConsoleLogs: 'all', forwardReports: 'all', usePciIntake: false, + sendLogsAfterSessionExpiration: false, } type MapLogsInitConfigurationKey = Key extends keyof InitConfiguration @@ -156,6 +157,7 @@ describe('serializeLogsConfiguration', () => { forward_console_logs: 'all', forward_reports: 'all', use_pci_intake: false, + send_logs_after_session_expiration: false, }) }) }) diff --git a/packages/logs/src/domain/configuration.ts b/packages/logs/src/domain/configuration.ts index 07271b94ce..91084fa7fa 100644 --- a/packages/logs/src/domain/configuration.ts +++ b/packages/logs/src/domain/configuration.ts @@ -19,6 +19,8 @@ export interface LogsInitConfiguration extends InitConfiguration { forwardErrorsToLogs?: boolean | undefined forwardConsoleLogs?: ConsoleApiName[] | 'all' | undefined forwardReports?: RawReportType[] | 'all' | undefined + // TODO next major: remove this option and make it the default behaviour + sendLogsAfterSessionExpiration?: boolean | undefined usePciIntake?: boolean } @@ -29,6 +31,7 @@ export interface LogsConfiguration extends Configuration { forwardConsoleLogs: ConsoleApiName[] forwardReports: RawReportType[] requestErrorResponseLengthLimit: number + sendLogsAfterSessionExpiration: boolean } /** @@ -73,6 +76,7 @@ export function validateAndBuildLogsConfiguration( forwardConsoleLogs, forwardReports, requestErrorResponseLengthLimit: DEFAULT_REQUEST_ERROR_RESPONSE_LENGTH_LIMIT, + sendLogsAfterSessionExpiration: !!initConfiguration.sendLogsAfterSessionExpiration, }, baseConfiguration ) @@ -104,6 +108,7 @@ export function serializeLogsConfiguration(configuration: LogsInitConfiguration) forward_console_logs: configuration.forwardConsoleLogs, forward_reports: configuration.forwardReports, use_pci_intake: configuration.usePciIntake, + send_logs_after_session_expiration: configuration.sendLogsAfterSessionExpiration, }, baseSerializedInitConfiguration ) satisfies RawTelemetryConfiguration diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 53c0170cca..35360e5ca7 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -7,11 +7,12 @@ import { stopSessionManager, ONE_SECOND, DOM_EVENT, + relativeNow, createTrackingConsentState, TrackingConsent, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { createNewEvent, mockClock } from '@datadog/browser-core/test' +import { createNewEvent, expireCookie, mockClock } from '@datadog/browser-core/test' import type { LogsConfiguration } from './configuration' import { @@ -71,7 +72,7 @@ describe('logs session manager', () => { it('should renew on activity after expiration', () => { startLogsSessionManagerWithDefaults() - setCookie(SESSION_STORE_KEY, 'isExpired=1', DURATION) + expireCookie() expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1') clock.tick(STORAGE_POLL_DELAY) @@ -81,8 +82,8 @@ describe('logs session manager', () => { expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) }) - describe('findSession', () => { - it('should return the current session', () => { + describe('findTrackedSession', () => { + it('should return the current active session', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) const logsSessionManager = startLogsSessionManagerWithDefaults() expect(logsSessionManager.findTrackedSession()!.id).toBe('abcdef') @@ -91,24 +92,35 @@ describe('logs session manager', () => { it('should return undefined if the session is not tracked', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=0', DURATION) const logsSessionManager = startLogsSessionManagerWithDefaults() - expect(logsSessionManager.findTrackedSession()).toBe(undefined) + expect(logsSessionManager.findTrackedSession()).toBeUndefined() + }) + + it('should not return the current session if it has expired by default', () => { + setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) + const logsSessionManager = startLogsSessionManagerWithDefaults() + clock.tick(10 * ONE_SECOND) + expireCookie() + clock.tick(STORAGE_POLL_DELAY) + expect(logsSessionManager.findTrackedSession()).toBeUndefined() }) - it('should return undefined if the session has expired', () => { + it('should return the current session if it has expired when returnExpired = true', () => { const logsSessionManager = startLogsSessionManagerWithDefaults() - setCookie(SESSION_STORE_KEY, '', DURATION) + expireCookie() clock.tick(STORAGE_POLL_DELAY) - expect(logsSessionManager.findTrackedSession()).toBe(undefined) + expect(logsSessionManager.findTrackedSession(relativeNow(), { returnInactive: true })).toBeDefined() }) it('should return session corresponding to start time', () => { - setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=foo&logs=1', DURATION) const logsSessionManager = startLogsSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) - setCookie(SESSION_STORE_KEY, '', DURATION) + setCookie(SESSION_STORE_KEY, 'id=bar&logs=1', DURATION) + // simulate a click to renew the session + document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK)) clock.tick(STORAGE_POLL_DELAY) - expect(logsSessionManager.findTrackedSession()).toBeUndefined() - expect(logsSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') + expect(logsSessionManager.findTrackedSession(0 as RelativeTime)!.id).toEqual('foo') + expect(logsSessionManager.findTrackedSession()!.id).toEqual('bar') }) }) diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index dcd426b0ef..def6986bda 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -5,7 +5,7 @@ import type { LogsConfiguration } from './configuration' export const LOGS_SESSION_KEY = 'logs' export interface LogsSessionManager { - findTrackedSession: (startTime?: RelativeTime) => LogsSession | undefined + findTrackedSession: (startTime?: RelativeTime, options?: { returnInactive: boolean }) => LogsSession | undefined expireObservable: Observable } @@ -29,8 +29,8 @@ export function startLogsSessionManager( trackingConsentState ) return { - findTrackedSession: (startTime) => { - const session = sessionManager.findActiveSession(startTime) + findTrackedSession: (startTime?: RelativeTime, options = { returnInactive: false }) => { + const session = sessionManager.findSession(startTime, options) return session && session.trackingType === LoggerTrackingType.TRACKED ? { id: session.id, diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index 22d7d1df09..56bd043bd0 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -13,7 +13,7 @@ import { BridgeCapability, } from '@datadog/browser-core' import type { Clock } from '@datadog/browser-core/test' -import { createNewEvent, initEventBridgeStub, mockClock } from '@datadog/browser-core/test' +import { createNewEvent, expireCookie, initEventBridgeStub, mockClock } from '@datadog/browser-core/test' import type { RumConfiguration } from './configuration' import { validateAndBuildRumConfiguration } from './configuration' @@ -113,7 +113,7 @@ describe('rum session manager', () => { startRumSessionManagerWithDefaults({ configuration: { sessionSampleRate: 100, sessionReplaySampleRate: 100 } }) - setCookie(SESSION_STORE_KEY, 'isExpired=1', DURATION) + expireCookie() expect(getCookie(SESSION_STORE_KEY)).toEqual('isExpired=1') expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() @@ -145,7 +145,7 @@ describe('rum session manager', () => { it('should return undefined if the session has expired', () => { const rumSessionManager = startRumSessionManagerWithDefaults() - setCookie(SESSION_STORE_KEY, 'isExpired=1', DURATION) + expireCookie() clock.tick(STORAGE_POLL_DELAY) expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) @@ -154,7 +154,7 @@ describe('rum session manager', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) const rumSessionManager = startRumSessionManagerWithDefaults() clock.tick(10 * ONE_SECOND) - setCookie(SESSION_STORE_KEY, '', DURATION) + expireCookie() clock.tick(STORAGE_POLL_DELAY) expect(rumSessionManager.findTrackedSession()).toBeUndefined() expect(rumSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index f4e8d06f10..6448e8e84b 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -52,7 +52,7 @@ export function startRumSessionManager( return { findTrackedSession: (startTime) => { - const session = sessionManager.findActiveSession(startTime) + const session = sessionManager.findSession(startTime) if (!session || !isTypeTracked(session.trackingType)) { return } diff --git a/rum-events-format b/rum-events-format index 78b1b1173a..dcd62e5856 160000 --- a/rum-events-format +++ b/rum-events-format @@ -1 +1 @@ -Subproject commit 78b1b1173a6cf14b98699b0b0646603df817fc90 +Subproject commit dcd62e58566b9d158c404f3588edc62c041262dd