From 119cc26dcdd38ded9899c48104c3695f223e3518 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Tue, 9 May 2023 17:32:18 +0200 Subject: [PATCH 01/40] =?UTF-8?q?=F0=9F=8E=A8=20Renamed=20cookieSession=20?= =?UTF-8?q?to=20sessionState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/session/sessionStore.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 98c3fe390b..2f481d50ec 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -48,16 +48,16 @@ export function startSessionStore( let isTracked: boolean withCookieLockAccess({ options, - process: (cookieSession) => { - const synchronizedSession = synchronizeSession(cookieSession) + process: (sessionState) => { + const synchronizedSession = synchronizeSession(sessionState) isTracked = expandOrRenewCookie(synchronizedSession) return synchronizedSession }, - after: (cookieSession) => { + after: (sessionState) => { if (isTracked && !hasSessionInCache()) { - renewSessionInCache(cookieSession) + renewSessionInCache(sessionState) } - sessionCache = cookieSession + sessionCache = sessionState }, }) } @@ -65,7 +65,7 @@ export function startSessionStore( function expandSession() { withCookieLockAccess({ options, - process: (cookieSession) => (hasSessionInCache() ? synchronizeSession(cookieSession) : undefined), + process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), }) } @@ -77,31 +77,31 @@ export function startSessionStore( function watchSession() { withCookieLockAccess({ options, - process: (cookieSession) => (!isActiveSession(cookieSession) ? {} : undefined), + process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), after: synchronizeSession, }) } - function synchronizeSession(cookieSession: SessionState) { - if (!isActiveSession(cookieSession)) { - cookieSession = {} + function synchronizeSession(sessionState: SessionState) { + if (!isActiveSession(sessionState)) { + sessionState = {} } if (hasSessionInCache()) { - if (isSessionInCacheOutdated(cookieSession)) { + if (isSessionInCacheOutdated(sessionState)) { expireSessionInCache() } else { - sessionCache = cookieSession + sessionCache = sessionState } } - return cookieSession + return sessionState } - function expandOrRenewCookie(cookieSession: SessionState) { - const { trackingType, isTracked } = computeSessionState(cookieSession[productKey]) - cookieSession[productKey] = trackingType - if (isTracked && !cookieSession.id) { - cookieSession.id = generateUUID() - cookieSession.created = String(dateNow()) + function expandOrRenewCookie(sessionState: SessionState) { + const { trackingType, isTracked } = computeSessionState(sessionState[productKey]) + sessionState[productKey] = trackingType + if (isTracked && !sessionState.id) { + sessionState.id = generateUUID() + sessionState.created = String(dateNow()) } return isTracked } @@ -110,8 +110,8 @@ export function startSessionStore( return sessionCache[productKey] !== undefined } - function isSessionInCacheOutdated(cookieSession: SessionState) { - return sessionCache.id !== cookieSession.id || sessionCache[productKey] !== cookieSession[productKey] + function isSessionInCacheOutdated(sessionState: SessionState) { + return sessionCache.id !== sessionState.id || sessionCache[productKey] !== sessionState[productKey] } function expireSessionInCache() { @@ -119,8 +119,8 @@ export function startSessionStore( expireObservable.notify() } - function renewSessionInCache(cookieSession: SessionState) { - sessionCache = cookieSession + function renewSessionInCache(sessionState: SessionState) { + sessionCache = sessionState renewObservable.notify() } @@ -132,12 +132,12 @@ export function startSessionStore( return {} } - function isActiveSession(session: SessionState) { + function isActiveSession(sessionDate: SessionState) { // created and expire can be undefined for versions which was not storing them // these checks could be removed when older versions will not be available/live anymore return ( - (session.created === undefined || dateNow() - Number(session.created) < SESSION_TIME_OUT_DELAY) && - (session.expire === undefined || dateNow() < Number(session.expire)) + (sessionDate.created === undefined || dateNow() - Number(sessionDate.created) < SESSION_TIME_OUT_DELAY) && + (sessionDate.expire === undefined || dateNow() < Number(sessionDate.expire)) ) } From 3aaa1fcf96e1b6a7d2bdbb6a5d883835523473cd Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Tue, 9 May 2023 17:50:51 +0200 Subject: [PATCH 02/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Create=20a=20Session?= =?UTF-8?q?Storage=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/session/oldCookiesMigration.ts | 2 +- .../src/domain/session/sessionCookieStore.spec.ts | 2 +- .../core/src/domain/session/sessionCookieStore.ts | 2 +- packages/core/src/domain/session/sessionStorage.ts | 14 ++++++++++++++ packages/core/src/domain/session/sessionStore.ts | 10 +--------- 5 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 packages/core/src/domain/session/sessionStorage.ts diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index c78e5eaf9e..486f7f82c5 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,6 +1,6 @@ import type { CookieOptions } from '../../browser/cookie' import { getCookie } from '../../browser/cookie' -import type { SessionState } from './sessionStore' +import type { SessionState } from './sessionStorage' import { SESSION_COOKIE_NAME, persistSessionCookie } from './sessionCookieStore' export const OLD_SESSION_COOKIE_NAME = '_dd' diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index 343b94934c..2d098175b6 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -9,7 +9,7 @@ import { LOCK_RETRY_DELAY, withCookieLockAccess, } from './sessionCookieStore' -import type { SessionState } from './sessionStore' +import type { SessionState } from './sessionStorage' describe('session cookie store', () => { const COOKIE_OPTIONS = {} diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index 4efeb76083..f1bb7e183e 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -7,7 +7,7 @@ import { objectEntries } from '../../tools/utils/polyfills' import { isEmptyObject } from '../../tools/utils/objectUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import type { SessionState } from './sessionStore' +import type { SessionState } from './sessionStorage' const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ const SESSION_ENTRY_SEPARATOR = '&' diff --git a/packages/core/src/domain/session/sessionStorage.ts b/packages/core/src/domain/session/sessionStorage.ts new file mode 100644 index 0000000000..bdb4f2e8b8 --- /dev/null +++ b/packages/core/src/domain/session/sessionStorage.ts @@ -0,0 +1,14 @@ +export interface SessionState { + id?: string + created?: string + expire?: string + lock?: string + + [key: string]: string | undefined +} + +export interface SessionStorage { + persistSession: (session: SessionState) => void + retrieveSession: () => SessionState + clearSession: () => void +} diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 2f481d50ec..a16efe7b81 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -7,6 +7,7 @@ import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { deleteSessionCookie, retrieveSessionCookie, withCookieLockAccess } from './sessionCookieStore' +import type { SessionState } from './sessionStorage' export interface SessionStore { expandOrRenewSession: () => void @@ -18,15 +19,6 @@ export interface SessionStore { stop: () => void } -export interface SessionState { - id?: string - created?: string - expire?: string - lock?: string - - [key: string]: string | undefined -} - /** * Different session concepts: * - tracked, the session has an id and is updated along the user navigation From c021c6712794b89980e1b2532832d73103d7e136 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Tue, 9 May 2023 20:19:04 +0200 Subject: [PATCH 03/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Move=20withCookie?= =?UTF-8?q?LockAccess=20to=20SessionStore=20and=20rename=20processStorageO?= =?UTF-8?q?perations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/session/sessionCookieStore.spec.ts | 248 +------ .../src/domain/session/sessionCookieStore.ts | 113 +-- .../src/domain/session/sessionStore.spec.ts | 683 ++++++++++++------ .../core/src/domain/session/sessionStore.ts | 108 ++- 4 files changed, 599 insertions(+), 553 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index 2d098175b6..8fe1034724 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -1,237 +1,41 @@ -import { mockClock, stubCookie } from '../../../test' -import { isChromium } from '../../tools/utils/browserDetection' +import type { CookieOptions } from '../../browser/cookie' +import { setCookie, deleteCookie } from '../../browser/cookie' +import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { SESSION_COOKIE_NAME, - toSessionString, - retrieveSessionCookie, + deleteSessionCookie, persistSessionCookie, - MAX_NUMBER_OF_LOCK_RETRIES, - LOCK_RETRY_DELAY, - withCookieLockAccess, + retrieveSessionCookie, } from './sessionCookieStore' + import type { SessionState } from './sessionStorage' describe('session cookie store', () => { - const COOKIE_OPTIONS = {} - let initialSession: SessionState - let otherSession: SessionState - let processSpy: jasmine.Spy - let afterSpy: jasmine.Spy - let cookie: ReturnType + const sessionState: SessionState = { id: '123', created: '0' } + const noOptions: CookieOptions = {} - beforeEach(() => { - initialSession = { id: '123', created: '0' } - otherSession = { id: '456', created: '100' } - processSpy = jasmine.createSpy('process') - afterSpy = jasmine.createSpy('after') - cookie = stubCookie() + afterEach(() => { + deleteCookie(SESSION_COOKIE_NAME) }) - describe('with cookie-lock disabled', () => { - beforeEach(() => { - isChromium() && pending('cookie-lock only disabled on non chromium browsers') - }) - - it('should persist session when process return a value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.returnValue({ ...otherSession }) - - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(retrieveSessionCookie()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should clear session when process return an empty value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.returnValue({}) - - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = {} - expect(retrieveSessionCookie()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should not persist session when process return undefined', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.returnValue(undefined) - - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - expect(retrieveSessionCookie()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) + it('should persist a session in a cookie', () => { + const now = Date.now() + persistSessionCookie(sessionState, noOptions) + const session = retrieveSessionCookie() + expect(session).toEqual({ ...sessionState }) + expect(+session.expire!).toBeGreaterThanOrEqual(now + SESSION_EXPIRATION_DELAY) }) - describe('with cookie-lock enabled', () => { - beforeEach(() => { - !isChromium() && pending('cookie-lock only enabled on chromium browsers') - }) - - it('should persist session when process return a value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) - - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(retrieveSessionCookie()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should clear session when process return an empty value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.returnValue({}) - - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = {} - expect(retrieveSessionCookie()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should not persist session when process return undefined', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.returnValue(undefined) - - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - expect(retrieveSessionCookie()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) - - type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } - - function lockScenario({ - onInitialLockCheck, - onAcquiredLockCheck, - onPostProcessLockCheck, - onPostPersistLockCheck, - }: { - onInitialLockCheck?: OnLockCheck - onAcquiredLockCheck?: OnLockCheck - onPostProcessLockCheck?: OnLockCheck - onPostPersistLockCheck?: OnLockCheck - }) { - const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] - cookie.getSpy.and.callFake(() => { - const currentOnLockCheck = onLockChecks.shift() - if (!currentOnLockCheck) { - return cookie.currentValue() - } - const { currentState, retryState } = currentOnLockCheck() - cookie.setCurrentValue(buildSessionString(retryState)) - return buildSessionString(currentState) - }) - } - - function buildSessionString(currentState: SessionState) { - return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` - } - - ;[ - { - description: 'should wait for lock to be free', - lockConflict: 'onInitialLockCheck', - }, - { - description: 'should retry if lock was acquired before process', - lockConflict: 'onAcquiredLockCheck', - }, - { - description: 'should retry if lock was acquired after process', - lockConflict: 'onPostProcessLockCheck', - }, - { - description: 'should retry if lock was acquired after persist', - lockConflict: 'onPostPersistLockCheck', - }, - ].forEach(({ description, lockConflict }) => { - it(description, (done) => { - lockScenario({ - [lockConflict]: () => ({ - currentState: { ...initialSession, lock: 'locked' }, - retryState: { ...initialSession, other: 'other' }, - }), - }) - persistSessionCookie(initialSession, COOKIE_OPTIONS) - processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) - - withCookieLockAccess({ - options: COOKIE_OPTIONS, - process: processSpy, - after: (afterSession) => { - // session with 'other' value on process - expect(processSpy).toHaveBeenCalledWith({ - ...initialSession, - other: 'other', - lock: jasmine.any(String), - expire: jasmine.any(String), - }) - - // end state with session 'other' and 'processed' value - const expectedSession = { - ...initialSession, - other: 'other', - processed: 'processed', - expire: jasmine.any(String), - } - expect(retrieveSessionCookie()).toEqual(expectedSession) - expect(afterSession).toEqual(expectedSession) - done() - }, - }) - }) - }) - - it('should abort after a max number of retry', () => { - const clock = mockClock() - - persistSessionCookie(initialSession, COOKIE_OPTIONS) - cookie.setSpy.calls.reset() - - cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) - withCookieLockAccess({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - - clock.tick(MAX_NUMBER_OF_LOCK_RETRIES * LOCK_RETRY_DELAY) - expect(processSpy).not.toHaveBeenCalled() - expect(afterSpy).not.toHaveBeenCalled() - expect(cookie.setSpy).not.toHaveBeenCalled() - - clock.cleanup() - }) - - it('should execute cookie accesses in order', (done) => { - lockScenario({ - onInitialLockCheck: () => ({ - currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later - retryState: initialSession, - }), - }) - persistSessionCookie(initialSession, COOKIE_OPTIONS) + it('should delete the cookie holding the session', () => { + persistSessionCookie(sessionState, noOptions) + deleteSessionCookie(noOptions) + const session = retrieveSessionCookie() + expect(session).toEqual({}) + }) - withCookieLockAccess({ - options: COOKIE_OPTIONS, - process: (session) => ({ ...session, value: 'foo' }), - after: afterSpy, - }) - withCookieLockAccess({ - options: COOKIE_OPTIONS, - process: (session) => ({ ...session, value: `${session.value || ''}bar` }), - after: (session) => { - expect(session.value).toBe('foobar') - expect(afterSpy).toHaveBeenCalled() - done() - }, - }) - }) + it('should return an empty object if session string is invalid', () => { + setCookie(SESSION_COOKIE_NAME, '{test:42}', 1000) + const session = retrieveSessionCookie() + expect(session).toEqual({}) }) }) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index f1bb7e183e..ab455fb731 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,11 +1,9 @@ import type { CookieOptions } from '../../browser/cookie' import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' -import { setTimeout } from '../../tools/timer' import { isChromium } from '../../tools/utils/browserDetection' import { dateNow } from '../../tools/utils/timeUtils' import { objectEntries } from '../../tools/utils/polyfills' import { isEmptyObject } from '../../tools/utils/objectUtils' -import { generateUUID } from '../../tools/utils/stringUtils' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState } from './sessionStorage' @@ -14,105 +12,6 @@ const SESSION_ENTRY_SEPARATOR = '&' export const SESSION_COOKIE_NAME = '_dd_s' -// arbitrary values -export const LOCK_RETRY_DELAY = 10 -export const MAX_NUMBER_OF_LOCK_RETRIES = 100 - -type Operations = { - options: CookieOptions - process: (cookieSession: SessionState) => SessionState | undefined - after?: (cookieSession: SessionState) => void -} - -const bufferedOperations: Operations[] = [] -let ongoingOperations: Operations | undefined - -export function withCookieLockAccess(operations: Operations, numberOfRetries = 0) { - if (!ongoingOperations) { - ongoingOperations = operations - } - if (operations !== ongoingOperations) { - bufferedOperations.push(operations) - return - } - if (numberOfRetries >= MAX_NUMBER_OF_LOCK_RETRIES) { - next() - return - } - let currentLock: string - let currentSession = retrieveSessionCookie() - if (isCookieLockEnabled()) { - // if someone has lock, retry later - if (currentSession.lock) { - retryLater(operations, numberOfRetries) - return - } - // acquire lock - currentLock = generateUUID() - currentSession.lock = currentLock - setSessionCookie(currentSession, operations.options) - // if lock is not acquired, retry later - currentSession = retrieveSessionCookie() - if (currentSession.lock !== currentLock) { - retryLater(operations, numberOfRetries) - return - } - } - let processedSession = operations.process(currentSession) - if (isCookieLockEnabled()) { - // if lock corrupted after process, retry later - currentSession = retrieveSessionCookie() - if (currentSession.lock !== currentLock!) { - retryLater(operations, numberOfRetries) - return - } - } - if (processedSession) { - persistSessionCookie(processedSession, operations.options) - } - if (isCookieLockEnabled()) { - // correctly handle lock around expiration would require to handle this case properly at several levels - // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it - if (!(processedSession && isExpiredState(processedSession))) { - // if lock corrupted after persist, retry later - currentSession = retrieveSessionCookie() - if (currentSession.lock !== currentLock!) { - retryLater(operations, numberOfRetries) - return - } - delete currentSession.lock - setSessionCookie(currentSession, operations.options) - processedSession = currentSession - } - } - // call after even if session is not persisted in order to perform operations on - // up-to-date cookie value, the value could have been modified by another tab - operations.after?.(processedSession || currentSession) - next() -} - -/** - * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. - * This issue concerns only chromium browsers and enabling this on firefox increase cookie write failures. - */ -function isCookieLockEnabled() { - return isChromium() -} - -function retryLater(operations: Operations, currentNumberOfRetries: number) { - setTimeout(() => { - withCookieLockAccess(operations, currentNumberOfRetries + 1) - }, LOCK_RETRY_DELAY) -} - -function next() { - ongoingOperations = undefined - const nextOperations = bufferedOperations.shift() - if (nextOperations) { - withCookieLockAccess(nextOperations) - } -} - export function persistSessionCookie(session: SessionState, options: CookieOptions) { if (isExpiredState(session)) { deleteSessionCookie(options) @@ -122,7 +21,7 @@ export function persistSessionCookie(session: SessionState, options: CookieOptio setSessionCookie(session, options) } -function setSessionCookie(session: SessionState, options: CookieOptions) { +export function setSessionCookie(session: SessionState, options: CookieOptions) { setCookie(SESSION_COOKIE_NAME, toSessionString(session), SESSION_EXPIRATION_DELAY, options) } @@ -158,6 +57,14 @@ function isValidSessionString(sessionString: string | undefined): sessionString ) } -function isExpiredState(session: SessionState) { +export function isExpiredState(session: SessionState) { return isEmptyObject(session) } + +/** + * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. + * This issue concerns only chromium browsers and enabling this on firefox increase cookie write failures. + */ +export function isCookieLockEnabled() { + return isChromium() +} diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index a4123e9b06..eab672cf00 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,11 +1,18 @@ import type { Clock } from '../../../test' -import { mockClock } from '../../../test' +import { stubCookie, mockClock } from '../../../test' import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' +import { isChromium } from '../../tools/utils/browserDetection' import type { SessionStore } from './sessionStore' -import { startSessionStore } from './sessionStore' -import { SESSION_COOKIE_NAME } from './sessionCookieStore' +import { + LOCK_RETRY_DELAY, + MAX_NUMBER_OF_LOCK_RETRIES, + processStorageOperations, + startSessionStore, +} from './sessionStore' +import { SESSION_COOKIE_NAME, persistSessionCookie, retrieveSessionCookie, toSessionString } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import type { SessionState } from './sessionStorage' const enum FakeTrackingType { TRACKED = 'tracked', @@ -47,303 +54,533 @@ function resetSessionInStore() { } describe('session store', () => { - let expireSpy: () => void - let renewSpy: () => void - let sessionStore: SessionStore - let clock: Clock - - function setupSessionStore( - computeSessionState: (rawTrackingType?: string) => { trackingType: FakeTrackingType; isTracked: boolean } = () => ({ - isTracked: true, - trackingType: FakeTrackingType.TRACKED, + describe('session lifecyle mechanism', () => { + let expireSpy: () => void + let renewSpy: () => void + let sessionStore: SessionStore + let clock: Clock + + function setupSessionStore( + computeSessionState: (rawTrackingType?: string) => { + trackingType: FakeTrackingType + isTracked: boolean + } = () => ({ + isTracked: true, + trackingType: FakeTrackingType.TRACKED, + }) + ) { + sessionStore = startSessionStore(COOKIE_OPTIONS, PRODUCT_KEY, computeSessionState) + sessionStore.expireObservable.subscribe(expireSpy) + sessionStore.renewObservable.subscribe(renewSpy) + } + + beforeEach(() => { + expireSpy = jasmine.createSpy('expire session') + renewSpy = jasmine.createSpy('renew session') + clock = mockClock() }) - ) { - sessionStore = startSessionStore(COOKIE_OPTIONS, PRODUCT_KEY, computeSessionState) - sessionStore.expireObservable.subscribe(expireSpy) - sessionStore.renewObservable.subscribe(renewSpy) - } - - beforeEach(() => { - expireSpy = jasmine.createSpy('expire session') - renewSpy = jasmine.createSpy('renew session') - clock = mockClock() - }) - afterEach(() => { - resetSessionInStore() - clock.cleanup() - sessionStore.stop() - }) + afterEach(() => { + resetSessionInStore() + clock.cleanup() + sessionStore.stop() + }) - describe('expand or renew session', () => { - it( - 'when session not in cache, session not in store and new session tracked, ' + - 'should create new session and trigger renew session ', - () => { + describe('expand or renew session', () => { + it( + 'when session not in cache, session not in store and new session tracked, ' + + 'should create new session and trigger renew session ', + () => { + setupSessionStore() + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeDefined() + expectTrackedSessionToBeInStore() + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + it( + 'when session not in cache, session not in store and new session not tracked, ' + + 'should store not tracked session', + () => { + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it('when session not in cache and session in store, should expand session and trigger renew session', () => { setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) sessionStore.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeDefined() - expectTrackedSessionToBeInStore() + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expectTrackedSessionToBeInStore(FIRST_ID) expect(expireSpy).not.toHaveBeenCalled() expect(renewSpy).toHaveBeenCalled() - } - ) - - it( - 'when session not in cache, session not in store and new session not tracked, ' + - 'should store not tracked session', - () => { - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + }) + + it( + 'when session in cache, session not in store and new session tracked, ' + + 'should expire session, create a new one and trigger renew session', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + sessionStore.expandOrRenewSession() + + const sessionId = sessionStore.getSession().id + expect(sessionId).toBeDefined() + expect(sessionId).not.toBe(FIRST_ID) + expectTrackedSessionToBeInStore(sessionId) + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + it( + 'when session in cache, session not in store and new session not tracked, ' + + 'should expire session and store not tracked session', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + resetSessionInStore() + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it( + 'when session not tracked in cache, session not in store and new session not tracked, ' + + 'should expire session and store not tracked session', + () => { + setSessionInStore(FakeTrackingType.NOT_TRACKED) + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + resetSessionInStore() + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it('when session in cache is same session than in store, should expand session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + clock.tick(10) sessionStore.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeUndefined() - expectNotTrackedSessionToBeInStore() + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expectTrackedSessionToBeInStore(FIRST_ID) expect(expireSpy).not.toHaveBeenCalled() expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it('when session not in cache and session in store, should expand session and trigger renew session', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expectTrackedSessionToBeInStore(FIRST_ID) - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() + }) + + it( + 'when session in cache is different session than in store and store session is tracked, ' + + 'should expire session, expand store session and trigger renew', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBe(SECOND_ID) + expectTrackedSessionToBeInStore(SECOND_ID) + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + 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', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore((rawTrackingType) => ({ + isTracked: rawTrackingType === FakeTrackingType.TRACKED, + trackingType: rawTrackingType as FakeTrackingType, + })) + setSessionInStore(FakeTrackingType.NOT_TRACKED, '') + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) }) - it( - 'when session in cache, session not in store and new session tracked, ' + - 'should expire session, create a new one and trigger renew session', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + describe('expand session', () => { + it('when session not in cache and session not in store, should do nothing', () => { setupSessionStore() - resetSessionInStore() - sessionStore.expandOrRenewSession() + sessionStore.expandSession() - const sessionId = sessionStore.getSession().id - expect(sessionId).toBeDefined() - expect(sessionId).not.toBe(FIRST_ID) - expectTrackedSessionToBeInStore(sessionId) - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) - it( - 'when session in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', - () => { + it('when session not in cache and session in store, should do nothing', () => { + setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - resetSessionInStore() - sessionStore.expandOrRenewSession() + sessionStore.expandSession() expect(sessionStore.getSession().id).toBeUndefined() - expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it( - 'when session not tracked in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', - () => { - setSessionInStore(FakeTrackingType.NOT_TRACKED) - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache and session not in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() resetSessionInStore() - sessionStore.expandOrRenewSession() + sessionStore.expandSession() expect(sessionStore.getSession().id).toBeUndefined() - expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() - expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) + }) - it('when session in cache is same session than in store, should expand session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() + it('when session in cache is same session than in store, should expand session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() - clock.tick(10) - sessionStore.expandOrRenewSession() + clock.tick(10) + sessionStore.expandSession() - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) - expectTrackedSessionToBeInStore(FIRST_ID) - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - }) + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(expireSpy).not.toHaveBeenCalled() + }) - it( - 'when session in cache is different session than in store and store session is tracked, ' + - 'should expire session, expand store session and trigger renew', - () => { + it('when session in cache is different session than in store, should expire session', () => { setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - sessionStore.expandOrRenewSession() + sessionStore.expandSession() - expect(sessionStore.getSession().id).toBe(SECOND_ID) + expect(sessionStore.getSession().id).toBeUndefined() expectTrackedSessionToBeInStore(SECOND_ID) expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) + }) + }) - 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', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore((rawTrackingType) => ({ - isTracked: rawTrackingType === FakeTrackingType.TRACKED, - trackingType: rawTrackingType as FakeTrackingType, - })) - setSessionInStore(FakeTrackingType.NOT_TRACKED, '') + describe('regular watch', () => { + it('when session not in cache and session not in store, should do nothing', () => { + setupSessionStore() - sessionStore.expandOrRenewSession() + clock.tick(COOKIE_ACCESS_DELAY) expect(sessionStore.getSession().id).toBeUndefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - }) + expect(expireSpy).not.toHaveBeenCalled() + }) - describe('expand session', () => { - it('when session not in cache and session not in store, should do nothing', () => { - setupSessionStore() + it('when session not in cache and session in store, should do nothing', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - sessionStore.expandSession() + clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) - it('when session not in cache and session in store, should do nothing', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + it('when session in cache and session not in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() - sessionStore.expandSession() + clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) - it('when session in cache and session not in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() + it('when session in cache is same session than in store, should synchronize session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) - sessionStore.expandSession() + clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(expireSpy).not.toHaveBeenCalled() + }) - it('when session in cache is same session than in store, should expand session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() + it('when session id in cache is different than session id in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - clock.tick(10) - sessionStore.expandSession() + clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) - expect(expireSpy).not.toHaveBeenCalled() - }) + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) - it('when session in cache is different session than in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + it('when session type in cache is different than session type in store, should expire session', () => { + setSessionInStore(FakeTrackingType.NOT_TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - sessionStore.expandSession() + clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() - expectTrackedSessionToBeInStore(SECOND_ID) - expect(expireSpy).toHaveBeenCalled() + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) }) }) - describe('regular watch', () => { - it('when session not in cache and session not in store, should do nothing', () => { - setupSessionStore() - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() + describe('process operations mechanism', () => { + const COOKIE_OPTIONS = {} + let initialSession: SessionState + let otherSession: SessionState + let processSpy: jasmine.Spy + let afterSpy: jasmine.Spy + let cookie: ReturnType + + beforeEach(() => { + initialSession = { id: '123', created: '0' } + otherSession = { id: '456', created: '100' } + processSpy = jasmine.createSpy('process') + afterSpy = jasmine.createSpy('after') + cookie = stubCookie() }) - it('when session not in cache and session in store, should do nothing', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + describe('with cookie-lock disabled', () => { + beforeEach(() => { + isChromium() && pending('cookie-lock only disabled on non chromium browsers') + }) - clock.tick(COOKIE_ACCESS_DELAY) + it('should persist session when process returns a value', () => { + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.returnValue({ ...otherSession }) - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - it('when session in cache and session not in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) - clock.tick(COOKIE_ACCESS_DELAY) + it('should clear session when process return an empty value', () => { + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.returnValue({}) - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - it('when session in cache is same session than in store, should synchronize session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = {} + expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) - clock.tick(COOKIE_ACCESS_DELAY) + it('should not persist session when process return undefined', () => { + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.returnValue(undefined) - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session id in cache is different than session id in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - clock.tick(COOKIE_ACCESS_DELAY) + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() + expect(processSpy).toHaveBeenCalledWith(initialSession) + expect(retrieveSessionCookie()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) }) - it('when session type in cache is different than session type in store, should expire session', () => { - setSessionInStore(FakeTrackingType.NOT_TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + describe('with cookie-lock enabled', () => { + beforeEach(() => { + !isChromium() && pending('cookie-lock only enabled on chromium browsers') + }) + + it('should persist session when process return a value', () => { + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) + + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should clear session when process return an empty value', () => { + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.returnValue({}) + + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = {} + expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should not persist session when process return undefined', () => { + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.returnValue(undefined) + + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + expect(retrieveSessionCookie()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) + + type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } + + function lockScenario({ + onInitialLockCheck, + onAcquiredLockCheck, + onPostProcessLockCheck, + onPostPersistLockCheck, + }: { + onInitialLockCheck?: OnLockCheck + onAcquiredLockCheck?: OnLockCheck + onPostProcessLockCheck?: OnLockCheck + onPostPersistLockCheck?: OnLockCheck + }) { + const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] + cookie.getSpy.and.callFake(() => { + const currentOnLockCheck = onLockChecks.shift() + if (!currentOnLockCheck) { + return cookie.currentValue() + } + const { currentState, retryState } = currentOnLockCheck() + cookie.setCurrentValue(buildSessionString(retryState)) + return buildSessionString(currentState) + }) + } - clock.tick(COOKIE_ACCESS_DELAY) + function buildSessionString(currentState: SessionState) { + return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` + } - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() + ;[ + { + description: 'should wait for lock to be free', + lockConflict: 'onInitialLockCheck', + }, + { + description: 'should retry if lock was acquired before process', + lockConflict: 'onAcquiredLockCheck', + }, + { + description: 'should retry if lock was acquired after process', + lockConflict: 'onPostProcessLockCheck', + }, + { + description: 'should retry if lock was acquired after persist', + lockConflict: 'onPostPersistLockCheck', + }, + ].forEach(({ description, lockConflict }) => { + it(description, (done) => { + lockScenario({ + [lockConflict]: () => ({ + currentState: { ...initialSession, lock: 'locked' }, + retryState: { ...initialSession, other: 'other' }, + }), + }) + persistSessionCookie(initialSession, COOKIE_OPTIONS) + processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) + + processStorageOperations({ + options: COOKIE_OPTIONS, + process: processSpy, + after: (afterSession) => { + // session with 'other' value on process + expect(processSpy).toHaveBeenCalledWith({ + ...initialSession, + other: 'other', + lock: jasmine.any(String), + expire: jasmine.any(String), + }) + + // end state with session 'other' and 'processed' value + const expectedSession = { + ...initialSession, + other: 'other', + processed: 'processed', + expire: jasmine.any(String), + } + expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(afterSession).toEqual(expectedSession) + done() + }, + }) + }) + }) + + it('should abort after a max number of retry', () => { + const clock = mockClock() + + persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookie.setSpy.calls.reset() + + cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) + processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + + clock.tick(MAX_NUMBER_OF_LOCK_RETRIES * LOCK_RETRY_DELAY) + expect(processSpy).not.toHaveBeenCalled() + expect(afterSpy).not.toHaveBeenCalled() + expect(cookie.setSpy).not.toHaveBeenCalled() + + clock.cleanup() + }) + + it('should execute cookie accesses in order', (done) => { + lockScenario({ + onInitialLockCheck: () => ({ + currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later + retryState: initialSession, + }), + }) + persistSessionCookie(initialSession, COOKIE_OPTIONS) + + processStorageOperations({ + options: COOKIE_OPTIONS, + process: (session) => ({ ...session, value: 'foo' }), + after: afterSpy, + }) + processStorageOperations({ + options: COOKIE_OPTIONS, + process: (session) => ({ ...session, value: `${session.value || ''}bar` }), + after: (session) => { + expect(session.value).toBe('foobar') + expect(afterSpy).toHaveBeenCalled() + done() + }, + }) + }) }) }) }) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index a16efe7b81..278628cff0 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -1,12 +1,19 @@ import type { CookieOptions } from '../../browser/cookie' import { COOKIE_ACCESS_DELAY } from '../../browser/cookie' -import { clearInterval, setInterval } from '../../tools/timer' +import { clearInterval, setInterval, setTimeout } from '../../tools/timer' import { Observable } from '../../tools/observable' import { dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { deleteSessionCookie, retrieveSessionCookie, withCookieLockAccess } from './sessionCookieStore' +import { + deleteSessionCookie, + isCookieLockEnabled, + isExpiredState, + persistSessionCookie, + retrieveSessionCookie, + setSessionCookie, +} from './sessionCookieStore' import type { SessionState } from './sessionStorage' export interface SessionStore { @@ -38,7 +45,7 @@ export function startSessionStore( function expandOrRenewSession() { let isTracked: boolean - withCookieLockAccess({ + processStorageOperations({ options, process: (sessionState) => { const synchronizedSession = synchronizeSession(sessionState) @@ -55,7 +62,7 @@ export function startSessionStore( } function expandSession() { - withCookieLockAccess({ + processStorageOperations({ options, process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), }) @@ -67,7 +74,7 @@ export function startSessionStore( * - if the session is not active, clear the session cookie and expire the session cache */ function watchSession() { - withCookieLockAccess({ + processStorageOperations({ options, process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), after: synchronizeSession, @@ -148,3 +155,94 @@ export function startSessionStore( }, } } + +// arbitrary values +export const LOCK_RETRY_DELAY = 10 +export const MAX_NUMBER_OF_LOCK_RETRIES = 100 + +type Operations = { + options: CookieOptions + process: (cookieSession: SessionState) => SessionState | undefined + after?: (cookieSession: SessionState) => void +} + +const bufferedOperations: Operations[] = [] +let ongoingOperations: Operations | undefined + +export function processStorageOperations(operations: Operations, numberOfRetries = 0) { + if (!ongoingOperations) { + ongoingOperations = operations + } + if (operations !== ongoingOperations) { + bufferedOperations.push(operations) + return + } + if (numberOfRetries >= MAX_NUMBER_OF_LOCK_RETRIES) { + next() + return + } + let currentLock: string + let currentSession = retrieveSessionCookie() + if (isCookieLockEnabled()) { + // if someone has lock, retry later + if (currentSession.lock) { + retryLater(operations, numberOfRetries) + return + } + // acquire lock + currentLock = generateUUID() + currentSession.lock = currentLock + setSessionCookie(currentSession, operations.options) + // if lock is not acquired, retry later + currentSession = retrieveSessionCookie() + if (currentSession.lock !== currentLock) { + retryLater(operations, numberOfRetries) + return + } + } + let processedSession = operations.process(currentSession) + if (isCookieLockEnabled()) { + // if lock corrupted after process, retry later + currentSession = retrieveSessionCookie() + if (currentSession.lock !== currentLock!) { + retryLater(operations, numberOfRetries) + return + } + } + if (processedSession) { + persistSessionCookie(processedSession, operations.options) + } + if (isCookieLockEnabled()) { + // correctly handle lock around expiration would require to handle this case properly at several levels + // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it + if (!(processedSession && isExpiredState(processedSession))) { + // if lock corrupted after persist, retry later + currentSession = retrieveSessionCookie() + if (currentSession.lock !== currentLock!) { + retryLater(operations, numberOfRetries) + return + } + delete currentSession.lock + setSessionCookie(currentSession, operations.options) + processedSession = currentSession + } + } + // call after even if session is not persisted in order to perform operations on + // up-to-date cookie value, the value could have been modified by another tab + operations.after?.(processedSession || currentSession) + next() +} + +function retryLater(operations: Operations, currentNumberOfRetries: number) { + setTimeout(() => { + processStorageOperations(operations, currentNumberOfRetries + 1) + }, LOCK_RETRY_DELAY) +} + +function next() { + ongoingOperations = undefined + const nextOperations = bufferedOperations.shift() + if (nextOperations) { + processStorageOperations(nextOperations) + } +} From ce2f8157ef3dc22497e046fe8ab6c8411e47df7b Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 11 May 2023 11:49:50 +0200 Subject: [PATCH 04/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Refactor=20Sessio?= =?UTF-8?q?nCookieStore=20to=20use=20SessionStorage=20interface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/oldCookiesMigration.ts | 13 +- .../domain/session/sessionCookieStore.spec.ts | 29 ++-- .../src/domain/session/sessionCookieStore.ts | 31 ++--- .../src/domain/session/sessionManager.spec.ts | 4 +- .../src/domain/session/sessionStorage.spec.ts | 15 ++ .../core/src/domain/session/sessionStorage.ts | 6 + .../src/domain/session/sessionStore.spec.ts | 129 ++++++++++-------- .../core/src/domain/session/sessionStore.ts | 117 ++++++++-------- 8 files changed, 193 insertions(+), 151 deletions(-) create mode 100644 packages/core/src/domain/session/sessionStorage.spec.ts diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index 486f7f82c5..02937b4129 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,7 +1,10 @@ import type { CookieOptions } from '../../browser/cookie' import { getCookie } from '../../browser/cookie' +import { dateNow } from '../../tools/utils/timeUtils' import type { SessionState } from './sessionStorage' -import { SESSION_COOKIE_NAME, persistSessionCookie } from './sessionCookieStore' +import { isSessionInExpiredState } from './sessionStorage' +import { SESSION_COOKIE_NAME, deleteSessionCookie, persistSessionCookie } from './sessionCookieStore' +import { SESSION_EXPIRATION_DELAY } from './sessionConstants' export const OLD_SESSION_COOKIE_NAME = '_dd' export const OLD_RUM_COOKIE_NAME = '_dd_r' @@ -31,6 +34,12 @@ export function tryOldCookiesMigration(options: CookieOptions) { if (oldRumType && /^[012]$/.test(oldRumType)) { session[RUM_SESSION_KEY] = oldRumType } - persistSessionCookie(session, options) + + if (!isSessionInExpiredState(session)) { + session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) + persistSessionCookie(options)(session) + } else { + deleteSessionCookie(options)() + } } } diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index 8fe1034724..5ca34e57d4 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -1,41 +1,38 @@ import type { CookieOptions } from '../../browser/cookie' import { setCookie, deleteCookie } from '../../browser/cookie' -import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { - SESSION_COOKIE_NAME, - deleteSessionCookie, - persistSessionCookie, - retrieveSessionCookie, -} from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStorage } from './sessionCookieStore' -import type { SessionState } from './sessionStorage' +import type { SessionState, SessionStorage } from './sessionStorage' describe('session cookie store', () => { const sessionState: SessionState = { id: '123', created: '0' } const noOptions: CookieOptions = {} + let cookieStorage: SessionStorage + + beforeEach(() => { + cookieStorage = initCookieStorage(noOptions) + }) afterEach(() => { deleteCookie(SESSION_COOKIE_NAME) }) it('should persist a session in a cookie', () => { - const now = Date.now() - persistSessionCookie(sessionState, noOptions) - const session = retrieveSessionCookie() + cookieStorage.persistSession(sessionState) + const session = cookieStorage.retrieveSession() expect(session).toEqual({ ...sessionState }) - expect(+session.expire!).toBeGreaterThanOrEqual(now + SESSION_EXPIRATION_DELAY) }) it('should delete the cookie holding the session', () => { - persistSessionCookie(sessionState, noOptions) - deleteSessionCookie(noOptions) - const session = retrieveSessionCookie() + cookieStorage.persistSession(sessionState) + cookieStorage.clearSession() + const session = cookieStorage.retrieveSession() expect(session).toEqual({}) }) it('should return an empty object if session string is invalid', () => { setCookie(SESSION_COOKIE_NAME, '{test:42}', 1000) - const session = retrieveSessionCookie() + const session = cookieStorage.retrieveSession() expect(session).toEqual({}) }) }) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index ab455fb731..e90354f09c 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,28 +1,27 @@ import type { CookieOptions } from '../../browser/cookie' import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' import { isChromium } from '../../tools/utils/browserDetection' -import { dateNow } from '../../tools/utils/timeUtils' import { objectEntries } from '../../tools/utils/polyfills' -import { isEmptyObject } from '../../tools/utils/objectUtils' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import type { SessionState } from './sessionStorage' +import type { SessionState, SessionStorage } from './sessionStorage' const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ const SESSION_ENTRY_SEPARATOR = '&' export const SESSION_COOKIE_NAME = '_dd_s' -export function persistSessionCookie(session: SessionState, options: CookieOptions) { - if (isExpiredState(session)) { - deleteSessionCookie(options) - return +export function initCookieStorage(options: CookieOptions): SessionStorage { + return { + persistSession: persistSessionCookie(options), + retrieveSession: retrieveSessionCookie, + clearSession: deleteSessionCookie(options), } - session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) - setSessionCookie(session, options) } -export function setSessionCookie(session: SessionState, options: CookieOptions) { - setCookie(SESSION_COOKIE_NAME, toSessionString(session), SESSION_EXPIRATION_DELAY, options) +export function persistSessionCookie(options: CookieOptions) { + return (session: SessionState) => { + setCookie(SESSION_COOKIE_NAME, toSessionString(session), SESSION_EXPIRATION_DELAY, options) + } } export function toSessionString(session: SessionState) { @@ -31,7 +30,7 @@ export function toSessionString(session: SessionState) { .join(SESSION_ENTRY_SEPARATOR) } -export function retrieveSessionCookie(): SessionState { +function retrieveSessionCookie(): SessionState { const sessionString = getCookie(SESSION_COOKIE_NAME) const session: SessionState = {} if (isValidSessionString(sessionString)) { @@ -47,7 +46,9 @@ export function retrieveSessionCookie(): SessionState { } export function deleteSessionCookie(options: CookieOptions) { - deleteCookie(SESSION_COOKIE_NAME, options) + return () => { + deleteCookie(SESSION_COOKIE_NAME, options) + } } function isValidSessionString(sessionString: string | undefined): sessionString is string { @@ -57,10 +58,6 @@ function isValidSessionString(sessionString: string | undefined): sessionString ) } -export function isExpiredState(session: SessionState) { - return isEmptyObject(session) -} - /** * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. * This issue concerns only chromium browsers and enabling this on firefox increase cookie write failures. diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index a2cbc72d52..90c27f9f79 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -19,12 +19,12 @@ const enum FakeTrackingType { const TRACKED_SESSION_STATE = { isTracked: true, trackingType: FakeTrackingType.TRACKED, -} +} as const const NOT_TRACKED_SESSION_STATE = { isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED, -} +} as const describe('startSessionManager', () => { const DURATION = 123456 diff --git a/packages/core/src/domain/session/sessionStorage.spec.ts b/packages/core/src/domain/session/sessionStorage.spec.ts new file mode 100644 index 0000000000..e3b8de5dba --- /dev/null +++ b/packages/core/src/domain/session/sessionStorage.spec.ts @@ -0,0 +1,15 @@ +import type { SessionState } from './sessionStorage' +import { isSessionInExpiredState } from './sessionStorage' + +describe('session storage utilities', () => { + const EXPIRED_SESSION: SessionState = {} + const LIVE_SESSION: SessionState = { created: '0', id: '123' } + + it('should correctly identify a session in expired state', () => { + expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) + }) + + it('should correctly identify a session in live state', () => { + expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) + }) +}) diff --git a/packages/core/src/domain/session/sessionStorage.ts b/packages/core/src/domain/session/sessionStorage.ts index bdb4f2e8b8..8b21dc5be0 100644 --- a/packages/core/src/domain/session/sessionStorage.ts +++ b/packages/core/src/domain/session/sessionStorage.ts @@ -1,3 +1,5 @@ +import { isEmptyObject } from '../../tools/utils/objectUtils' + export interface SessionState { id?: string created?: string @@ -12,3 +14,7 @@ export interface SessionStorage { retrieveSession: () => SessionState clearSession: () => void } + +export function isSessionInExpiredState(session: SessionState) { + return isEmptyObject(session) +} diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index eab672cf00..6a2f8ae3bb 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -10,9 +10,9 @@ import { processStorageOperations, startSessionStore, } from './sessionStore' -import { SESSION_COOKIE_NAME, persistSessionCookie, retrieveSessionCookie, toSessionString } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStorage, toSessionString } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import type { SessionState } from './sessionStorage' +import type { SessionState, SessionStorage } from './sessionStorage' const enum FakeTrackingType { TRACKED = 'tracked', @@ -366,8 +366,10 @@ describe('session store', () => { let processSpy: jasmine.Spy let afterSpy: jasmine.Spy let cookie: ReturnType + let cookieStorage: SessionStorage beforeEach(() => { + cookieStorage = initCookieStorage(COOKIE_OPTIONS) initialSession = { id: '123', created: '0' } otherSession = { id: '456', created: '100' } processSpy = jasmine.createSpy('process') @@ -381,37 +383,37 @@ describe('session store', () => { }) it('should persist session when process returns a value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) processSpy.and.returnValue({ ...otherSession }) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) expect(processSpy).toHaveBeenCalledWith(initialSession) const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should clear session when process return an empty value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) processSpy.and.returnValue({}) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) expect(processSpy).toHaveBeenCalledWith(initialSession) const expectedSession = {} - expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should not persist session when process return undefined', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) processSpy.and.returnValue(undefined) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) expect(processSpy).toHaveBeenCalledWith(initialSession) - expect(retrieveSessionCookie()).toEqual(initialSession) + expect(cookieStorage.retrieveSession()).toEqual(initialSession) expect(afterSpy).toHaveBeenCalledWith(initialSession) }) }) @@ -422,37 +424,37 @@ describe('session store', () => { }) it('should persist session when process return a value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should clear session when process return an empty value', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) processSpy.and.returnValue({}) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) const expectedSession = {} - expect(retrieveSessionCookie()).toEqual(expectedSession) + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should not persist session when process return undefined', () => { - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) processSpy.and.returnValue(undefined) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - expect(retrieveSessionCookie()).toEqual(initialSession) + expect(cookieStorage.retrieveSession()).toEqual(initialSession) expect(afterSpy).toHaveBeenCalledWith(initialSession) }) @@ -510,44 +512,47 @@ describe('session store', () => { retryState: { ...initialSession, other: 'other' }, }), }) - persistSessionCookie(initialSession, COOKIE_OPTIONS) + initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) + cookieStorage.persistSession(initialSession) processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) - processStorageOperations({ - options: COOKIE_OPTIONS, - process: processSpy, - after: (afterSession) => { - // session with 'other' value on process - expect(processSpy).toHaveBeenCalledWith({ - ...initialSession, - other: 'other', - lock: jasmine.any(String), - expire: jasmine.any(String), - }) - - // end state with session 'other' and 'processed' value - const expectedSession = { - ...initialSession, - other: 'other', - processed: 'processed', - expire: jasmine.any(String), - } - expect(retrieveSessionCookie()).toEqual(expectedSession) - expect(afterSession).toEqual(expectedSession) - done() + processStorageOperations( + { + process: processSpy, + after: (afterSession) => { + // session with 'other' value on process + expect(processSpy).toHaveBeenCalledWith({ + ...initialSession, + other: 'other', + lock: jasmine.any(String), + expire: jasmine.any(String), + }) + + // end state with session 'other' and 'processed' value + const expectedSession = { + ...initialSession, + other: 'other', + processed: 'processed', + expire: jasmine.any(String), + } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSession).toEqual(expectedSession) + done() + }, }, - }) + cookieStorage + ) }) }) it('should abort after a max number of retry', () => { const clock = mockClock() - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) cookie.setSpy.calls.reset() cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) - processStorageOperations({ options: COOKIE_OPTIONS, process: processSpy, after: afterSpy }) + processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) clock.tick(MAX_NUMBER_OF_LOCK_RETRIES * LOCK_RETRY_DELAY) expect(processSpy).not.toHaveBeenCalled() @@ -564,22 +569,26 @@ describe('session store', () => { retryState: initialSession, }), }) - persistSessionCookie(initialSession, COOKIE_OPTIONS) + cookieStorage.persistSession(initialSession) - processStorageOperations({ - options: COOKIE_OPTIONS, - process: (session) => ({ ...session, value: 'foo' }), - after: afterSpy, - }) - processStorageOperations({ - options: COOKIE_OPTIONS, - process: (session) => ({ ...session, value: `${session.value || ''}bar` }), - after: (session) => { - expect(session.value).toBe('foobar') - expect(afterSpy).toHaveBeenCalled() - done() + processStorageOperations( + { + process: (session) => ({ ...session, value: 'foo' }), + after: afterSpy, }, - }) + cookieStorage + ) + processStorageOperations( + { + process: (session) => ({ ...session, value: `${session.value || ''}bar` }), + after: (session) => { + expect(session.value).toBe('foobar') + expect(afterSpy).toHaveBeenCalled() + done() + }, + }, + cookieStorage + ) }) }) }) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 278628cff0..46982ad573 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -5,16 +5,10 @@ import { Observable } from '../../tools/observable' import { dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' -import { SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { - deleteSessionCookie, - isCookieLockEnabled, - isExpiredState, - persistSessionCookie, - retrieveSessionCookie, - setSessionCookie, -} from './sessionCookieStore' -import type { SessionState } from './sessionStorage' +import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { isCookieLockEnabled, initCookieStorage } from './sessionCookieStore' +import type { SessionState, SessionStorage } from './sessionStorage' +import { isSessionInExpiredState } from './sessionStorage' export interface SessionStore { expandOrRenewSession: () => void @@ -40,32 +34,39 @@ export function startSessionStore( const renewObservable = new Observable() const expireObservable = new Observable() + const sessionStorage = initCookieStorage(options) + const { clearSession, retrieveSession } = sessionStorage + const watchSessionTimeoutId = setInterval(watchSession, COOKIE_ACCESS_DELAY) let sessionCache: SessionState = retrieveActiveSession() function expandOrRenewSession() { let isTracked: boolean - processStorageOperations({ - options, - process: (sessionState) => { - const synchronizedSession = synchronizeSession(sessionState) - isTracked = expandOrRenewCookie(synchronizedSession) - return synchronizedSession - }, - after: (sessionState) => { - if (isTracked && !hasSessionInCache()) { - renewSessionInCache(sessionState) - } - sessionCache = sessionState + processStorageOperations( + { + process: (sessionState) => { + const synchronizedSession = synchronizeSession(sessionState) + isTracked = expandOrRenewCookie(synchronizedSession) + return synchronizedSession + }, + after: (sessionState) => { + if (isTracked && !hasSessionInCache()) { + renewSessionInCache(sessionState) + } + sessionCache = sessionState + }, }, - }) + sessionStorage + ) } function expandSession() { - processStorageOperations({ - options, - process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), - }) + processStorageOperations( + { + process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), + }, + sessionStorage + ) } /** @@ -74,11 +75,13 @@ export function startSessionStore( * - if the session is not active, clear the session cookie and expire the session cache */ function watchSession() { - processStorageOperations({ - options, - process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), - after: synchronizeSession, - }) + processStorageOperations( + { + process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), + after: synchronizeSession, + }, + sessionStorage + ) } function synchronizeSession(sessionState: SessionState) { @@ -124,7 +127,7 @@ export function startSessionStore( } function retrieveActiveSession(): SessionState { - const session = retrieveSessionCookie() + const session = retrieveSession() if (isActiveSession(session)) { return session } @@ -147,7 +150,7 @@ export function startSessionStore( renewObservable, expireObservable, expire: () => { - deleteSessionCookie(options) + clearSession() synchronizeSession({}) }, stop: () => { @@ -161,7 +164,6 @@ export const LOCK_RETRY_DELAY = 10 export const MAX_NUMBER_OF_LOCK_RETRIES = 100 type Operations = { - options: CookieOptions process: (cookieSession: SessionState) => SessionState | undefined after?: (cookieSession: SessionState) => void } @@ -169,7 +171,9 @@ type Operations = { const bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined -export function processStorageOperations(operations: Operations, numberOfRetries = 0) { +export function processStorageOperations(operations: Operations, sessionStorage: SessionStorage, numberOfRetries = 0) { + const { retrieveSession, persistSession, clearSession } = sessionStorage + if (!ongoingOperations) { ongoingOperations = operations } @@ -178,71 +182,76 @@ export function processStorageOperations(operations: Operations, numberOfRetries return } if (numberOfRetries >= MAX_NUMBER_OF_LOCK_RETRIES) { - next() + next(sessionStorage) return } let currentLock: string - let currentSession = retrieveSessionCookie() + let currentSession = retrieveSession() if (isCookieLockEnabled()) { // if someone has lock, retry later if (currentSession.lock) { - retryLater(operations, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries) return } // acquire lock currentLock = generateUUID() currentSession.lock = currentLock - setSessionCookie(currentSession, operations.options) + persistSession(currentSession) // if lock is not acquired, retry later - currentSession = retrieveSessionCookie() + currentSession = retrieveSession() if (currentSession.lock !== currentLock) { - retryLater(operations, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries) return } } let processedSession = operations.process(currentSession) if (isCookieLockEnabled()) { // if lock corrupted after process, retry later - currentSession = retrieveSessionCookie() + currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries) return } } if (processedSession) { - persistSessionCookie(processedSession, operations.options) + if (isSessionInExpiredState(processedSession)) { + clearSession() + } else { + processedSession.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) + persistSession(processedSession) + } } if (isCookieLockEnabled()) { // correctly handle lock around expiration would require to handle this case properly at several levels // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it - if (!(processedSession && isExpiredState(processedSession))) { + if (!(processedSession && isSessionInExpiredState(processedSession))) { // if lock corrupted after persist, retry later - currentSession = retrieveSessionCookie() + currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries) return } delete currentSession.lock - setSessionCookie(currentSession, operations.options) + persistSession(currentSession) processedSession = currentSession } } // call after even if session is not persisted in order to perform operations on // up-to-date cookie value, the value could have been modified by another tab operations.after?.(processedSession || currentSession) - next() + next(sessionStorage) } -function retryLater(operations: Operations, currentNumberOfRetries: number) { +function retryLater(operations: Operations, sessionStorage: SessionStorage, currentNumberOfRetries: number) { setTimeout(() => { - processStorageOperations(operations, currentNumberOfRetries + 1) + processStorageOperations(operations, sessionStorage, currentNumberOfRetries + 1) }, LOCK_RETRY_DELAY) } -function next() { +function next(sessionStorage: SessionStorage) { ongoingOperations = undefined const nextOperations = bufferedOperations.shift() if (nextOperations) { - processStorageOperations(nextOperations) + processStorageOperations(nextOperations, sessionStorage) } } From f26717e8a6e1703f0daf8910302470872e39e2f2 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 15 May 2023 10:56:28 +0200 Subject: [PATCH 05/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Update=20session?= =?UTF-8?q?=20storage=20interface=20and=20move=20back=20lock=20configurati?= =?UTF-8?q?on=20to=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/sessionCookieStore.ts | 24 +++++---- .../core/src/domain/session/sessionStorage.ts | 18 +++++++ .../src/domain/session/sessionStore.spec.ts | 16 +++--- .../core/src/domain/session/sessionStore.ts | 49 +++++++++---------- 4 files changed, 66 insertions(+), 41 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index e90354f09c..d887916aeb 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,5 +1,5 @@ import type { CookieOptions } from '../../browser/cookie' -import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' +import { COOKIE_ACCESS_DELAY, deleteCookie, getCookie, setCookie } from '../../browser/cookie' import { isChromium } from '../../tools/utils/browserDetection' import { objectEntries } from '../../tools/utils/polyfills' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' @@ -10,8 +10,22 @@ const SESSION_ENTRY_SEPARATOR = '&' export const SESSION_COOKIE_NAME = '_dd_s' +// Arbitrary values +export const LOCK_RETRY_DELAY = 10 +export const MAX_NUMBER_OF_LOCK_RETRIES = 100 + export function initCookieStorage(options: CookieOptions): SessionStorage { return { + storageAccessOptions: { + pollDelay: COOKIE_ACCESS_DELAY, + /** + * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. + * This issue concerns only chromium browsers and enabling this on firefox increase cookie write failures. + */ + lockEnabled: isChromium(), + lockRetryDelay: LOCK_RETRY_DELAY, + lockMaxTries: MAX_NUMBER_OF_LOCK_RETRIES, + }, persistSession: persistSessionCookie(options), retrieveSession: retrieveSessionCookie, clearSession: deleteSessionCookie(options), @@ -57,11 +71,3 @@ function isValidSessionString(sessionString: string | undefined): sessionString (sessionString.indexOf(SESSION_ENTRY_SEPARATOR) !== -1 || SESSION_ENTRY_REGEXP.test(sessionString)) ) } - -/** - * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. - * This issue concerns only chromium browsers and enabling this on firefox increase cookie write failures. - */ -export function isCookieLockEnabled() { - return isChromium() -} diff --git a/packages/core/src/domain/session/sessionStorage.ts b/packages/core/src/domain/session/sessionStorage.ts index 8b21dc5be0..45e8ec04b6 100644 --- a/packages/core/src/domain/session/sessionStorage.ts +++ b/packages/core/src/domain/session/sessionStorage.ts @@ -1,3 +1,4 @@ +import type { CookieOptions } from '../../browser/cookie' import { isEmptyObject } from '../../tools/utils/objectUtils' export interface SessionState { @@ -9,7 +10,24 @@ export interface SessionState { [key: string]: string | undefined } +interface StorageAccessOptionsWithLock { + pollDelay: number + lockEnabled: true + lockRetryDelay: number + lockMaxTries: number +} + +interface StorageAccessOptionsWithoutLock { + pollDelay: number + lockEnabled: false +} + +type StorageAccessOptions = StorageAccessOptionsWithLock | StorageAccessOptionsWithoutLock + +export type StorageInitOptions = CookieOptions + export interface SessionStorage { + storageAccessOptions: StorageAccessOptions persistSession: (session: SessionState) => void retrieveSession: () => SessionState clearSession: () => void diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 6a2f8ae3bb..70d122b642 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -4,12 +4,7 @@ import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import { isChromium } from '../../tools/utils/browserDetection' import type { SessionStore } from './sessionStore' -import { - LOCK_RETRY_DELAY, - MAX_NUMBER_OF_LOCK_RETRIES, - processStorageOperations, - startSessionStore, -} from './sessionStore' +import { processStorageOperations, startSessionStore } from './sessionStore' import { SESSION_COOKIE_NAME, initCookieStorage, toSessionString } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import type { SessionState, SessionStorage } from './sessionStorage' @@ -554,7 +549,14 @@ describe('session store', () => { cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - clock.tick(MAX_NUMBER_OF_LOCK_RETRIES * LOCK_RETRY_DELAY) + const lockMaxTries = cookieStorage.storageAccessOptions.lockEnabled + ? cookieStorage.storageAccessOptions.lockMaxTries + : 0 + const lockRetryDelay = cookieStorage.storageAccessOptions.lockEnabled + ? cookieStorage.storageAccessOptions.lockRetryDelay + : 0 + + clock.tick(lockMaxTries * lockRetryDelay) expect(processSpy).not.toHaveBeenCalled() expect(afterSpy).not.toHaveBeenCalled() expect(cookie.setSpy).not.toHaveBeenCalled() diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 46982ad573..b2001c5e84 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -1,13 +1,11 @@ -import type { CookieOptions } from '../../browser/cookie' -import { COOKIE_ACCESS_DELAY } from '../../browser/cookie' import { clearInterval, setInterval, setTimeout } from '../../tools/timer' import { Observable } from '../../tools/observable' import { dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { isCookieLockEnabled, initCookieStorage } from './sessionCookieStore' -import type { SessionState, SessionStorage } from './sessionStorage' +import { initCookieStorage } from './sessionCookieStore' +import type { SessionState, SessionStorage, StorageInitOptions } from './sessionStorage' import { isSessionInExpiredState } from './sessionStorage' export interface SessionStore { @@ -27,7 +25,7 @@ export interface SessionStore { * - inactive, no session in store or session expired, waiting for a renew session */ export function startSessionStore( - options: CookieOptions, + options: StorageInitOptions, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionStore { @@ -35,9 +33,9 @@ export function startSessionStore( const expireObservable = new Observable() const sessionStorage = initCookieStorage(options) - const { clearSession, retrieveSession } = sessionStorage + const { clearSession, retrieveSession, storageAccessOptions } = sessionStorage - const watchSessionTimeoutId = setInterval(watchSession, COOKIE_ACCESS_DELAY) + const watchSessionTimeoutId = setInterval(watchSession, storageAccessOptions.pollDelay) let sessionCache: SessionState = retrieveActiveSession() function expandOrRenewSession() { @@ -144,7 +142,7 @@ export function startSessionStore( } return { - expandOrRenewSession: throttle(expandOrRenewSession, COOKIE_ACCESS_DELAY).throttled, + expandOrRenewSession: throttle(expandOrRenewSession, storageAccessOptions.pollDelay).throttled, expandSession, getSession: () => sessionCache, renewObservable, @@ -159,20 +157,16 @@ export function startSessionStore( } } -// arbitrary values -export const LOCK_RETRY_DELAY = 10 -export const MAX_NUMBER_OF_LOCK_RETRIES = 100 - type Operations = { - process: (cookieSession: SessionState) => SessionState | undefined - after?: (cookieSession: SessionState) => void + process: (sessionState: SessionState) => SessionState | undefined + after?: (sessionState: SessionState) => void } const bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined export function processStorageOperations(operations: Operations, sessionStorage: SessionStorage, numberOfRetries = 0) { - const { retrieveSession, persistSession, clearSession } = sessionStorage + const { retrieveSession, persistSession, clearSession, storageAccessOptions } = sessionStorage if (!ongoingOperations) { ongoingOperations = operations @@ -181,16 +175,16 @@ export function processStorageOperations(operations: Operations, sessionStorage: bufferedOperations.push(operations) return } - if (numberOfRetries >= MAX_NUMBER_OF_LOCK_RETRIES) { + if (storageAccessOptions.lockEnabled && numberOfRetries >= storageAccessOptions.lockMaxTries) { next(sessionStorage) return } let currentLock: string let currentSession = retrieveSession() - if (isCookieLockEnabled()) { + if (storageAccessOptions.lockEnabled) { // if someone has lock, retry later if (currentSession.lock) { - retryLater(operations, sessionStorage, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) return } // acquire lock @@ -200,16 +194,16 @@ export function processStorageOperations(operations: Operations, sessionStorage: // if lock is not acquired, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock) { - retryLater(operations, sessionStorage, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) return } } let processedSession = operations.process(currentSession) - if (isCookieLockEnabled()) { + if (storageAccessOptions.lockEnabled) { // if lock corrupted after process, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStorage, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) return } } @@ -221,14 +215,14 @@ export function processStorageOperations(operations: Operations, sessionStorage: persistSession(processedSession) } } - if (isCookieLockEnabled()) { + if (storageAccessOptions.lockEnabled) { // correctly handle lock around expiration would require to handle this case properly at several levels // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it if (!(processedSession && isSessionInExpiredState(processedSession))) { // if lock corrupted after persist, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStorage, numberOfRetries) + retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) return } delete currentSession.lock @@ -242,10 +236,15 @@ export function processStorageOperations(operations: Operations, sessionStorage: next(sessionStorage) } -function retryLater(operations: Operations, sessionStorage: SessionStorage, currentNumberOfRetries: number) { +function retryLater( + operations: Operations, + sessionStorage: SessionStorage, + currentNumberOfRetries: number, + retryDelay: number +) { setTimeout(() => { processStorageOperations(operations, sessionStorage, currentNumberOfRetries + 1) - }, LOCK_RETRY_DELAY) + }, retryDelay) } function next(sessionStorage: SessionStorage) { From 44d3ed7a8125b6486e8f4376e79daae3caecd458 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 15 May 2023 11:15:35 +0200 Subject: [PATCH 06/40] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20remaining=20refer?= =?UTF-8?q?ences=20to=20cookies=20from=20sessionStore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/domain/session/sessionStore.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index b2001c5e84..34bf3a9625 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -69,8 +69,8 @@ export function startSessionStore( /** * allows two behaviors: - * - if the session is active, synchronize the session cache without updating the session cookie - * - if the session is not active, clear the session cookie and expire the session cache + * - if the session is active, synchronize the session cache without updating the session storage + * - if the session is not active, clear the session storage and expire the session cache */ function watchSession() { processStorageOperations( @@ -231,7 +231,7 @@ export function processStorageOperations(operations: Operations, sessionStorage: } } // call after even if session is not persisted in order to perform operations on - // up-to-date cookie value, the value could have been modified by another tab + // up-to-date session state value => the value could have been modified by another tab operations.after?.(processedSession || currentSession) next(sessionStorage) } From 20b0d29a9fa9c4f187c135d25318a04201dbd52d Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 25 May 2023 17:51:35 +0200 Subject: [PATCH 07/40] =?UTF-8?q?=F0=9F=8E=A8=20Session=20components=20nam?= =?UTF-8?q?ing=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/oldCookiesMigration.ts | 4 +- .../domain/session/sessionCookieStore.spec.ts | 8 +- .../src/domain/session/sessionCookieStore.ts | 6 +- .../core/src/domain/session/sessionManager.ts | 4 +- .../src/domain/session/sessionStorage.spec.ts | 15 - .../core/src/domain/session/sessionStorage.ts | 38 -- .../src/domain/session/sessionStore.spec.ts | 600 +----------------- .../core/src/domain/session/sessionStore.ts | 270 +------- .../session/sessionStoreManager.spec.ts | 597 +++++++++++++++++ .../src/domain/session/sessionStoreManager.ts | 256 ++++++++ 10 files changed, 898 insertions(+), 900 deletions(-) delete mode 100644 packages/core/src/domain/session/sessionStorage.spec.ts delete mode 100644 packages/core/src/domain/session/sessionStorage.ts create mode 100644 packages/core/src/domain/session/sessionStoreManager.spec.ts create mode 100644 packages/core/src/domain/session/sessionStoreManager.ts diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index 02937b4129..fd1e0a256f 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,8 +1,8 @@ import type { CookieOptions } from '../../browser/cookie' import { getCookie } from '../../browser/cookie' import { dateNow } from '../../tools/utils/timeUtils' -import type { SessionState } from './sessionStorage' -import { isSessionInExpiredState } from './sessionStorage' +import type { SessionState } from './sessionStore' +import { isSessionInExpiredState } from './sessionStore' import { SESSION_COOKIE_NAME, deleteSessionCookie, persistSessionCookie } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index 5ca34e57d4..214c46693b 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -1,16 +1,16 @@ import type { CookieOptions } from '../../browser/cookie' import { setCookie, deleteCookie } from '../../browser/cookie' -import { SESSION_COOKIE_NAME, initCookieStorage } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' -import type { SessionState, SessionStorage } from './sessionStorage' +import type { SessionState, SessionStore } from './sessionStore' describe('session cookie store', () => { const sessionState: SessionState = { id: '123', created: '0' } const noOptions: CookieOptions = {} - let cookieStorage: SessionStorage + let cookieStorage: SessionStore beforeEach(() => { - cookieStorage = initCookieStorage(noOptions) + cookieStorage = initCookieStore(noOptions) }) afterEach(() => { diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index d887916aeb..57b6b549c7 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -3,7 +3,7 @@ import { COOKIE_ACCESS_DELAY, deleteCookie, getCookie, setCookie } from '../../b import { isChromium } from '../../tools/utils/browserDetection' import { objectEntries } from '../../tools/utils/polyfills' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import type { SessionState, SessionStorage } from './sessionStorage' +import type { SessionState, SessionStore } from './sessionStore' const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ const SESSION_ENTRY_SEPARATOR = '&' @@ -14,9 +14,9 @@ export const SESSION_COOKIE_NAME = '_dd_s' export const LOCK_RETRY_DELAY = 10 export const MAX_NUMBER_OF_LOCK_RETRIES = 100 -export function initCookieStorage(options: CookieOptions): SessionStorage { +export function initCookieStore(options: CookieOptions): SessionStore { return { - storageAccessOptions: { + storeAccessOptions: { pollDelay: COOKIE_ACCESS_DELAY, /** * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index f492f5ad39..724fe58c2c 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -7,7 +7,7 @@ import { relativeNow, clocksOrigin, ONE_MINUTE } from '../../tools/utils/timeUti import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/addEventListener' import { clearInterval, setInterval } from '../../tools/timer' import { tryOldCookiesMigration } from './oldCookiesMigration' -import { startSessionStore } from './sessionStore' +import { startSessionStoreManager } from './sessionStoreManager' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' export interface SessionManager { @@ -32,7 +32,7 @@ export function startSessionManager( computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionManager { tryOldCookiesMigration(options) - const sessionStore = startSessionStore(options, productKey, computeSessionState) + const sessionStore = startSessionStoreManager(options, productKey, computeSessionState) stopCallbacks.push(() => sessionStore.stop()) const sessionContextHistory = new ValueHistory>(SESSION_CONTEXT_TIMEOUT_DELAY) diff --git a/packages/core/src/domain/session/sessionStorage.spec.ts b/packages/core/src/domain/session/sessionStorage.spec.ts deleted file mode 100644 index e3b8de5dba..0000000000 --- a/packages/core/src/domain/session/sessionStorage.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { SessionState } from './sessionStorage' -import { isSessionInExpiredState } from './sessionStorage' - -describe('session storage utilities', () => { - const EXPIRED_SESSION: SessionState = {} - const LIVE_SESSION: SessionState = { created: '0', id: '123' } - - it('should correctly identify a session in expired state', () => { - expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) - }) - - it('should correctly identify a session in live state', () => { - expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) - }) -}) diff --git a/packages/core/src/domain/session/sessionStorage.ts b/packages/core/src/domain/session/sessionStorage.ts deleted file mode 100644 index 45e8ec04b6..0000000000 --- a/packages/core/src/domain/session/sessionStorage.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CookieOptions } from '../../browser/cookie' -import { isEmptyObject } from '../../tools/utils/objectUtils' - -export interface SessionState { - id?: string - created?: string - expire?: string - lock?: string - - [key: string]: string | undefined -} - -interface StorageAccessOptionsWithLock { - pollDelay: number - lockEnabled: true - lockRetryDelay: number - lockMaxTries: number -} - -interface StorageAccessOptionsWithoutLock { - pollDelay: number - lockEnabled: false -} - -type StorageAccessOptions = StorageAccessOptionsWithLock | StorageAccessOptionsWithoutLock - -export type StorageInitOptions = CookieOptions - -export interface SessionStorage { - storageAccessOptions: StorageAccessOptions - persistSession: (session: SessionState) => void - retrieveSession: () => SessionState - clearSession: () => void -} - -export function isSessionInExpiredState(session: SessionState) { - return isEmptyObject(session) -} diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 70d122b642..ed67c69cf0 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,597 +1,15 @@ -import type { Clock } from '../../../test' -import { stubCookie, mockClock } from '../../../test' -import type { CookieOptions } from '../../browser/cookie' -import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' -import { isChromium } from '../../tools/utils/browserDetection' -import type { SessionStore } from './sessionStore' -import { processStorageOperations, startSessionStore } from './sessionStore' -import { SESSION_COOKIE_NAME, initCookieStorage, toSessionString } from './sessionCookieStore' -import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import type { SessionState, SessionStorage } from './sessionStorage' +import type { SessionState } from './sessionStore' +import { isSessionInExpiredState } from './sessionStore' -const enum FakeTrackingType { - TRACKED = 'tracked', - NOT_TRACKED = 'not-tracked', -} +describe('session storage utilities', () => { + const EXPIRED_SESSION: SessionState = {} + const LIVE_SESSION: SessionState = { created: '0', id: '123' } -const DURATION = 123456 -const PRODUCT_KEY = 'product' -const FIRST_ID = 'first' -const SECOND_ID = 'second' -const COOKIE_OPTIONS: CookieOptions = {} - -function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { - setCookie( - SESSION_COOKIE_NAME, - `${id ? `id=${id}&` : ''}${PRODUCT_KEY}=${trackingType}&created=${Date.now()}&expire=${ - expire || Date.now() + SESSION_EXPIRATION_DELAY - }`, - DURATION - ) -} - -function expectTrackedSessionToBeInStore(id?: string) { - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(new RegExp(`id=${id ? id : '[a-f0-9-]+'}`)) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.TRACKED}`) -} - -function expectNotTrackedSessionToBeInStore() { - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`) -} - -function getStoreExpiration() { - return /expire=(\d+)/.exec(getCookie(SESSION_COOKIE_NAME)!)?.[1] -} - -function resetSessionInStore() { - setCookie(SESSION_COOKIE_NAME, '', DURATION) -} - -describe('session store', () => { - describe('session lifecyle mechanism', () => { - let expireSpy: () => void - let renewSpy: () => void - let sessionStore: SessionStore - let clock: Clock - - function setupSessionStore( - computeSessionState: (rawTrackingType?: string) => { - trackingType: FakeTrackingType - isTracked: boolean - } = () => ({ - isTracked: true, - trackingType: FakeTrackingType.TRACKED, - }) - ) { - sessionStore = startSessionStore(COOKIE_OPTIONS, PRODUCT_KEY, computeSessionState) - sessionStore.expireObservable.subscribe(expireSpy) - sessionStore.renewObservable.subscribe(renewSpy) - } - - beforeEach(() => { - expireSpy = jasmine.createSpy('expire session') - renewSpy = jasmine.createSpy('renew session') - clock = mockClock() - }) - - afterEach(() => { - resetSessionInStore() - clock.cleanup() - sessionStore.stop() - }) - - describe('expand or renew session', () => { - it( - 'when session not in cache, session not in store and new session tracked, ' + - 'should create new session and trigger renew session ', - () => { - setupSessionStore() - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBeDefined() - expectTrackedSessionToBeInStore() - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) - - it( - 'when session not in cache, session not in store and new session not tracked, ' + - 'should store not tracked session', - () => { - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it('when session not in cache and session in store, should expand session and trigger renew session', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expectTrackedSessionToBeInStore(FIRST_ID) - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - }) - - it( - 'when session in cache, session not in store and new session tracked, ' + - 'should expire session, create a new one and trigger renew session', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() - - sessionStore.expandOrRenewSession() - - const sessionId = sessionStore.getSession().id - expect(sessionId).toBeDefined() - expect(sessionId).not.toBe(FIRST_ID) - expectTrackedSessionToBeInStore(sessionId) - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) - - it( - 'when session in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - resetSessionInStore() - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it( - 'when session not tracked in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', - () => { - setSessionInStore(FakeTrackingType.NOT_TRACKED) - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - resetSessionInStore() - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it('when session in cache is same session than in store, should expand session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - - clock.tick(10) - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) - expectTrackedSessionToBeInStore(FIRST_ID) - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - }) - - it( - 'when session in cache is different session than in store and store session is tracked, ' + - 'should expire session, expand store session and trigger renew', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBe(SECOND_ID) - expectTrackedSessionToBeInStore(SECOND_ID) - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) - - 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', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore((rawTrackingType) => ({ - isTracked: rawTrackingType === FakeTrackingType.TRACKED, - trackingType: rawTrackingType as FakeTrackingType, - })) - setSessionInStore(FakeTrackingType.NOT_TRACKED, '') - - sessionStore.expandOrRenewSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - }) - - describe('expand session', () => { - it('when session not in cache and session not in store, should do nothing', () => { - setupSessionStore() - - sessionStore.expandSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session not in cache and session in store, should do nothing', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - sessionStore.expandSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session in cache and session not in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() - - sessionStore.expandSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - - it('when session in cache is same session than in store, should expand session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - - clock.tick(10) - sessionStore.expandSession() - - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session in cache is different session than in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - sessionStore.expandSession() - - expect(sessionStore.getSession().id).toBeUndefined() - expectTrackedSessionToBeInStore(SECOND_ID) - expect(expireSpy).toHaveBeenCalled() - }) - }) - - describe('regular watch', () => { - it('when session not in cache and session not in store, should do nothing', () => { - setupSessionStore() - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session not in cache and session in store, should do nothing', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session in cache and session not in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - - it('when session in cache is same session than in store, should synchronize session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session id in cache is different than session id in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - - it('when session type in cache is different than session type in store, should expire session', () => { - setSessionInStore(FakeTrackingType.NOT_TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStore.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - }) + it('should correctly identify a session in expired state', () => { + expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) }) - describe('process operations mechanism', () => { - const COOKIE_OPTIONS = {} - let initialSession: SessionState - let otherSession: SessionState - let processSpy: jasmine.Spy - let afterSpy: jasmine.Spy - let cookie: ReturnType - let cookieStorage: SessionStorage - - beforeEach(() => { - cookieStorage = initCookieStorage(COOKIE_OPTIONS) - initialSession = { id: '123', created: '0' } - otherSession = { id: '456', created: '100' } - processSpy = jasmine.createSpy('process') - afterSpy = jasmine.createSpy('after') - cookie = stubCookie() - }) - - describe('with cookie-lock disabled', () => { - beforeEach(() => { - isChromium() && pending('cookie-lock only disabled on non chromium browsers') - }) - - it('should persist session when process returns a value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({ ...otherSession }) - - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should clear session when process return an empty value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({}) - - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = {} - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should not persist session when process return undefined', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue(undefined) - - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - expect(cookieStorage.retrieveSession()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) - }) - - describe('with cookie-lock enabled', () => { - beforeEach(() => { - !isChromium() && pending('cookie-lock only enabled on chromium browsers') - }) - - it('should persist session when process return a value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) - - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should clear session when process return an empty value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({}) - - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = {} - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should not persist session when process return undefined', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue(undefined) - - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - expect(cookieStorage.retrieveSession()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) - - type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } - - function lockScenario({ - onInitialLockCheck, - onAcquiredLockCheck, - onPostProcessLockCheck, - onPostPersistLockCheck, - }: { - onInitialLockCheck?: OnLockCheck - onAcquiredLockCheck?: OnLockCheck - onPostProcessLockCheck?: OnLockCheck - onPostPersistLockCheck?: OnLockCheck - }) { - const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] - cookie.getSpy.and.callFake(() => { - const currentOnLockCheck = onLockChecks.shift() - if (!currentOnLockCheck) { - return cookie.currentValue() - } - const { currentState, retryState } = currentOnLockCheck() - cookie.setCurrentValue(buildSessionString(retryState)) - return buildSessionString(currentState) - }) - } - - function buildSessionString(currentState: SessionState) { - return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` - } - - ;[ - { - description: 'should wait for lock to be free', - lockConflict: 'onInitialLockCheck', - }, - { - description: 'should retry if lock was acquired before process', - lockConflict: 'onAcquiredLockCheck', - }, - { - description: 'should retry if lock was acquired after process', - lockConflict: 'onPostProcessLockCheck', - }, - { - description: 'should retry if lock was acquired after persist', - lockConflict: 'onPostPersistLockCheck', - }, - ].forEach(({ description, lockConflict }) => { - it(description, (done) => { - lockScenario({ - [lockConflict]: () => ({ - currentState: { ...initialSession, lock: 'locked' }, - retryState: { ...initialSession, other: 'other' }, - }), - }) - initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) - cookieStorage.persistSession(initialSession) - processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) - - processStorageOperations( - { - process: processSpy, - after: (afterSession) => { - // session with 'other' value on process - expect(processSpy).toHaveBeenCalledWith({ - ...initialSession, - other: 'other', - lock: jasmine.any(String), - expire: jasmine.any(String), - }) - - // end state with session 'other' and 'processed' value - const expectedSession = { - ...initialSession, - other: 'other', - processed: 'processed', - expire: jasmine.any(String), - } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSession).toEqual(expectedSession) - done() - }, - }, - cookieStorage - ) - }) - }) - - it('should abort after a max number of retry', () => { - const clock = mockClock() - - cookieStorage.persistSession(initialSession) - cookie.setSpy.calls.reset() - - cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) - processStorageOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - const lockMaxTries = cookieStorage.storageAccessOptions.lockEnabled - ? cookieStorage.storageAccessOptions.lockMaxTries - : 0 - const lockRetryDelay = cookieStorage.storageAccessOptions.lockEnabled - ? cookieStorage.storageAccessOptions.lockRetryDelay - : 0 - - clock.tick(lockMaxTries * lockRetryDelay) - expect(processSpy).not.toHaveBeenCalled() - expect(afterSpy).not.toHaveBeenCalled() - expect(cookie.setSpy).not.toHaveBeenCalled() - - clock.cleanup() - }) - - it('should execute cookie accesses in order', (done) => { - lockScenario({ - onInitialLockCheck: () => ({ - currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later - retryState: initialSession, - }), - }) - cookieStorage.persistSession(initialSession) - - processStorageOperations( - { - process: (session) => ({ ...session, value: 'foo' }), - after: afterSpy, - }, - cookieStorage - ) - processStorageOperations( - { - process: (session) => ({ ...session, value: `${session.value || ''}bar` }), - after: (session) => { - expect(session.value).toBe('foobar') - expect(afterSpy).toHaveBeenCalled() - done() - }, - }, - cookieStorage - ) - }) - }) + it('should correctly identify a session in live state', () => { + expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) }) }) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 34bf3a9625..18049f846b 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -1,256 +1,36 @@ -import { clearInterval, setInterval, setTimeout } from '../../tools/timer' -import { Observable } from '../../tools/observable' -import { dateNow } from '../../tools/utils/timeUtils' -import { throttle } from '../../tools/utils/functionUtils' -import { generateUUID } from '../../tools/utils/stringUtils' -import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { initCookieStorage } from './sessionCookieStore' -import type { SessionState, SessionStorage, StorageInitOptions } from './sessionStorage' -import { isSessionInExpiredState } from './sessionStorage' +import type { CookieOptions } from '../../browser/cookie' +import { isEmptyObject } from '../../tools/utils/objectUtils' -export interface SessionStore { - expandOrRenewSession: () => void - expandSession: () => void - getSession: () => SessionState - renewObservable: Observable - expireObservable: Observable - expire: () => void - stop: () => void -} - -/** - * Different session concepts: - * - tracked, the session has an id and is updated along the user navigation - * - not tracked, the session does not have an id but it is updated along the user navigation - * - inactive, no session in store or session expired, waiting for a renew session - */ -export function startSessionStore( - options: StorageInitOptions, - productKey: string, - computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } -): SessionStore { - const renewObservable = new Observable() - const expireObservable = new Observable() - - const sessionStorage = initCookieStorage(options) - const { clearSession, retrieveSession, storageAccessOptions } = sessionStorage - - const watchSessionTimeoutId = setInterval(watchSession, storageAccessOptions.pollDelay) - let sessionCache: SessionState = retrieveActiveSession() - - function expandOrRenewSession() { - let isTracked: boolean - processStorageOperations( - { - process: (sessionState) => { - const synchronizedSession = synchronizeSession(sessionState) - isTracked = expandOrRenewCookie(synchronizedSession) - return synchronizedSession - }, - after: (sessionState) => { - if (isTracked && !hasSessionInCache()) { - renewSessionInCache(sessionState) - } - sessionCache = sessionState - }, - }, - sessionStorage - ) - } - - function expandSession() { - processStorageOperations( - { - process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), - }, - sessionStorage - ) - } - - /** - * allows two behaviors: - * - if the session is active, synchronize the session cache without updating the session storage - * - if the session is not active, clear the session storage and expire the session cache - */ - function watchSession() { - processStorageOperations( - { - process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), - after: synchronizeSession, - }, - sessionStorage - ) - } - - function synchronizeSession(sessionState: SessionState) { - if (!isActiveSession(sessionState)) { - sessionState = {} - } - if (hasSessionInCache()) { - if (isSessionInCacheOutdated(sessionState)) { - expireSessionInCache() - } else { - sessionCache = sessionState - } - } - return sessionState - } - - function expandOrRenewCookie(sessionState: SessionState) { - const { trackingType, isTracked } = computeSessionState(sessionState[productKey]) - sessionState[productKey] = trackingType - if (isTracked && !sessionState.id) { - sessionState.id = generateUUID() - sessionState.created = String(dateNow()) - } - return isTracked - } +export interface SessionState { + id?: string + created?: string + expire?: string + lock?: string - function hasSessionInCache() { - return sessionCache[productKey] !== undefined - } - - function isSessionInCacheOutdated(sessionState: SessionState) { - return sessionCache.id !== sessionState.id || sessionCache[productKey] !== sessionState[productKey] - } - - function expireSessionInCache() { - sessionCache = {} - expireObservable.notify() - } - - function renewSessionInCache(sessionState: SessionState) { - sessionCache = sessionState - renewObservable.notify() - } - - function retrieveActiveSession(): SessionState { - const session = retrieveSession() - if (isActiveSession(session)) { - return session - } - return {} - } - - function isActiveSession(sessionDate: SessionState) { - // created and expire can be undefined for versions which was not storing them - // these checks could be removed when older versions will not be available/live anymore - return ( - (sessionDate.created === undefined || dateNow() - Number(sessionDate.created) < SESSION_TIME_OUT_DELAY) && - (sessionDate.expire === undefined || dateNow() < Number(sessionDate.expire)) - ) - } - - return { - expandOrRenewSession: throttle(expandOrRenewSession, storageAccessOptions.pollDelay).throttled, - expandSession, - getSession: () => sessionCache, - renewObservable, - expireObservable, - expire: () => { - clearSession() - synchronizeSession({}) - }, - stop: () => { - clearInterval(watchSessionTimeoutId) - }, - } + [key: string]: string | undefined } -type Operations = { - process: (sessionState: SessionState) => SessionState | undefined - after?: (sessionState: SessionState) => void +type StoreAccessOptionsWithLock = { + pollDelay: number + lockEnabled: true + lockRetryDelay: number + lockMaxTries: number } -const bufferedOperations: Operations[] = [] -let ongoingOperations: Operations | undefined - -export function processStorageOperations(operations: Operations, sessionStorage: SessionStorage, numberOfRetries = 0) { - const { retrieveSession, persistSession, clearSession, storageAccessOptions } = sessionStorage - - if (!ongoingOperations) { - ongoingOperations = operations - } - if (operations !== ongoingOperations) { - bufferedOperations.push(operations) - return - } - if (storageAccessOptions.lockEnabled && numberOfRetries >= storageAccessOptions.lockMaxTries) { - next(sessionStorage) - return - } - let currentLock: string - let currentSession = retrieveSession() - if (storageAccessOptions.lockEnabled) { - // if someone has lock, retry later - if (currentSession.lock) { - retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) - return - } - // acquire lock - currentLock = generateUUID() - currentSession.lock = currentLock - persistSession(currentSession) - // if lock is not acquired, retry later - currentSession = retrieveSession() - if (currentSession.lock !== currentLock) { - retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) - return - } - } - let processedSession = operations.process(currentSession) - if (storageAccessOptions.lockEnabled) { - // if lock corrupted after process, retry later - currentSession = retrieveSession() - if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) - return - } - } - if (processedSession) { - if (isSessionInExpiredState(processedSession)) { - clearSession() - } else { - processedSession.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) - persistSession(processedSession) - } - } - if (storageAccessOptions.lockEnabled) { - // correctly handle lock around expiration would require to handle this case properly at several levels - // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it - if (!(processedSession && isSessionInExpiredState(processedSession))) { - // if lock corrupted after persist, retry later - currentSession = retrieveSession() - if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStorage, numberOfRetries, storageAccessOptions.lockRetryDelay) - return - } - delete currentSession.lock - persistSession(currentSession) - processedSession = currentSession - } - } - // call after even if session is not persisted in order to perform operations on - // up-to-date session state value => the value could have been modified by another tab - operations.after?.(processedSession || currentSession) - next(sessionStorage) +type StoreAccessOptionsWithoutLock = { + pollDelay: number + lockEnabled: false } -function retryLater( - operations: Operations, - sessionStorage: SessionStorage, - currentNumberOfRetries: number, - retryDelay: number -) { - setTimeout(() => { - processStorageOperations(operations, sessionStorage, currentNumberOfRetries + 1) - }, retryDelay) +export type StoreInitOptions = CookieOptions + +export interface SessionStore { + storeAccessOptions: StoreAccessOptionsWithLock | StoreAccessOptionsWithoutLock + persistSession: (session: SessionState) => void + retrieveSession: () => SessionState + clearSession: () => void } -function next(sessionStorage: SessionStorage) { - ongoingOperations = undefined - const nextOperations = bufferedOperations.shift() - if (nextOperations) { - processStorageOperations(nextOperations, sessionStorage) - } +export function isSessionInExpiredState(session: SessionState) { + return isEmptyObject(session) } diff --git a/packages/core/src/domain/session/sessionStoreManager.spec.ts b/packages/core/src/domain/session/sessionStoreManager.spec.ts new file mode 100644 index 0000000000..3751ea21fb --- /dev/null +++ b/packages/core/src/domain/session/sessionStoreManager.spec.ts @@ -0,0 +1,597 @@ +import type { Clock } from '../../../test' +import { stubCookie, mockClock } from '../../../test' +import type { CookieOptions } from '../../browser/cookie' +import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' +import { isChromium } from '../../tools/utils/browserDetection' +import type { SessionStoreManager } from './sessionStoreManager' +import { processSessionStoreOperations, startSessionStoreManager } from './sessionStoreManager' +import { SESSION_COOKIE_NAME, initCookieStore, toSessionString } from './sessionCookieStore' +import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import type { SessionState, SessionStore } from './sessionStore' + +const enum FakeTrackingType { + TRACKED = 'tracked', + NOT_TRACKED = 'not-tracked', +} + +const DURATION = 123456 +const PRODUCT_KEY = 'product' +const FIRST_ID = 'first' +const SECOND_ID = 'second' +const COOKIE_OPTIONS: CookieOptions = {} + +function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { + setCookie( + SESSION_COOKIE_NAME, + `${id ? `id=${id}&` : ''}${PRODUCT_KEY}=${trackingType}&created=${Date.now()}&expire=${ + expire || Date.now() + SESSION_EXPIRATION_DELAY + }`, + DURATION + ) +} + +function expectTrackedSessionToBeInStore(id?: string) { + expect(getCookie(SESSION_COOKIE_NAME)).toMatch(new RegExp(`id=${id ? id : '[a-f0-9-]+'}`)) + expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.TRACKED}`) +} + +function expectNotTrackedSessionToBeInStore() { + expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') + expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`) +} + +function getStoreExpiration() { + return /expire=(\d+)/.exec(getCookie(SESSION_COOKIE_NAME)!)?.[1] +} + +function resetSessionInStore() { + setCookie(SESSION_COOKIE_NAME, '', DURATION) +} + +describe('session store', () => { + describe('session lifecyle mechanism', () => { + let expireSpy: () => void + let renewSpy: () => void + let sessionStore: SessionStoreManager + let clock: Clock + + function setupSessionStore( + computeSessionState: (rawTrackingType?: string) => { + trackingType: FakeTrackingType + isTracked: boolean + } = () => ({ + isTracked: true, + trackingType: FakeTrackingType.TRACKED, + }) + ) { + sessionStore = startSessionStoreManager(COOKIE_OPTIONS, PRODUCT_KEY, computeSessionState) + sessionStore.expireObservable.subscribe(expireSpy) + sessionStore.renewObservable.subscribe(renewSpy) + } + + beforeEach(() => { + expireSpy = jasmine.createSpy('expire session') + renewSpy = jasmine.createSpy('renew session') + clock = mockClock() + }) + + afterEach(() => { + resetSessionInStore() + clock.cleanup() + sessionStore.stop() + }) + + describe('expand or renew session', () => { + it( + 'when session not in cache, session not in store and new session tracked, ' + + 'should create new session and trigger renew session ', + () => { + setupSessionStore() + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeDefined() + expectTrackedSessionToBeInStore() + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + it( + 'when session not in cache, session not in store and new session not tracked, ' + + 'should store not tracked session', + () => { + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it('when session not in cache and session in store, should expand session and trigger renew session', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expectTrackedSessionToBeInStore(FIRST_ID) + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + }) + + it( + 'when session in cache, session not in store and new session tracked, ' + + 'should expire session, create a new one and trigger renew session', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + sessionStore.expandOrRenewSession() + + const sessionId = sessionStore.getSession().id + expect(sessionId).toBeDefined() + expect(sessionId).not.toBe(FIRST_ID) + expectTrackedSessionToBeInStore(sessionId) + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + it( + 'when session in cache, session not in store and new session not tracked, ' + + 'should expire session and store not tracked session', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + resetSessionInStore() + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it( + 'when session not tracked in cache, session not in store and new session not tracked, ' + + 'should expire session and store not tracked session', + () => { + setSessionInStore(FakeTrackingType.NOT_TRACKED) + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + resetSessionInStore() + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it('when session in cache is same session than in store, should expand session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + + clock.tick(10) + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expectTrackedSessionToBeInStore(FIRST_ID) + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + }) + + it( + 'when session in cache is different session than in store and store session is tracked, ' + + 'should expire session, expand store session and trigger renew', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBe(SECOND_ID) + expectTrackedSessionToBeInStore(SECOND_ID) + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + 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', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore((rawTrackingType) => ({ + isTracked: rawTrackingType === FakeTrackingType.TRACKED, + trackingType: rawTrackingType as FakeTrackingType, + })) + setSessionInStore(FakeTrackingType.NOT_TRACKED, '') + + sessionStore.expandOrRenewSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + }) + + describe('expand session', () => { + it('when session not in cache and session not in store, should do nothing', () => { + setupSessionStore() + + sessionStore.expandSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session not in cache and session in store, should do nothing', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + sessionStore.expandSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache and session not in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + sessionStore.expandSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + + it('when session in cache is same session than in store, should expand session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + + clock.tick(10) + sessionStore.expandSession() + + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache is different session than in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + sessionStore.expandSession() + + expect(sessionStore.getSession().id).toBeUndefined() + expectTrackedSessionToBeInStore(SECOND_ID) + expect(expireSpy).toHaveBeenCalled() + }) + }) + + describe('regular watch', () => { + it('when session not in cache and session not in store, should do nothing', () => { + setupSessionStore() + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session not in cache and session in store, should do nothing', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache and session not in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + + it('when session in cache is same session than in store, should synchronize session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session id in cache is different than session id in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + + it('when session type in cache is different than session type in store, should expire session', () => { + setSessionInStore(FakeTrackingType.NOT_TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStore.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + }) + }) + + describe('process operations mechanism', () => { + const COOKIE_OPTIONS = {} + let initialSession: SessionState + let otherSession: SessionState + let processSpy: jasmine.Spy + let afterSpy: jasmine.Spy + let cookie: ReturnType + let cookieStorage: SessionStore + + beforeEach(() => { + cookieStorage = initCookieStore(COOKIE_OPTIONS) + initialSession = { id: '123', created: '0' } + otherSession = { id: '456', created: '100' } + processSpy = jasmine.createSpy('process') + afterSpy = jasmine.createSpy('after') + cookie = stubCookie() + }) + + describe('with cookie-lock disabled', () => { + beforeEach(() => { + isChromium() && pending('cookie-lock only disabled on non chromium browsers') + }) + + it('should persist session when process returns a value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({ ...otherSession }) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should clear session when process return an empty value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({}) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = {} + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should not persist session when process return undefined', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue(undefined) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + expect(cookieStorage.retrieveSession()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) + }) + + describe('with cookie-lock enabled', () => { + beforeEach(() => { + !isChromium() && pending('cookie-lock only enabled on chromium browsers') + }) + + it('should persist session when process return a value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should clear session when process return an empty value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({}) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = {} + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should not persist session when process return undefined', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue(undefined) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + expect(cookieStorage.retrieveSession()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) + + type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } + + function lockScenario({ + onInitialLockCheck, + onAcquiredLockCheck, + onPostProcessLockCheck, + onPostPersistLockCheck, + }: { + onInitialLockCheck?: OnLockCheck + onAcquiredLockCheck?: OnLockCheck + onPostProcessLockCheck?: OnLockCheck + onPostPersistLockCheck?: OnLockCheck + }) { + const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] + cookie.getSpy.and.callFake(() => { + const currentOnLockCheck = onLockChecks.shift() + if (!currentOnLockCheck) { + return cookie.currentValue() + } + const { currentState, retryState } = currentOnLockCheck() + cookie.setCurrentValue(buildSessionString(retryState)) + return buildSessionString(currentState) + }) + } + + function buildSessionString(currentState: SessionState) { + return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` + } + + ;[ + { + description: 'should wait for lock to be free', + lockConflict: 'onInitialLockCheck', + }, + { + description: 'should retry if lock was acquired before process', + lockConflict: 'onAcquiredLockCheck', + }, + { + description: 'should retry if lock was acquired after process', + lockConflict: 'onPostProcessLockCheck', + }, + { + description: 'should retry if lock was acquired after persist', + lockConflict: 'onPostPersistLockCheck', + }, + ].forEach(({ description, lockConflict }) => { + it(description, (done) => { + lockScenario({ + [lockConflict]: () => ({ + currentState: { ...initialSession, lock: 'locked' }, + retryState: { ...initialSession, other: 'other' }, + }), + }) + initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) + cookieStorage.persistSession(initialSession) + processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) + + processSessionStoreOperations( + { + process: processSpy, + after: (afterSession) => { + // session with 'other' value on process + expect(processSpy).toHaveBeenCalledWith({ + ...initialSession, + other: 'other', + lock: jasmine.any(String), + expire: jasmine.any(String), + }) + + // end state with session 'other' and 'processed' value + const expectedSession = { + ...initialSession, + other: 'other', + processed: 'processed', + expire: jasmine.any(String), + } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSession).toEqual(expectedSession) + done() + }, + }, + cookieStorage + ) + }) + }) + + it('should abort after a max number of retry', () => { + const clock = mockClock() + + cookieStorage.persistSession(initialSession) + cookie.setSpy.calls.reset() + + cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + const lockMaxTries = cookieStorage.storeAccessOptions.lockEnabled + ? cookieStorage.storeAccessOptions.lockMaxTries + : 0 + const lockRetryDelay = cookieStorage.storeAccessOptions.lockEnabled + ? cookieStorage.storeAccessOptions.lockRetryDelay + : 0 + + clock.tick(lockMaxTries * lockRetryDelay) + expect(processSpy).not.toHaveBeenCalled() + expect(afterSpy).not.toHaveBeenCalled() + expect(cookie.setSpy).not.toHaveBeenCalled() + + clock.cleanup() + }) + + it('should execute cookie accesses in order', (done) => { + lockScenario({ + onInitialLockCheck: () => ({ + currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later + retryState: initialSession, + }), + }) + cookieStorage.persistSession(initialSession) + + processSessionStoreOperations( + { + process: (session) => ({ ...session, value: 'foo' }), + after: afterSpy, + }, + cookieStorage + ) + processSessionStoreOperations( + { + process: (session) => ({ ...session, value: `${session.value || ''}bar` }), + after: (session) => { + expect(session.value).toBe('foobar') + expect(afterSpy).toHaveBeenCalled() + done() + }, + }, + cookieStorage + ) + }) + }) + }) +}) diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts new file mode 100644 index 0000000000..d410317e3e --- /dev/null +++ b/packages/core/src/domain/session/sessionStoreManager.ts @@ -0,0 +1,256 @@ +import { clearInterval, setInterval, setTimeout } from '../../tools/timer' +import { Observable } from '../../tools/observable' +import { dateNow } from '../../tools/utils/timeUtils' +import { throttle } from '../../tools/utils/functionUtils' +import { generateUUID } from '../../tools/utils/stringUtils' +import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { initCookieStore } from './sessionCookieStore' +import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' +import { isSessionInExpiredState } from './sessionStore' + +export interface SessionStoreManager { + expandOrRenewSession: () => void + expandSession: () => void + getSession: () => SessionState + renewObservable: Observable + expireObservable: Observable + expire: () => void + stop: () => void +} + +/** + * Different session concepts: + * - tracked, the session has an id and is updated along the user navigation + * - not tracked, the session does not have an id but it is updated along the user navigation + * - inactive, no session in store or session expired, waiting for a renew session + */ +export function startSessionStoreManager( + options: StoreInitOptions, + productKey: string, + computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } +): SessionStoreManager { + const renewObservable = new Observable() + const expireObservable = new Observable() + + const sessionStore = initCookieStore(options) + const { clearSession, retrieveSession, storeAccessOptions } = sessionStore + + const watchSessionTimeoutId = setInterval(watchSession, storeAccessOptions.pollDelay) + let sessionCache: SessionState = retrieveActiveSession() + + function expandOrRenewSession() { + let isTracked: boolean + processSessionStoreOperations( + { + process: (sessionState) => { + const synchronizedSession = synchronizeSession(sessionState) + isTracked = expandOrRenewCookie(synchronizedSession) + return synchronizedSession + }, + after: (sessionState) => { + if (isTracked && !hasSessionInCache()) { + renewSessionInCache(sessionState) + } + sessionCache = sessionState + }, + }, + sessionStore + ) + } + + function expandSession() { + processSessionStoreOperations( + { + process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), + }, + sessionStore + ) + } + + /** + * allows two behaviors: + * - if the session is active, synchronize the session cache without updating the session store + * - if the session is not active, clear the session store and expire the session cache + */ + function watchSession() { + processSessionStoreOperations( + { + process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), + after: synchronizeSession, + }, + sessionStore + ) + } + + function synchronizeSession(sessionState: SessionState) { + if (!isActiveSession(sessionState)) { + sessionState = {} + } + if (hasSessionInCache()) { + if (isSessionInCacheOutdated(sessionState)) { + expireSessionInCache() + } else { + sessionCache = sessionState + } + } + return sessionState + } + + function expandOrRenewCookie(sessionState: SessionState) { + const { trackingType, isTracked } = computeSessionState(sessionState[productKey]) + sessionState[productKey] = trackingType + if (isTracked && !sessionState.id) { + sessionState.id = generateUUID() + sessionState.created = String(dateNow()) + } + return isTracked + } + + function hasSessionInCache() { + return sessionCache[productKey] !== undefined + } + + function isSessionInCacheOutdated(sessionState: SessionState) { + return sessionCache.id !== sessionState.id || sessionCache[productKey] !== sessionState[productKey] + } + + function expireSessionInCache() { + sessionCache = {} + expireObservable.notify() + } + + function renewSessionInCache(sessionState: SessionState) { + sessionCache = sessionState + renewObservable.notify() + } + + function retrieveActiveSession(): SessionState { + const session = retrieveSession() + if (isActiveSession(session)) { + return session + } + return {} + } + + function isActiveSession(sessionDate: SessionState) { + // created and expire can be undefined for versions which was not storing them + // these checks could be removed when older versions will not be available/live anymore + return ( + (sessionDate.created === undefined || dateNow() - Number(sessionDate.created) < SESSION_TIME_OUT_DELAY) && + (sessionDate.expire === undefined || dateNow() < Number(sessionDate.expire)) + ) + } + + return { + expandOrRenewSession: throttle(expandOrRenewSession, storeAccessOptions.pollDelay).throttled, + expandSession, + getSession: () => sessionCache, + renewObservable, + expireObservable, + expire: () => { + clearSession() + synchronizeSession({}) + }, + stop: () => { + clearInterval(watchSessionTimeoutId) + }, + } +} + +type Operations = { + process: (sessionState: SessionState) => SessionState | undefined + after?: (sessionState: SessionState) => void +} + +const bufferedOperations: Operations[] = [] +let ongoingOperations: Operations | undefined + +export function processSessionStoreOperations(operations: Operations, sessionStore: SessionStore, numberOfRetries = 0) { + const { retrieveSession, persistSession, clearSession, storeAccessOptions } = sessionStore + + if (!ongoingOperations) { + ongoingOperations = operations + } + if (operations !== ongoingOperations) { + bufferedOperations.push(operations) + return + } + if (storeAccessOptions.lockEnabled && numberOfRetries >= storeAccessOptions.lockMaxTries) { + next(sessionStore) + return + } + let currentLock: string + let currentSession = retrieveSession() + if (storeAccessOptions.lockEnabled) { + // if someone has lock, retry later + if (currentSession.lock) { + retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + return + } + // acquire lock + currentLock = generateUUID() + currentSession.lock = currentLock + persistSession(currentSession) + // if lock is not acquired, retry later + currentSession = retrieveSession() + if (currentSession.lock !== currentLock) { + retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + return + } + } + let processedSession = operations.process(currentSession) + if (storeAccessOptions.lockEnabled) { + // if lock corrupted after process, retry later + currentSession = retrieveSession() + if (currentSession.lock !== currentLock!) { + retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + return + } + } + if (processedSession) { + if (isSessionInExpiredState(processedSession)) { + clearSession() + } else { + processedSession.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) + persistSession(processedSession) + } + } + if (storeAccessOptions.lockEnabled) { + // correctly handle lock around expiration would require to handle this case properly at several levels + // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it + if (!(processedSession && isSessionInExpiredState(processedSession))) { + // if lock corrupted after persist, retry later + currentSession = retrieveSession() + if (currentSession.lock !== currentLock!) { + retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + return + } + delete currentSession.lock + persistSession(currentSession) + processedSession = currentSession + } + } + // call after even if session is not persisted in order to perform operations on + // up-to-date session state value => the value could have been modified by another tab + operations.after?.(processedSession || currentSession) + next(sessionStore) +} + +function retryLater( + operations: Operations, + sessionStore: SessionStore, + currentNumberOfRetries: number, + retryDelay: number +) { + setTimeout(() => { + processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1) + }, retryDelay) +} + +function next(sessionStore: SessionStore) { + ongoingOperations = undefined + const nextOperations = bufferedOperations.shift() + if (nextOperations) { + processSessionStoreOperations(nextOperations, sessionStore) + } +} From 17920d8b005f902c15f967e2f556c3827456c0e9 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 1 Jun 2023 09:11:21 +0200 Subject: [PATCH 08/40] =?UTF-8?q?=F0=9F=91=8C=20Removed=20StoreAccessOptio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/sessionCookieStore.ts | 17 +------ .../core/src/domain/session/sessionStore.ts | 13 ----- .../session/sessionStoreManager.spec.ts | 16 ++++--- .../src/domain/session/sessionStoreManager.ts | 48 +++++++++++-------- 4 files changed, 38 insertions(+), 56 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index 57b6b549c7..a22b1fc2f2 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,6 +1,5 @@ import type { CookieOptions } from '../../browser/cookie' -import { COOKIE_ACCESS_DELAY, deleteCookie, getCookie, setCookie } from '../../browser/cookie' -import { isChromium } from '../../tools/utils/browserDetection' +import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' import { objectEntries } from '../../tools/utils/polyfills' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' @@ -10,22 +9,8 @@ const SESSION_ENTRY_SEPARATOR = '&' export const SESSION_COOKIE_NAME = '_dd_s' -// Arbitrary values -export const LOCK_RETRY_DELAY = 10 -export const MAX_NUMBER_OF_LOCK_RETRIES = 100 - export function initCookieStore(options: CookieOptions): SessionStore { return { - storeAccessOptions: { - pollDelay: COOKIE_ACCESS_DELAY, - /** - * Cookie lock strategy allows mitigating issues due to concurrent access to cookie. - * This issue concerns only chromium browsers and enabling this on firefox increase cookie write failures. - */ - lockEnabled: isChromium(), - lockRetryDelay: LOCK_RETRY_DELAY, - lockMaxTries: MAX_NUMBER_OF_LOCK_RETRIES, - }, persistSession: persistSessionCookie(options), retrieveSession: retrieveSessionCookie, clearSession: deleteSessionCookie(options), diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 18049f846b..7399e927a9 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -10,22 +10,9 @@ export interface SessionState { [key: string]: string | undefined } -type StoreAccessOptionsWithLock = { - pollDelay: number - lockEnabled: true - lockRetryDelay: number - lockMaxTries: number -} - -type StoreAccessOptionsWithoutLock = { - pollDelay: number - lockEnabled: false -} - export type StoreInitOptions = CookieOptions export interface SessionStore { - storeAccessOptions: StoreAccessOptionsWithLock | StoreAccessOptionsWithoutLock persistSession: (session: SessionState) => void retrieveSession: () => SessionState clearSession: () => void diff --git a/packages/core/src/domain/session/sessionStoreManager.spec.ts b/packages/core/src/domain/session/sessionStoreManager.spec.ts index 3751ea21fb..532bf51509 100644 --- a/packages/core/src/domain/session/sessionStoreManager.spec.ts +++ b/packages/core/src/domain/session/sessionStoreManager.spec.ts @@ -4,7 +4,13 @@ import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import { isChromium } from '../../tools/utils/browserDetection' import type { SessionStoreManager } from './sessionStoreManager' -import { processSessionStoreOperations, startSessionStoreManager } from './sessionStoreManager' +import { + LOCK_MAX_TRIES, + LOCK_RETRY_DELAY, + isLockEnabled, + processSessionStoreOperations, + startSessionStoreManager, +} from './sessionStoreManager' import { SESSION_COOKIE_NAME, initCookieStore, toSessionString } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' @@ -549,12 +555,8 @@ describe('session store', () => { cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - const lockMaxTries = cookieStorage.storeAccessOptions.lockEnabled - ? cookieStorage.storeAccessOptions.lockMaxTries - : 0 - const lockRetryDelay = cookieStorage.storeAccessOptions.lockEnabled - ? cookieStorage.storeAccessOptions.lockRetryDelay - : 0 + const lockMaxTries = isLockEnabled() ? LOCK_MAX_TRIES : 0 + const lockRetryDelay = isLockEnabled() ? LOCK_RETRY_DELAY : 0 clock.tick(lockMaxTries * lockRetryDelay) expect(processSpy).not.toHaveBeenCalled() diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts index d410317e3e..01bc61228d 100644 --- a/packages/core/src/domain/session/sessionStoreManager.ts +++ b/packages/core/src/domain/session/sessionStoreManager.ts @@ -1,8 +1,9 @@ import { clearInterval, setInterval, setTimeout } from '../../tools/timer' import { Observable } from '../../tools/observable' -import { dateNow } from '../../tools/utils/timeUtils' +import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' +import { isChromium } from '../../tools/utils/browserDetection' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import { initCookieStore } from './sessionCookieStore' import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' @@ -18,6 +19,8 @@ export interface SessionStoreManager { stop: () => void } +const POLL_DELAY = ONE_SECOND + /** * Different session concepts: * - tracked, the session has an id and is updated along the user navigation @@ -33,9 +36,9 @@ export function startSessionStoreManager( const expireObservable = new Observable() const sessionStore = initCookieStore(options) - const { clearSession, retrieveSession, storeAccessOptions } = sessionStore + const { clearSession, retrieveSession } = sessionStore - const watchSessionTimeoutId = setInterval(watchSession, storeAccessOptions.pollDelay) + const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) let sessionCache: SessionState = retrieveActiveSession() function expandOrRenewSession() { @@ -142,7 +145,7 @@ export function startSessionStoreManager( } return { - expandOrRenewSession: throttle(expandOrRenewSession, storeAccessOptions.pollDelay).throttled, + expandOrRenewSession: throttle(expandOrRenewSession, POLL_DELAY).throttled, expandSession, getSession: () => sessionCache, renewObservable, @@ -162,11 +165,15 @@ type Operations = { after?: (sessionState: SessionState) => void } +export const LOCK_RETRY_DELAY = 10 +export const LOCK_MAX_TRIES = 100 + const bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined export function processSessionStoreOperations(operations: Operations, sessionStore: SessionStore, numberOfRetries = 0) { - const { retrieveSession, persistSession, clearSession, storeAccessOptions } = sessionStore + const { retrieveSession, persistSession, clearSession } = sessionStore + const lockEnabled = isLockEnabled() if (!ongoingOperations) { ongoingOperations = operations @@ -175,16 +182,16 @@ export function processSessionStoreOperations(operations: Operations, sessionSto bufferedOperations.push(operations) return } - if (storeAccessOptions.lockEnabled && numberOfRetries >= storeAccessOptions.lockMaxTries) { + if (lockEnabled && numberOfRetries >= LOCK_MAX_TRIES) { next(sessionStore) return } let currentLock: string let currentSession = retrieveSession() - if (storeAccessOptions.lockEnabled) { + if (lockEnabled) { // if someone has lock, retry later if (currentSession.lock) { - retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + retryLater(operations, sessionStore, numberOfRetries) return } // acquire lock @@ -194,16 +201,16 @@ export function processSessionStoreOperations(operations: Operations, sessionSto // if lock is not acquired, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock) { - retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + retryLater(operations, sessionStore, numberOfRetries) return } } let processedSession = operations.process(currentSession) - if (storeAccessOptions.lockEnabled) { + if (lockEnabled) { // if lock corrupted after process, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + retryLater(operations, sessionStore, numberOfRetries) return } } @@ -215,14 +222,14 @@ export function processSessionStoreOperations(operations: Operations, sessionSto persistSession(processedSession) } } - if (storeAccessOptions.lockEnabled) { + if (lockEnabled) { // correctly handle lock around expiration would require to handle this case properly at several levels // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it if (!(processedSession && isSessionInExpiredState(processedSession))) { // if lock corrupted after persist, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStore, numberOfRetries, storeAccessOptions.lockRetryDelay) + retryLater(operations, sessionStore, numberOfRetries) return } delete currentSession.lock @@ -236,15 +243,16 @@ export function processSessionStoreOperations(operations: Operations, sessionSto next(sessionStore) } -function retryLater( - operations: Operations, - sessionStore: SessionStore, - currentNumberOfRetries: number, - retryDelay: number -) { +/** + * Lock strategy allows mitigating issues due to concurrent access to cookie. + * This issue concerns only chromium browsers and enabling this on firefox increases cookie write failures. + */ +export const isLockEnabled = () => isChromium() + +function retryLater(operations: Operations, sessionStore: SessionStore, currentNumberOfRetries: number) { setTimeout(() => { processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1) - }, retryDelay) + }, LOCK_RETRY_DELAY) } function next(sessionStore: SessionStore) { From bc24152408c885047566aeeca2dd39975f1df68c Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 1 Jun 2023 09:21:17 +0200 Subject: [PATCH 09/40] =?UTF-8?q?=F0=9F=91=8C=20Extract=20sessionStoreOper?= =?UTF-8?q?ations=20in=20its=20own=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/sessionStoreManager.spec.ts | 251 +----------------- .../src/domain/session/sessionStoreManager.ts | 112 +------- .../session/sessionStoreOperations.spec.ts | 248 +++++++++++++++++ .../domain/session/sessionStoreOperations.ts | 107 ++++++++ 4 files changed, 362 insertions(+), 356 deletions(-) create mode 100644 packages/core/src/domain/session/sessionStoreOperations.spec.ts create mode 100644 packages/core/src/domain/session/sessionStoreOperations.ts diff --git a/packages/core/src/domain/session/sessionStoreManager.spec.ts b/packages/core/src/domain/session/sessionStoreManager.spec.ts index 532bf51509..53b6faf90d 100644 --- a/packages/core/src/domain/session/sessionStoreManager.spec.ts +++ b/packages/core/src/domain/session/sessionStoreManager.spec.ts @@ -1,19 +1,11 @@ import type { Clock } from '../../../test' -import { stubCookie, mockClock } from '../../../test' +import { mockClock } from '../../../test' import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' -import { isChromium } from '../../tools/utils/browserDetection' import type { SessionStoreManager } from './sessionStoreManager' -import { - LOCK_MAX_TRIES, - LOCK_RETRY_DELAY, - isLockEnabled, - processSessionStoreOperations, - startSessionStoreManager, -} from './sessionStoreManager' -import { SESSION_COOKIE_NAME, initCookieStore, toSessionString } from './sessionCookieStore' +import { startSessionStoreManager } from './sessionStoreManager' +import { SESSION_COOKIE_NAME } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import type { SessionState, SessionStore } from './sessionStore' const enum FakeTrackingType { TRACKED = 'tracked', @@ -359,241 +351,4 @@ describe('session store', () => { }) }) }) - - describe('process operations mechanism', () => { - const COOKIE_OPTIONS = {} - let initialSession: SessionState - let otherSession: SessionState - let processSpy: jasmine.Spy - let afterSpy: jasmine.Spy - let cookie: ReturnType - let cookieStorage: SessionStore - - beforeEach(() => { - cookieStorage = initCookieStore(COOKIE_OPTIONS) - initialSession = { id: '123', created: '0' } - otherSession = { id: '456', created: '100' } - processSpy = jasmine.createSpy('process') - afterSpy = jasmine.createSpy('after') - cookie = stubCookie() - }) - - describe('with cookie-lock disabled', () => { - beforeEach(() => { - isChromium() && pending('cookie-lock only disabled on non chromium browsers') - }) - - it('should persist session when process returns a value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({ ...otherSession }) - - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should clear session when process return an empty value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({}) - - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = {} - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should not persist session when process return undefined', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue(undefined) - - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith(initialSession) - expect(cookieStorage.retrieveSession()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) - }) - - describe('with cookie-lock enabled', () => { - beforeEach(() => { - !isChromium() && pending('cookie-lock only enabled on chromium browsers') - }) - - it('should persist session when process return a value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) - - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should clear session when process return an empty value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({}) - - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = {} - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - - it('should not persist session when process return undefined', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue(undefined) - - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - expect(cookieStorage.retrieveSession()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) - - type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } - - function lockScenario({ - onInitialLockCheck, - onAcquiredLockCheck, - onPostProcessLockCheck, - onPostPersistLockCheck, - }: { - onInitialLockCheck?: OnLockCheck - onAcquiredLockCheck?: OnLockCheck - onPostProcessLockCheck?: OnLockCheck - onPostPersistLockCheck?: OnLockCheck - }) { - const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] - cookie.getSpy.and.callFake(() => { - const currentOnLockCheck = onLockChecks.shift() - if (!currentOnLockCheck) { - return cookie.currentValue() - } - const { currentState, retryState } = currentOnLockCheck() - cookie.setCurrentValue(buildSessionString(retryState)) - return buildSessionString(currentState) - }) - } - - function buildSessionString(currentState: SessionState) { - return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` - } - - ;[ - { - description: 'should wait for lock to be free', - lockConflict: 'onInitialLockCheck', - }, - { - description: 'should retry if lock was acquired before process', - lockConflict: 'onAcquiredLockCheck', - }, - { - description: 'should retry if lock was acquired after process', - lockConflict: 'onPostProcessLockCheck', - }, - { - description: 'should retry if lock was acquired after persist', - lockConflict: 'onPostPersistLockCheck', - }, - ].forEach(({ description, lockConflict }) => { - it(description, (done) => { - lockScenario({ - [lockConflict]: () => ({ - currentState: { ...initialSession, lock: 'locked' }, - retryState: { ...initialSession, other: 'other' }, - }), - }) - initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) - cookieStorage.persistSession(initialSession) - processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) - - processSessionStoreOperations( - { - process: processSpy, - after: (afterSession) => { - // session with 'other' value on process - expect(processSpy).toHaveBeenCalledWith({ - ...initialSession, - other: 'other', - lock: jasmine.any(String), - expire: jasmine.any(String), - }) - - // end state with session 'other' and 'processed' value - const expectedSession = { - ...initialSession, - other: 'other', - processed: 'processed', - expire: jasmine.any(String), - } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSession).toEqual(expectedSession) - done() - }, - }, - cookieStorage - ) - }) - }) - - it('should abort after a max number of retry', () => { - const clock = mockClock() - - cookieStorage.persistSession(initialSession) - cookie.setSpy.calls.reset() - - cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) - - const lockMaxTries = isLockEnabled() ? LOCK_MAX_TRIES : 0 - const lockRetryDelay = isLockEnabled() ? LOCK_RETRY_DELAY : 0 - - clock.tick(lockMaxTries * lockRetryDelay) - expect(processSpy).not.toHaveBeenCalled() - expect(afterSpy).not.toHaveBeenCalled() - expect(cookie.setSpy).not.toHaveBeenCalled() - - clock.cleanup() - }) - - it('should execute cookie accesses in order', (done) => { - lockScenario({ - onInitialLockCheck: () => ({ - currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later - retryState: initialSession, - }), - }) - cookieStorage.persistSession(initialSession) - - processSessionStoreOperations( - { - process: (session) => ({ ...session, value: 'foo' }), - after: afterSpy, - }, - cookieStorage - ) - processSessionStoreOperations( - { - process: (session) => ({ ...session, value: `${session.value || ''}bar` }), - after: (session) => { - expect(session.value).toBe('foobar') - expect(afterSpy).toHaveBeenCalled() - done() - }, - }, - cookieStorage - ) - }) - }) - }) }) diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts index 01bc61228d..4cfe639431 100644 --- a/packages/core/src/domain/session/sessionStoreManager.ts +++ b/packages/core/src/domain/session/sessionStoreManager.ts @@ -1,13 +1,12 @@ -import { clearInterval, setInterval, setTimeout } from '../../tools/timer' +import { clearInterval, setInterval } from '../../tools/timer' import { Observable } from '../../tools/observable' import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' -import { isChromium } from '../../tools/utils/browserDetection' -import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { initCookieStore } from './sessionCookieStore' -import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' -import { isSessionInExpiredState } from './sessionStore' +import type { SessionState, StoreInitOptions } from './sessionStore' +import { processSessionStoreOperations } from './sessionStoreOperations' export interface SessionStoreManager { expandOrRenewSession: () => void @@ -159,106 +158,3 @@ export function startSessionStoreManager( }, } } - -type Operations = { - process: (sessionState: SessionState) => SessionState | undefined - after?: (sessionState: SessionState) => void -} - -export const LOCK_RETRY_DELAY = 10 -export const LOCK_MAX_TRIES = 100 - -const bufferedOperations: Operations[] = [] -let ongoingOperations: Operations | undefined - -export function processSessionStoreOperations(operations: Operations, sessionStore: SessionStore, numberOfRetries = 0) { - const { retrieveSession, persistSession, clearSession } = sessionStore - const lockEnabled = isLockEnabled() - - if (!ongoingOperations) { - ongoingOperations = operations - } - if (operations !== ongoingOperations) { - bufferedOperations.push(operations) - return - } - if (lockEnabled && numberOfRetries >= LOCK_MAX_TRIES) { - next(sessionStore) - return - } - let currentLock: string - let currentSession = retrieveSession() - if (lockEnabled) { - // if someone has lock, retry later - if (currentSession.lock) { - retryLater(operations, sessionStore, numberOfRetries) - return - } - // acquire lock - currentLock = generateUUID() - currentSession.lock = currentLock - persistSession(currentSession) - // if lock is not acquired, retry later - currentSession = retrieveSession() - if (currentSession.lock !== currentLock) { - retryLater(operations, sessionStore, numberOfRetries) - return - } - } - let processedSession = operations.process(currentSession) - if (lockEnabled) { - // if lock corrupted after process, retry later - currentSession = retrieveSession() - if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStore, numberOfRetries) - return - } - } - if (processedSession) { - if (isSessionInExpiredState(processedSession)) { - clearSession() - } else { - processedSession.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) - persistSession(processedSession) - } - } - if (lockEnabled) { - // correctly handle lock around expiration would require to handle this case properly at several levels - // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it - if (!(processedSession && isSessionInExpiredState(processedSession))) { - // if lock corrupted after persist, retry later - currentSession = retrieveSession() - if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStore, numberOfRetries) - return - } - delete currentSession.lock - persistSession(currentSession) - processedSession = currentSession - } - } - // call after even if session is not persisted in order to perform operations on - // up-to-date session state value => the value could have been modified by another tab - operations.after?.(processedSession || currentSession) - next(sessionStore) -} - -/** - * Lock strategy allows mitigating issues due to concurrent access to cookie. - * This issue concerns only chromium browsers and enabling this on firefox increases cookie write failures. - */ -export const isLockEnabled = () => isChromium() - -function retryLater(operations: Operations, sessionStore: SessionStore, currentNumberOfRetries: number) { - setTimeout(() => { - processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1) - }, LOCK_RETRY_DELAY) -} - -function next(sessionStore: SessionStore) { - ongoingOperations = undefined - const nextOperations = bufferedOperations.shift() - if (nextOperations) { - processSessionStoreOperations(nextOperations, sessionStore) - } -} diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts new file mode 100644 index 0000000000..5fe587fbb2 --- /dev/null +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -0,0 +1,248 @@ +import { stubCookie, mockClock } from '../../../test' +import { isChromium } from '../../tools/utils/browserDetection' +import { SESSION_EXPIRATION_DELAY } from './sessionConstants' +import { initCookieStore, SESSION_COOKIE_NAME, toSessionString } from './sessionCookieStore' +import type { SessionState, SessionStore } from './sessionStore' +import { + processSessionStoreOperations, + isLockEnabled, + LOCK_MAX_TRIES, + LOCK_RETRY_DELAY, +} from './sessionStoreOperations' + +describe('process operations mechanism', () => { + const COOKIE_OPTIONS = {} + let initialSession: SessionState + let otherSession: SessionState + let processSpy: jasmine.Spy + let afterSpy: jasmine.Spy + let cookie: ReturnType + let cookieStorage: SessionStore + + beforeEach(() => { + cookieStorage = initCookieStore(COOKIE_OPTIONS) + initialSession = { id: '123', created: '0' } + otherSession = { id: '456', created: '100' } + processSpy = jasmine.createSpy('process') + afterSpy = jasmine.createSpy('after') + cookie = stubCookie() + }) + + describe('with cookie-lock disabled', () => { + beforeEach(() => { + isChromium() && pending('cookie-lock only disabled on non chromium browsers') + }) + + it('should persist session when process returns a value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({ ...otherSession }) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should clear session when process return an empty value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({}) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = {} + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should not persist session when process return undefined', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue(undefined) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + expect(cookieStorage.retrieveSession()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) + }) + + describe('with cookie-lock enabled', () => { + beforeEach(() => { + !isChromium() && pending('cookie-lock only enabled on chromium browsers') + }) + + it('should persist session when process return a value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should clear session when process return an empty value', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({}) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = {} + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) + + it('should not persist session when process return undefined', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue(undefined) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + expect(cookieStorage.retrieveSession()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) + + type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } + + function lockScenario({ + onInitialLockCheck, + onAcquiredLockCheck, + onPostProcessLockCheck, + onPostPersistLockCheck, + }: { + onInitialLockCheck?: OnLockCheck + onAcquiredLockCheck?: OnLockCheck + onPostProcessLockCheck?: OnLockCheck + onPostPersistLockCheck?: OnLockCheck + }) { + const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] + cookie.getSpy.and.callFake(() => { + const currentOnLockCheck = onLockChecks.shift() + if (!currentOnLockCheck) { + return cookie.currentValue() + } + const { currentState, retryState } = currentOnLockCheck() + cookie.setCurrentValue(buildSessionString(retryState)) + return buildSessionString(currentState) + }) + } + + function buildSessionString(currentState: SessionState) { + return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` + } + + ;[ + { + description: 'should wait for lock to be free', + lockConflict: 'onInitialLockCheck', + }, + { + description: 'should retry if lock was acquired before process', + lockConflict: 'onAcquiredLockCheck', + }, + { + description: 'should retry if lock was acquired after process', + lockConflict: 'onPostProcessLockCheck', + }, + { + description: 'should retry if lock was acquired after persist', + lockConflict: 'onPostPersistLockCheck', + }, + ].forEach(({ description, lockConflict }) => { + it(description, (done) => { + lockScenario({ + [lockConflict]: () => ({ + currentState: { ...initialSession, lock: 'locked' }, + retryState: { ...initialSession, other: 'other' }, + }), + }) + initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) + cookieStorage.persistSession(initialSession) + processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) + + processSessionStoreOperations( + { + process: processSpy, + after: (afterSession) => { + // session with 'other' value on process + expect(processSpy).toHaveBeenCalledWith({ + ...initialSession, + other: 'other', + lock: jasmine.any(String), + expire: jasmine.any(String), + }) + + // end state with session 'other' and 'processed' value + const expectedSession = { + ...initialSession, + other: 'other', + processed: 'processed', + expire: jasmine.any(String), + } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSession).toEqual(expectedSession) + done() + }, + }, + cookieStorage + ) + }) + }) + + it('should abort after a max number of retry', () => { + const clock = mockClock() + + cookieStorage.persistSession(initialSession) + cookie.setSpy.calls.reset() + + cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + + const lockMaxTries = isLockEnabled() ? LOCK_MAX_TRIES : 0 + const lockRetryDelay = isLockEnabled() ? LOCK_RETRY_DELAY : 0 + + clock.tick(lockMaxTries * lockRetryDelay) + expect(processSpy).not.toHaveBeenCalled() + expect(afterSpy).not.toHaveBeenCalled() + expect(cookie.setSpy).not.toHaveBeenCalled() + + clock.cleanup() + }) + + it('should execute cookie accesses in order', (done) => { + lockScenario({ + onInitialLockCheck: () => ({ + currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later + retryState: initialSession, + }), + }) + cookieStorage.persistSession(initialSession) + + processSessionStoreOperations( + { + process: (session) => ({ ...session, value: 'foo' }), + after: afterSpy, + }, + cookieStorage + ) + processSessionStoreOperations( + { + process: (session) => ({ ...session, value: `${session.value || ''}bar` }), + after: (session) => { + expect(session.value).toBe('foobar') + expect(afterSpy).toHaveBeenCalled() + done() + }, + }, + cookieStorage + ) + }) + }) +}) diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts new file mode 100644 index 0000000000..cfdd3852d0 --- /dev/null +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -0,0 +1,107 @@ +import { setTimeout } from '../../tools/timer' +import { dateNow } from '../../tools/utils/timeUtils' +import { generateUUID } from '../../tools/utils/stringUtils' +import { isChromium } from '../../tools/utils/browserDetection' +import { SESSION_EXPIRATION_DELAY } from './sessionConstants' +import type { SessionState, SessionStore } from './sessionStore' +import { isSessionInExpiredState } from './sessionStore' + +type Operations = { + process: (sessionState: SessionState) => SessionState | undefined + after?: (sessionState: SessionState) => void +} + +export const LOCK_RETRY_DELAY = 10 +export const LOCK_MAX_TRIES = 100 +const bufferedOperations: Operations[] = [] +let ongoingOperations: Operations | undefined + +export function processSessionStoreOperations(operations: Operations, sessionStore: SessionStore, numberOfRetries = 0) { + const { retrieveSession, persistSession, clearSession } = sessionStore + const lockEnabled = isLockEnabled() + + if (!ongoingOperations) { + ongoingOperations = operations + } + if (operations !== ongoingOperations) { + bufferedOperations.push(operations) + return + } + if (lockEnabled && numberOfRetries >= LOCK_MAX_TRIES) { + next(sessionStore) + return + } + let currentLock: string + let currentSession = retrieveSession() + if (lockEnabled) { + // if someone has lock, retry later + if (currentSession.lock) { + retryLater(operations, sessionStore, numberOfRetries) + return + } + // acquire lock + currentLock = generateUUID() + currentSession.lock = currentLock + persistSession(currentSession) + // if lock is not acquired, retry later + currentSession = retrieveSession() + if (currentSession.lock !== currentLock) { + retryLater(operations, sessionStore, numberOfRetries) + return + } + } + let processedSession = operations.process(currentSession) + if (lockEnabled) { + // if lock corrupted after process, retry later + currentSession = retrieveSession() + if (currentSession.lock !== currentLock!) { + retryLater(operations, sessionStore, numberOfRetries) + return + } + } + if (processedSession) { + if (isSessionInExpiredState(processedSession)) { + clearSession() + } else { + processedSession.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) + persistSession(processedSession) + } + } + if (lockEnabled) { + // correctly handle lock around expiration would require to handle this case properly at several levels + // since we don't have evidence of lock issues around expiration, let's just not do the corruption check for it + if (!(processedSession && isSessionInExpiredState(processedSession))) { + // if lock corrupted after persist, retry later + currentSession = retrieveSession() + if (currentSession.lock !== currentLock!) { + retryLater(operations, sessionStore, numberOfRetries) + return + } + delete currentSession.lock + persistSession(currentSession) + processedSession = currentSession + } + } + // call after even if session is not persisted in order to perform operations on + // up-to-date session state value => the value could have been modified by another tab + operations.after?.(processedSession || currentSession) + next(sessionStore) +} +/** + * Lock strategy allows mitigating issues due to concurrent access to cookie. + * This issue concerns only chromium browsers and enabling this on firefox increases cookie write failures. + */ + +export const isLockEnabled = () => isChromium() +function retryLater(operations: Operations, sessionStore: SessionStore, currentNumberOfRetries: number) { + setTimeout(() => { + processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1) + }, LOCK_RETRY_DELAY) +} +function next(sessionStore: SessionStore) { + ongoingOperations = undefined + const nextOperations = bufferedOperations.shift() + if (nextOperations) { + processSessionStoreOperations(nextOperations, sessionStore) + } +} From 97f9e7230796824c0b14c8756a5c6fad94886d79 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 1 Jun 2023 09:27:23 +0200 Subject: [PATCH 10/40] =?UTF-8?q?=F0=9F=91=8C=20Renamed=20expandOrRenewCoo?= =?UTF-8?q?kie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/domain/session/sessionStoreManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts index 4cfe639431..5e70f68476 100644 --- a/packages/core/src/domain/session/sessionStoreManager.ts +++ b/packages/core/src/domain/session/sessionStoreManager.ts @@ -46,7 +46,7 @@ export function startSessionStoreManager( { process: (sessionState) => { const synchronizedSession = synchronizeSession(sessionState) - isTracked = expandOrRenewCookie(synchronizedSession) + isTracked = expandOrRenewSessionState(synchronizedSession) return synchronizedSession }, after: (sessionState) => { @@ -98,7 +98,7 @@ export function startSessionStoreManager( return sessionState } - function expandOrRenewCookie(sessionState: SessionState) { + function expandOrRenewSessionState(sessionState: SessionState) { const { trackingType, isTracked } = computeSessionState(sessionState[productKey]) sessionState[productKey] = trackingType if (isTracked && !sessionState.id) { From 72f4303e8fd667d417cbb616dd9707f0cb70bbd2 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 1 Jun 2023 16:22:44 +0200 Subject: [PATCH 11/40] =?UTF-8?q?=F0=9F=91=8C=20Added=20tests=20to=20oldCo?= =?UTF-8?q?okiesMigration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/oldCookiesMigration.spec.ts | 11 +++++++++-- .../core/src/domain/session/oldCookiesMigration.ts | 4 +--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index faf61babf2..732df81c6c 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -13,11 +13,11 @@ describe('old cookies migration', () => { const options: CookieOptions = {} it('should not touch current cookie', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1', SESSION_EXPIRATION_DELAY) + setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) tryOldCookiesMigration(options) - expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1') + expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1&expire=1234567890') }) it('should create new cookie from old cookie values', () => { @@ -30,6 +30,7 @@ describe('old cookies migration', () => { expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcde') expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') expect(getCookie(SESSION_COOKIE_NAME)).toContain('logs=1') + expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/expire=\d+/) }) it('should create new cookie from a single old cookie', () => { @@ -39,5 +40,11 @@ describe('old cookies migration', () => { expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') + expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/expire=\d+/) + }) + + it('should not create a new cookie if no old cookie is present', () => { + tryOldCookiesMigration(options) + expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) }) diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index fd1e0a256f..6b9a547a18 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -3,7 +3,7 @@ import { getCookie } from '../../browser/cookie' import { dateNow } from '../../tools/utils/timeUtils' import type { SessionState } from './sessionStore' import { isSessionInExpiredState } from './sessionStore' -import { SESSION_COOKIE_NAME, deleteSessionCookie, persistSessionCookie } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, persistSessionCookie } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' export const OLD_SESSION_COOKIE_NAME = '_dd' @@ -38,8 +38,6 @@ export function tryOldCookiesMigration(options: CookieOptions) { if (!isSessionInExpiredState(session)) { session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) persistSessionCookie(options)(session) - } else { - deleteSessionCookie(options)() } } } From fdc1996591fb4fbebecb4009e48478f1247a0e0b Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 5 Jun 2023 16:10:38 +0200 Subject: [PATCH 12/40] =?UTF-8?q?=F0=9F=91=8C=20Updated=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/domain/session/sessionCookieStore.spec.ts | 2 ++ packages/core/src/domain/session/sessionManager.spec.ts | 4 ++-- packages/core/src/domain/session/sessionStoreOperations.ts | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index 214c46693b..b80526482a 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -21,6 +21,7 @@ describe('session cookie store', () => { cookieStorage.persistSession(sessionState) const session = cookieStorage.retrieveSession() expect(session).toEqual({ ...sessionState }) + expect(document.cookie).toMatch(/_dd_s=.*id=.*created/) }) it('should delete the cookie holding the session', () => { @@ -28,6 +29,7 @@ describe('session cookie store', () => { cookieStorage.clearSession() const session = cookieStorage.retrieveSession() expect(session).toEqual({}) + expect(document.cookie).not.toMatch(/_dd_s=/) }) it('should return an empty object if session string is invalid', () => { diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 90c27f9f79..a2cbc72d52 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -19,12 +19,12 @@ const enum FakeTrackingType { const TRACKED_SESSION_STATE = { isTracked: true, trackingType: FakeTrackingType.TRACKED, -} as const +} const NOT_TRACKED_SESSION_STATE = { isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED, -} as const +} describe('startSessionManager', () => { const DURATION = 123456 diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index cfdd3852d0..a430c945ae 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -93,11 +93,13 @@ export function processSessionStoreOperations(operations: Operations, sessionSto */ export const isLockEnabled = () => isChromium() + function retryLater(operations: Operations, sessionStore: SessionStore, currentNumberOfRetries: number) { setTimeout(() => { processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1) }, LOCK_RETRY_DELAY) } + function next(sessionStore: SessionStore) { ongoingOperations = undefined const nextOperations = bufferedOperations.shift() From 99d6f9739cc6200c17c4dad74de1731af3f86e18 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 11 May 2023 15:13:53 +0200 Subject: [PATCH 13/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Extract=20session?= =?UTF-8?q?=20utilities=20from=20CookieStore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/sessionCookieStore.ts | 30 +------------- .../src/domain/session/sessionStore.spec.ts | 41 ++++++++++++++++--- .../core/src/domain/session/sessionStore.ts | 31 ++++++++++++++ .../session/sessionStoreOperations.spec.ts | 3 +- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index a22b1fc2f2..35427bd34e 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,11 +1,8 @@ import type { CookieOptions } from '../../browser/cookie' import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' -import { objectEntries } from '../../tools/utils/polyfills' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' - -const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ -const SESSION_ENTRY_SEPARATOR = '&' +import { sessionStringToSessionState, toSessionString } from './sessionStore' export const SESSION_COOKIE_NAME = '_dd_s' @@ -23,25 +20,9 @@ export function persistSessionCookie(options: CookieOptions) { } } -export function toSessionString(session: SessionState) { - return objectEntries(session) - .map(([key, value]) => `${key}=${value as string}`) - .join(SESSION_ENTRY_SEPARATOR) -} - function retrieveSessionCookie(): SessionState { const sessionString = getCookie(SESSION_COOKIE_NAME) - const session: SessionState = {} - if (isValidSessionString(sessionString)) { - sessionString.split(SESSION_ENTRY_SEPARATOR).forEach((entry) => { - const matches = SESSION_ENTRY_REGEXP.exec(entry) - if (matches !== null) { - const [, key, value] = matches - session[key] = value - } - }) - } - return session + return sessionStringToSessionState(sessionString) } export function deleteSessionCookie(options: CookieOptions) { @@ -49,10 +30,3 @@ export function deleteSessionCookie(options: CookieOptions) { deleteCookie(SESSION_COOKIE_NAME, options) } } - -function isValidSessionString(sessionString: string | undefined): sessionString is string { - return ( - sessionString !== undefined && - (sessionString.indexOf(SESSION_ENTRY_SEPARATOR) !== -1 || SESSION_ENTRY_REGEXP.test(sessionString)) - ) -} diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index ed67c69cf0..d8183d63b7 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,15 +1,44 @@ import type { SessionState } from './sessionStore' -import { isSessionInExpiredState } from './sessionStore' +import { isSessionInExpiredState, toSessionString, sessionStringToSessionState } from './sessionStore' -describe('session storage utilities', () => { +describe('session store utilities', () => { const EXPIRED_SESSION: SessionState = {} + const SERIALIZED_EXPIRED_SESSION = '' const LIVE_SESSION: SessionState = { created: '0', id: '123' } + const SERIALIZED_LIVE_SESSION = 'created=0&id=123' - it('should correctly identify a session in expired state', () => { - expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) + describe('isSessionInExpiredState', () => { + it('should correctly identify a session in expired state', () => { + expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) + }) + + it('should correctly identify a session in live state', () => { + expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) + }) + }) + + describe('toSessionString', () => { + it('should serialize a sessionState to a string', () => { + expect(toSessionString(LIVE_SESSION)).toEqual(SERIALIZED_LIVE_SESSION) + }) + + it('should handle empty sessionStates', () => { + expect(toSessionString(EXPIRED_SESSION)).toEqual(SERIALIZED_EXPIRED_SESSION) + }) }) - it('should correctly identify a session in live state', () => { - expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) + describe('sessionStringToSessionState', () => { + it('should deserialize a session string to a sessionState', () => { + expect(sessionStringToSessionState(SERIALIZED_LIVE_SESSION)).toEqual(LIVE_SESSION) + }) + + it('should handle empty session strings', () => { + expect(sessionStringToSessionState(SERIALIZED_EXPIRED_SESSION)).toEqual(EXPIRED_SESSION) + }) + + it('should handle invalid session strings', () => { + const sessionString = '{invalid: true}' + expect(sessionStringToSessionState(sessionString)).toEqual(EXPIRED_SESSION) + }) }) }) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 7399e927a9..9baa670d7c 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -1,5 +1,9 @@ import type { CookieOptions } from '../../browser/cookie' import { isEmptyObject } from '../../tools/utils/objectUtils' +import { objectEntries } from '../../tools/utils/polyfills' + +const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ +const SESSION_ENTRY_SEPARATOR = '&' export interface SessionState { id?: string @@ -21,3 +25,30 @@ export interface SessionStore { export function isSessionInExpiredState(session: SessionState) { return isEmptyObject(session) } + +export function toSessionString(session: SessionState) { + return objectEntries(session) + .map(([key, value]) => `${key}=${value as string}`) + .join(SESSION_ENTRY_SEPARATOR) +} + +export function sessionStringToSessionState(sessionString: string | undefined | null) { + const session: SessionState = {} + if (isValidSessionString(sessionString)) { + sessionString.split(SESSION_ENTRY_SEPARATOR).forEach((entry) => { + const matches = SESSION_ENTRY_REGEXP.exec(entry) + if (matches !== null) { + const [, key, value] = matches + session[key] = value + } + }) + } + return session +} + +function isValidSessionString(sessionString: string | undefined | null): sessionString is string { + return ( + !!sessionString && + (sessionString.indexOf(SESSION_ENTRY_SEPARATOR) !== -1 || SESSION_ENTRY_REGEXP.test(sessionString)) + ) +} diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 5fe587fbb2..2764d580e8 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -1,8 +1,9 @@ import { stubCookie, mockClock } from '../../../test' import { isChromium } from '../../tools/utils/browserDetection' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { initCookieStore, SESSION_COOKIE_NAME, toSessionString } from './sessionCookieStore' +import { initCookieStore, SESSION_COOKIE_NAME } from './sessionCookieStore' import type { SessionState, SessionStore } from './sessionStore' +import { toSessionString } from './sessionStore' import { processSessionStoreOperations, isLockEnabled, From 59e2610de07213e07efd6484b670a13d926825a1 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 12 May 2023 17:25:33 +0200 Subject: [PATCH 14/40] =?UTF-8?q?=E2=9C=A8=20Implement=20Local=20Storage?= =?UTF-8?q?=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/sessionLocalStorageStore.spec.ts | 42 +++++++++++++++++++ .../session/sessionLocalStorageStore.ts | 25 +++++++++++ 2 files changed, 67 insertions(+) create mode 100644 packages/core/src/domain/session/sessionLocalStorageStore.spec.ts create mode 100644 packages/core/src/domain/session/sessionLocalStorageStore.ts diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts new file mode 100644 index 0000000000..65bb1ebcfa --- /dev/null +++ b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts @@ -0,0 +1,42 @@ +import { LOCAL_STORAGE_KEY, initLocalStorage } from './sessionLocalStorageStore' +import type { SessionState, SessionStore } from './sessionStore' + +describe('session local storage store', () => { + const sessionState: SessionState = { id: '123', created: '0' } + let localStorageStore: SessionStore + + beforeEach(() => { + localStorageStore = initLocalStorage() + }) + + afterEach(() => { + window.localStorage.clear() + }) + + it('should persist a session in local storage', () => { + localStorageStore.persistSession(sessionState) + const session = localStorageStore.retrieveSession() + expect(session).toEqual({ ...sessionState }) + }) + + it('should delete the local storage item holding the session', () => { + localStorageStore.persistSession(sessionState) + localStorageStore.clearSession() + const session = localStorageStore.retrieveSession() + expect(session).toEqual({}) + }) + + it('should not interfere with other keys present in local storage', () => { + window.localStorage.setItem('test', 'hello') + localStorageStore.persistSession(sessionState) + localStorageStore.retrieveSession() + localStorageStore.clearSession() + expect(window.localStorage.getItem('test')).toEqual('hello') + }) + + it('should return an empty object if session string is invalid', () => { + window.localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') + const session = localStorageStore.retrieveSession() + expect(session).toEqual({}) + }) +}) diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.ts b/packages/core/src/domain/session/sessionLocalStorageStore.ts new file mode 100644 index 0000000000..11c45d4e0c --- /dev/null +++ b/packages/core/src/domain/session/sessionLocalStorageStore.ts @@ -0,0 +1,25 @@ +import type { SessionState, SessionStore } from './sessionStore' +import { toSessionString, sessionStringToSessionState } from './sessionStore' + +export const LOCAL_STORAGE_KEY = '_dd_s' + +export function initLocalStorage(): SessionStore { + return { + persistSession: persistInLocalStorage, + retrieveSession: retrieveSessionFromLocalStorage, + clearSession: clearSessionFromLocalStorage, + } +} + +function persistInLocalStorage(sessionState: SessionState) { + localStorage.setItem(LOCAL_STORAGE_KEY, toSessionString(sessionState)) +} + +function retrieveSessionFromLocalStorage(): SessionState { + const sessionString = localStorage.getItem(LOCAL_STORAGE_KEY) + return sessionStringToSessionState(sessionString) +} + +function clearSessionFromLocalStorage() { + localStorage.removeItem(LOCAL_STORAGE_KEY) +} From 2de32d1a2a3a5898f33fb91bfe72424facc32b62 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 5 Jun 2023 16:34:24 +0200 Subject: [PATCH 15/40] =?UTF-8?q?=F0=9F=91=8C=20Naming=20toSessionState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/domain/session/sessionCookieStore.ts | 4 ++-- .../core/src/domain/session/sessionLocalStorageStore.ts | 4 ++-- packages/core/src/domain/session/sessionStore.spec.ts | 8 ++++---- packages/core/src/domain/session/sessionStore.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index 35427bd34e..43e6e0d347 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -2,7 +2,7 @@ import type { CookieOptions } from '../../browser/cookie' import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' -import { sessionStringToSessionState, toSessionString } from './sessionStore' +import { toSessionState, toSessionString } from './sessionStore' export const SESSION_COOKIE_NAME = '_dd_s' @@ -22,7 +22,7 @@ export function persistSessionCookie(options: CookieOptions) { function retrieveSessionCookie(): SessionState { const sessionString = getCookie(SESSION_COOKIE_NAME) - return sessionStringToSessionState(sessionString) + return toSessionState(sessionString) } export function deleteSessionCookie(options: CookieOptions) { diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.ts b/packages/core/src/domain/session/sessionLocalStorageStore.ts index 11c45d4e0c..f7670d6607 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.ts @@ -1,5 +1,5 @@ import type { SessionState, SessionStore } from './sessionStore' -import { toSessionString, sessionStringToSessionState } from './sessionStore' +import { toSessionString, toSessionState } from './sessionStore' export const LOCAL_STORAGE_KEY = '_dd_s' @@ -17,7 +17,7 @@ function persistInLocalStorage(sessionState: SessionState) { function retrieveSessionFromLocalStorage(): SessionState { const sessionString = localStorage.getItem(LOCAL_STORAGE_KEY) - return sessionStringToSessionState(sessionString) + return toSessionState(sessionString) } function clearSessionFromLocalStorage() { diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index d8183d63b7..f3e181d942 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 { SessionState } from './sessionStore' -import { isSessionInExpiredState, toSessionString, sessionStringToSessionState } from './sessionStore' +import { isSessionInExpiredState, toSessionString, toSessionState } from './sessionStore' describe('session store utilities', () => { const EXPIRED_SESSION: SessionState = {} @@ -29,16 +29,16 @@ describe('session store utilities', () => { describe('sessionStringToSessionState', () => { it('should deserialize a session string to a sessionState', () => { - expect(sessionStringToSessionState(SERIALIZED_LIVE_SESSION)).toEqual(LIVE_SESSION) + expect(toSessionState(SERIALIZED_LIVE_SESSION)).toEqual(LIVE_SESSION) }) it('should handle empty session strings', () => { - expect(sessionStringToSessionState(SERIALIZED_EXPIRED_SESSION)).toEqual(EXPIRED_SESSION) + expect(toSessionState(SERIALIZED_EXPIRED_SESSION)).toEqual(EXPIRED_SESSION) }) it('should handle invalid session strings', () => { const sessionString = '{invalid: true}' - expect(sessionStringToSessionState(sessionString)).toEqual(EXPIRED_SESSION) + expect(toSessionState(sessionString)).toEqual(EXPIRED_SESSION) }) }) }) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 9baa670d7c..185e4e48a8 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -32,7 +32,7 @@ export function toSessionString(session: SessionState) { .join(SESSION_ENTRY_SEPARATOR) } -export function sessionStringToSessionState(sessionString: string | undefined | null) { +export function toSessionState(sessionString: string | undefined | null) { const session: SessionState = {} if (isValidSessionString(sessionString)) { sessionString.split(SESSION_ENTRY_SEPARATOR).forEach((entry) => { From 6fa570941c989680cb9f597623bb3ff9b5ad9aa8 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 5 Jun 2023 16:39:46 +0200 Subject: [PATCH 16/40] =?UTF-8?q?=F0=9F=91=8C=20Improved=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/session/sessionLocalStorageStore.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts index 65bb1ebcfa..ad69a67dfa 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts @@ -17,6 +17,7 @@ describe('session local storage store', () => { localStorageStore.persistSession(sessionState) const session = localStorageStore.retrieveSession() expect(session).toEqual({ ...sessionState }) + expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toMatch(/.*id=.*created/) }) it('should delete the local storage item holding the session', () => { @@ -24,6 +25,7 @@ describe('session local storage store', () => { localStorageStore.clearSession() const session = localStorageStore.retrieveSession() expect(session).toEqual({}) + expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull() }) it('should not interfere with other keys present in local storage', () => { From d176b798c0b12a96a016990eee139f60435eb766 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Tue, 6 Jun 2023 11:55:04 +0200 Subject: [PATCH 17/40] =?UTF-8?q?=F0=9F=91=8C=20Added=20test=20for=20MAX?= =?UTF-8?q?=5FLOCK=5FTRIES=20+=20used=20getCookie=20to=20check=20session?= =?UTF-8?q?=20persistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/sessionCookieStore.spec.ts | 6 +++--- .../domain/session/sessionStoreOperations.spec.ts | 12 ++++++++++++ .../src/domain/session/sessionStoreOperations.ts | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index b80526482a..d5d653e439 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -1,5 +1,5 @@ import type { CookieOptions } from '../../browser/cookie' -import { setCookie, deleteCookie } from '../../browser/cookie' +import { getCookie, setCookie, deleteCookie } from '../../browser/cookie' import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' import type { SessionState, SessionStore } from './sessionStore' @@ -21,7 +21,7 @@ describe('session cookie store', () => { cookieStorage.persistSession(sessionState) const session = cookieStorage.retrieveSession() expect(session).toEqual({ ...sessionState }) - expect(document.cookie).toMatch(/_dd_s=.*id=.*created/) + expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=123&created=0') }) it('should delete the cookie holding the session', () => { @@ -29,7 +29,7 @@ describe('session cookie store', () => { cookieStorage.clearSession() const session = cookieStorage.retrieveSession() expect(session).toEqual({}) - expect(document.cookie).not.toMatch(/_dd_s=/) + expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) it('should return an empty object if session string is invalid', () => { diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 5fe587fbb2..464e135a96 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -67,6 +67,18 @@ describe('process operations mechanism', () => { expect(cookieStorage.retrieveSession()).toEqual(initialSession) expect(afterSpy).toHaveBeenCalledWith(initialSession) }) + + it('LOCK_MAX_TRIES value should not influence the behavior when lock mechanism is not enabled', () => { + cookieStorage.persistSession(initialSession) + processSpy.and.returnValue({ ...otherSession }) + + processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage, LOCK_MAX_TRIES) + + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(cookieStorage.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) }) describe('with cookie-lock enabled', () => { diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index a430c945ae..a47558c0e0 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -87,11 +87,11 @@ export function processSessionStoreOperations(operations: Operations, sessionSto operations.after?.(processedSession || currentSession) next(sessionStore) } + /** * Lock strategy allows mitigating issues due to concurrent access to cookie. * This issue concerns only chromium browsers and enabling this on firefox increases cookie write failures. */ - export const isLockEnabled = () => isChromium() function retryLater(operations: Operations, sessionStore: SessionStore, currentNumberOfRetries: number) { From 81618f405f6d4400c2d0fdc26411a76a96be5551 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 15 May 2023 21:30:56 +0200 Subject: [PATCH 18/40] =?UTF-8?q?=E2=9C=85=20Added=20tests=20on=20session?= =?UTF-8?q?=20handling=20capabilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/rum-core/src/boot/rumPublicApi.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/rum-core/src/boot/rumPublicApi.spec.ts b/packages/rum-core/src/boot/rumPublicApi.spec.ts index 455f13fdd6..4cb83c8e88 100644 --- a/packages/rum-core/src/boot/rumPublicApi.spec.ts +++ b/packages/rum-core/src/boot/rumPublicApi.spec.ts @@ -96,6 +96,13 @@ describe('rum public api', () => { rumPublicApi.init(invalidConfiguration as RumInitConfiguration) expect(rumPublicApi.getInitConfiguration()?.sessionSampleRate).toEqual(100) }) + + it('should initialize even if session cannot be handled', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const rumPublicApi = makeRumPublicApi(startRumSpy, noopRecorderApi, {}) + rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(startRumSpy).toHaveBeenCalled() + }) }) }) @@ -110,6 +117,15 @@ describe('rum public api', () => { cleanupSyntheticsWorkerValues() }) + 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 rumPublicApi = makeRumPublicApi(startRumSpy, noopRecorderApi, {}) + rumPublicApi.init(DEFAULT_INIT_CONFIGURATION) + expect(startRumSpy).not.toHaveBeenCalled() + expect(displaySpy).toHaveBeenCalled() + }) + describe('skipInitIfSyntheticsWillInjectRum option', () => { it('when true, ignores init() call if Synthetics will inject its own instance of RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) From 8e5b802719665f9390f519086dcf2840b9805b1f Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Wed, 17 May 2023 14:50:13 +0200 Subject: [PATCH 19/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Moved=20old=20coo?= =?UTF-8?q?kie=20migration=20invocation=20to=20cookie=20storage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/session/oldCookiesMigration.spec.ts | 13 ++++++------- .../src/domain/session/oldCookiesMigration.ts | 17 ++++++++--------- .../src/domain/session/sessionCookieStore.ts | 7 ++++++- .../core/src/domain/session/sessionManager.ts | 4 +--- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index 732df81c6c..0ee10ab043 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -1,4 +1,3 @@ -import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie } from '../../browser/cookie' import { OLD_LOGS_COOKIE_NAME, @@ -7,15 +6,15 @@ import { tryOldCookiesMigration, } from './oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { SESSION_COOKIE_NAME } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' describe('old cookies migration', () => { - const options: CookieOptions = {} + const sessionStore = initCookieStore({})! it('should not touch current cookie', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(options) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1&expire=1234567890') }) @@ -25,7 +24,7 @@ describe('old cookies migration', () => { setCookie(OLD_LOGS_COOKIE_NAME, '1', SESSION_EXPIRATION_DELAY) setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(options) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcde') expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') @@ -36,7 +35,7 @@ describe('old cookies migration', () => { it('should create new cookie from a single old cookie', () => { setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(options) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') @@ -44,7 +43,7 @@ describe('old cookies migration', () => { }) it('should not create a new cookie if no old cookie is present', () => { - tryOldCookiesMigration(options) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) }) diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index 6b9a547a18..7573ba00c2 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,9 +1,7 @@ -import type { CookieOptions } from '../../browser/cookie' import { getCookie } from '../../browser/cookie' import { dateNow } from '../../tools/utils/timeUtils' -import type { SessionState } from './sessionStore' +import type { SessionState, SessionStore } from './sessionStore' import { isSessionInExpiredState } from './sessionStore' -import { SESSION_COOKIE_NAME, persistSessionCookie } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' export const OLD_SESSION_COOKIE_NAME = '_dd' @@ -18,13 +16,14 @@ export const LOGS_SESSION_KEY = 'logs' * This migration should remain in the codebase as long as older versions are available/live * to allow older sdk versions to be upgraded to newer versions without compatibility issues. */ -export function tryOldCookiesMigration(options: CookieOptions) { - const sessionString = getCookie(SESSION_COOKIE_NAME) - const oldSessionId = getCookie(OLD_SESSION_COOKIE_NAME) - const oldRumType = getCookie(OLD_RUM_COOKIE_NAME) - const oldLogsType = getCookie(OLD_LOGS_COOKIE_NAME) +export function tryOldCookiesMigration(cookieName: string, sessionStore: SessionStore) { + const sessionString = getCookie(cookieName) if (!sessionString) { + const oldSessionId = getCookie(OLD_SESSION_COOKIE_NAME) + const oldRumType = getCookie(OLD_RUM_COOKIE_NAME) + const oldLogsType = getCookie(OLD_LOGS_COOKIE_NAME) const session: SessionState = {} + if (oldSessionId) { session.id = oldSessionId } @@ -37,7 +36,7 @@ export function tryOldCookiesMigration(options: CookieOptions) { if (!isSessionInExpiredState(session)) { session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) - persistSessionCookie(options)(session) + sessionStore.persistSession(session) } } } diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index 43e6e0d347..df86d77c42 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,5 +1,6 @@ import type { CookieOptions } from '../../browser/cookie' import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' +import { tryOldCookiesMigration } from './oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' import { toSessionState, toSessionString } from './sessionStore' @@ -7,11 +8,15 @@ import { toSessionState, toSessionString } from './sessionStore' export const SESSION_COOKIE_NAME = '_dd_s' export function initCookieStore(options: CookieOptions): SessionStore { - return { + const cookieStore = { persistSession: persistSessionCookie(options), retrieveSession: retrieveSessionCookie, clearSession: deleteSessionCookie(options), } + + tryOldCookiesMigration(SESSION_COOKIE_NAME, cookieStore) + + return cookieStore } export function persistSessionCookie(options: CookieOptions) { diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 724fe58c2c..352ec6f8cc 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -6,9 +6,8 @@ import type { RelativeTime } from '../../tools/utils/timeUtils' import { relativeNow, clocksOrigin, ONE_MINUTE } from '../../tools/utils/timeUtils' import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/addEventListener' import { clearInterval, setInterval } from '../../tools/timer' -import { tryOldCookiesMigration } from './oldCookiesMigration' -import { startSessionStoreManager } from './sessionStoreManager' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { startSessionStoreManager } from './sessionStoreManager' export interface SessionManager { findActiveSession: (startTime?: RelativeTime) => SessionContext | undefined @@ -31,7 +30,6 @@ export function startSessionManager( productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionManager { - tryOldCookiesMigration(options) const sessionStore = startSessionStoreManager(options, productKey, computeSessionState) stopCallbacks.push(() => sessionStore.stop()) From 008e86edfd28d7c64e66eafedbd5fd2905868f86 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Wed, 17 May 2023 14:27:12 +0200 Subject: [PATCH 20/40] =?UTF-8?q?=E2=9C=A8=20Implement=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/configuration/configuration.ts | 13 ++ .../domain/session/sessionCookieStore.spec.ts | 14 +- .../src/domain/session/sessionCookieStore.ts | 12 +- .../session/sessionLocalStorageStore.spec.ts | 42 +++-- .../session/sessionLocalStorageStore.ts | 21 ++- .../src/domain/session/sessionManager.spec.ts | 102 ++++-------- .../core/src/domain/session/sessionManager.ts | 28 ++-- .../session/sessionStoreManager.spec.ts | 154 ++++++++++++------ .../src/domain/session/sessionStoreManager.ts | 22 ++- .../session/sessionStoreOperations.spec.ts | 2 +- packages/core/src/index.ts | 1 + packages/logs/src/boot/startLogs.ts | 3 +- .../src/domain/logsSessionManager.spec.ts | 6 +- .../logs/src/domain/logsSessionManager.ts | 6 +- packages/rum-core/src/boot/rumPublicApi.ts | 34 ++-- .../rum-core/src/domain/rumSessionManager.ts | 6 +- 16 files changed, 279 insertions(+), 187 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index c2f57f639e..2791cec7d6 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -10,6 +10,8 @@ import { isPercentage } from '../../tools/utils/numberUtils' import { ONE_KIBI_BYTE } from '../../tools/utils/byteUtils' import { objectHasValue } from '../../tools/utils/objectUtils' import { assign } from '../../tools/utils/polyfills' +import { initSessionStore } from '../session/sessionStoreManager' +import type { SessionStore } from '../session/sessionStore' import type { TransportConfiguration } from './transportConfiguration' import { computeTransportConfiguration } from './transportConfiguration' @@ -52,6 +54,9 @@ export interface InitConfiguration { useSecureSessionCookie?: boolean | undefined trackSessionAcrossSubdomains?: boolean | undefined + // alternate storage option + allowFallbackToLocalStorage?: boolean | undefined + // internal options enableExperimentalFeatures?: string[] | undefined replica?: ReplicaUserConfiguration | undefined @@ -74,6 +79,8 @@ export interface Configuration extends TransportConfiguration { // Built from init configuration beforeSend: GenericBeforeSendCallback | undefined cookieOptions: CookieOptions + allowFallbackToLocalStorage: boolean + sessionStore: SessionStore | undefined sessionSampleRate: number telemetrySampleRate: number telemetryConfigurationSampleRate: number @@ -116,6 +123,10 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati return } + const cookieOptions = buildCookieOptions(initConfiguration) + const allowFallbackToLocalStorage = initConfiguration.allowFallbackToLocalStorage || false + const sessionStore = initSessionStore(cookieOptions, allowFallbackToLocalStorage) + // Set the experimental feature flags as early as possible, so we can use them in most places if (Array.isArray(initConfiguration.enableExperimentalFeatures)) { addExperimentalFeatures( @@ -130,6 +141,8 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), cookieOptions: buildCookieOptions(initConfiguration), + allowFallbackToLocalStorage, + sessionStore, sessionSampleRate: sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index d5d653e439..d8e14158a9 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -7,7 +7,7 @@ import type { SessionState, SessionStore } from './sessionStore' describe('session cookie store', () => { const sessionState: SessionState = { id: '123', created: '0' } const noOptions: CookieOptions = {} - let cookieStorage: SessionStore + let cookieStorage: SessionStore | undefined beforeEach(() => { cookieStorage = initCookieStore(noOptions) @@ -18,23 +18,23 @@ describe('session cookie store', () => { }) it('should persist a session in a cookie', () => { - cookieStorage.persistSession(sessionState) - const session = cookieStorage.retrieveSession() + cookieStorage?.persistSession(sessionState) + const session = cookieStorage?.retrieveSession() expect(session).toEqual({ ...sessionState }) expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=123&created=0') }) it('should delete the cookie holding the session', () => { - cookieStorage.persistSession(sessionState) - cookieStorage.clearSession() - const session = cookieStorage.retrieveSession() + cookieStorage?.persistSession(sessionState) + cookieStorage?.clearSession() + const session = cookieStorage?.retrieveSession() expect(session).toEqual({}) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) it('should return an empty object if session string is invalid', () => { setCookie(SESSION_COOKIE_NAME, '{test:42}', 1000) - const session = cookieStorage.retrieveSession() + const session = cookieStorage?.retrieveSession() expect(session).toEqual({}) }) }) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index df86d77c42..a5e7989042 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,5 +1,5 @@ import type { CookieOptions } from '../../browser/cookie' -import { deleteCookie, getCookie, setCookie } from '../../browser/cookie' +import { areCookiesAuthorized, deleteCookie, getCookie, setCookie } from '../../browser/cookie' import { tryOldCookiesMigration } from './oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' @@ -7,7 +7,11 @@ import { toSessionState, toSessionString } from './sessionStore' export const SESSION_COOKIE_NAME = '_dd_s' -export function initCookieStore(options: CookieOptions): SessionStore { +export function initCookieStore(options: CookieOptions): SessionStore | undefined { + if (!areCookiesAuthorized(options)) { + return undefined + } + const cookieStore = { persistSession: persistSessionCookie(options), retrieveSession: retrieveSessionCookie, @@ -19,7 +23,7 @@ export function initCookieStore(options: CookieOptions): SessionStore { return cookieStore } -export function persistSessionCookie(options: CookieOptions) { +function persistSessionCookie(options: CookieOptions) { return (session: SessionState) => { setCookie(SESSION_COOKIE_NAME, toSessionString(session), SESSION_EXPIRATION_DELAY, options) } @@ -30,7 +34,7 @@ function retrieveSessionCookie(): SessionState { return toSessionState(sessionString) } -export function deleteSessionCookie(options: CookieOptions) { +function deleteSessionCookie(options: CookieOptions) { return () => { deleteCookie(SESSION_COOKIE_NAME, options) } diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts index ad69a67dfa..8d33f6e450 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts @@ -1,44 +1,54 @@ import { LOCAL_STORAGE_KEY, initLocalStorage } from './sessionLocalStorageStore' -import type { SessionState, SessionStore } from './sessionStore' +import type { SessionState } from './sessionStore' describe('session local storage store', () => { const sessionState: SessionState = { id: '123', created: '0' } - let localStorageStore: SessionStore - - beforeEach(() => { - localStorageStore = initLocalStorage() - }) afterEach(() => { window.localStorage.clear() }) + it('should report local storage as available', () => { + const localStorageStore = initLocalStorage({}) + expect(localStorageStore).toBeDefined() + }) + + it('should report local storage as not available', () => { + spyOn(localStorage, 'getItem').and.throwError('Unavailable') + const localStorageStore = initLocalStorage({}) + expect(localStorageStore).not.toBeDefined() + }) + it('should persist a session in local storage', () => { - localStorageStore.persistSession(sessionState) - const session = localStorageStore.retrieveSession() + const localStorageStore = initLocalStorage({}) + localStorageStore?.persistSession(sessionState) + const session = localStorageStore?.retrieveSession() expect(session).toEqual({ ...sessionState }) expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toMatch(/.*id=.*created/) }) it('should delete the local storage item holding the session', () => { - localStorageStore.persistSession(sessionState) - localStorageStore.clearSession() - const session = localStorageStore.retrieveSession() + const localStorageStore = initLocalStorage({}) + localStorageStore?.persistSession(sessionState) + localStorageStore?.clearSession() + const session = localStorageStore?.retrieveSession() expect(session).toEqual({}) expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull() }) it('should not interfere with other keys present in local storage', () => { window.localStorage.setItem('test', 'hello') - localStorageStore.persistSession(sessionState) - localStorageStore.retrieveSession() - localStorageStore.clearSession() + const localStorageStore = initLocalStorage({}) + localStorageStore?.persistSession(sessionState) + localStorageStore?.retrieveSession() + localStorageStore?.clearSession() expect(window.localStorage.getItem('test')).toEqual('hello') }) it('should return an empty object if session string is invalid', () => { - window.localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') - const session = localStorageStore.retrieveSession() + const localStorageStore = initLocalStorage({}) + localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') + const session = localStorageStore?.retrieveSession() expect(session).toEqual({}) }) }) diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.ts b/packages/core/src/domain/session/sessionLocalStorageStore.ts index f7670d6607..6a90772c72 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.ts @@ -1,9 +1,14 @@ -import type { SessionState, SessionStore } from './sessionStore' +import { generateUUID } from '../../tools/utils/stringUtils' +import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' import { toSessionString, toSessionState } from './sessionStore' export const LOCAL_STORAGE_KEY = '_dd_s' -export function initLocalStorage(): SessionStore { +export function initLocalStorage(_options: StoreInitOptions): SessionStore | undefined { + if (!isLocalStorageAvailable()) { + return undefined + } + return { persistSession: persistInLocalStorage, retrieveSession: retrieveSessionFromLocalStorage, @@ -23,3 +28,15 @@ function retrieveSessionFromLocalStorage(): SessionState { function clearSessionFromLocalStorage() { localStorage.removeItem(LOCAL_STORAGE_KEY) } + +function isLocalStorageAvailable() { + try { + const id = generateUUID() + localStorage.setItem(`_dd_s_${id}`, id) + const retrievedId = localStorage.getItem(`_dd_s_${id}`) + localStorage.removeItem(`_dd_s_${id}`) + return id === retrievedId + } catch (e) { + return false + } +} diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index a2cbc72d52..39735adf38 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -8,7 +8,7 @@ import { DOM_EVENT } from '../../browser/addEventListener' import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' -import { SESSION_COOKIE_NAME } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' const enum FakeTrackingType { @@ -31,6 +31,7 @@ describe('startSessionManager', () => { const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' const COOKIE_OPTIONS: CookieOptions = {} + const sessionStore = initCookieStore(COOKIE_OPTIONS)! let clock: Clock function expireSessionCookie() { @@ -84,14 +85,14 @@ describe('startSessionManager', () => { describe('cookie management', () => { it('when tracked, should store tracking type and session id', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expectSessionIdToBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) it('when not tracked should store tracking type', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -100,7 +101,7 @@ describe('startSessionManager', () => { it('when tracked should keep existing tracking type and session id', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcdef&first=tracked', DURATION) - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expectSessionIdToBe(sessionManager, 'abcdef') expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) @@ -109,46 +110,13 @@ describe('startSessionManager', () => { it('when not tracked should keep existing tracking type', () => { setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) }) }) - describe('cookie options', () => { - ;[ - { - cookieOptions: {}, - cookieString: /^_dd_s=[^;]*;expires=[^;]+;path=\/;samesite=strict$/, - description: 'should set same-site to strict by default', - }, - { - cookieOptions: { crossSite: true }, - cookieString: /^_dd_s=[^;]*;expires=[^;]+;path=\/;samesite=none$/, - description: 'should set same site to none for crossSite', - }, - { - cookieOptions: { secure: true }, - cookieString: /^_dd_s=[^;]*;expires=[^;]+;path=\/;samesite=strict;secure$/, - description: 'should add secure attribute when defined', - }, - { - cookieOptions: { domain: 'foo.bar' }, - cookieString: /^_dd_s=[^;]*;expires=[^;]+;path=\/;samesite=strict;domain=foo\.bar$/, - description: 'should set cookie domain when defined', - }, - ].forEach(({ description, cookieOptions, cookieString }) => { - it(description, () => { - const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') - - startSessionManager(cookieOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) - - expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) - }) - }) - }) - describe('computeSessionState', () => { let spy: (rawTrackingType?: string) => { trackingType: FakeTrackingType; isTracked: boolean } @@ -157,32 +125,32 @@ describe('startSessionManager', () => { }) it('should be called with an empty value if the cookie is not defined', () => { - startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(undefined) }) it('should be called with an invalid value if the cookie has an invalid value', () => { setCookie(SESSION_COOKIE_NAME, 'first=invalid', DURATION) - startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith('invalid') }) it('should be called with TRACKED', () => { setCookie(SESSION_COOKIE_NAME, 'first=tracked', DURATION) - startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) it('should be called with NOT_TRACKED', () => { setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) - startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) }) describe('session renewal', () => { it('should renew on activity after expiration', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -200,7 +168,7 @@ describe('startSessionManager', () => { }) it('should not renew on visibility after expiration', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -215,17 +183,17 @@ describe('startSessionManager', () => { describe('multiple startSessionManager calls', () => { it('should re-use the same session id', () => { - const firstSessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const idA = firstSessionManager.findActiveSession()!.id - const secondSessionManager = startSessionManager(COOKIE_OPTIONS, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManager(sessionStore, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const idB = secondSessionManager.findActiveSession()!.id expect(idA).toBe(idB) }) it('should not erase other session type', () => { - startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // schedule an expandOrRenewSession document.dispatchEvent(new CustomEvent('click')) @@ -235,7 +203,7 @@ describe('startSessionManager', () => { // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) - startSessionManager(COOKIE_OPTIONS, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager(sessionStore, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // cookie correctly set expect(getCookie(SESSION_COOKIE_NAME)).toContain('first') @@ -249,9 +217,9 @@ describe('startSessionManager', () => { }) it('should have independent tracking types', () => { - const firstSessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const secondSessionManager = startSessionManager( - COOKIE_OPTIONS, + sessionStore, SECOND_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -261,13 +229,13 @@ describe('startSessionManager', () => { }) it('should notify each expire and renew observables', () => { - const firstSessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionASpy = jasmine.createSpy() firstSessionManager.expireObservable.subscribe(expireSessionASpy) const renewSessionASpy = jasmine.createSpy() firstSessionManager.renewObservable.subscribe(renewSessionASpy) - const secondSessionManager = startSessionManager(COOKIE_OPTIONS, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManager(sessionStore, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionBSpy = jasmine.createSpy() secondSessionManager.expireObservable.subscribe(expireSessionBSpy) const renewSessionBSpy = jasmine.createSpy() @@ -289,7 +257,7 @@ describe('startSessionManager', () => { describe('session timeout', () => { it('should expire the session when the time out delay is reached', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -305,7 +273,7 @@ describe('startSessionManager', () => { it('should renew an existing timed out session', () => { setCookie(SESSION_COOKIE_NAME, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -317,7 +285,7 @@ describe('startSessionManager', () => { it('should not add created date to an existing session from an older versions', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcde&first=tracked', DURATION) - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expect(sessionManager.findActiveSession()!.id).toBe('abcde') expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('created=') @@ -334,7 +302,7 @@ describe('startSessionManager', () => { }) it('should expire the session after expiration delay', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -346,7 +314,7 @@ describe('startSessionManager', () => { }) it('should expand duration on activity', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -365,7 +333,7 @@ describe('startSessionManager', () => { }) it('should expand not tracked session duration on activity', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -386,7 +354,7 @@ describe('startSessionManager', () => { it('should expand session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -407,7 +375,7 @@ describe('startSessionManager', () => { it('should expand not tracked session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -428,7 +396,7 @@ describe('startSessionManager', () => { describe('manual session expiration', () => { it('expires the session when calling expire()', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -439,7 +407,7 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() multiple times', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -451,7 +419,7 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() after the session has been expired', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -463,7 +431,7 @@ describe('startSessionManager', () => { }) it('renew the session on user activity', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) clock.tick(COOKIE_ACCESS_DELAY) sessionManager.expire() @@ -476,21 +444,21 @@ describe('startSessionManager', () => { describe('session history', () => { it('should return undefined when there is no current session and no startTime', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expireSessionCookie() expect(sessionManager.findActiveSession()).toBeUndefined() }) it('should return the current session context when there is no start time', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expect(sessionManager.findActiveSession()!.id).toBeDefined() expect(sessionManager.findActiveSession()!.trackingType).toBeDefined() }) it('should return the session context corresponding to startTime', () => { - const sessionManager = startSessionManager(COOKIE_OPTIONS, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // 0s to 10s: first session clock.tick(10 * ONE_SECOND - COOKIE_ACCESS_DELAY) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 352ec6f8cc..4d8ba66f9e 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -1,4 +1,3 @@ -import type { CookieOptions } from '../../browser/cookie' import type { Observable } from '../../tools/observable' import type { Context } from '../../tools/serialisation/context' import { ValueHistory } from '../../tools/valueHistory' @@ -8,6 +7,7 @@ import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/ad import { clearInterval, setInterval } from '../../tools/timer' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { startSessionStoreManager } from './sessionStoreManager' +import type { SessionStore } from './sessionStore' export interface SessionManager { findActiveSession: (startTime?: RelativeTime) => SessionContext | undefined @@ -26,41 +26,41 @@ const SESSION_CONTEXT_TIMEOUT_DELAY = SESSION_TIME_OUT_DELAY let stopCallbacks: Array<() => void> = [] export function startSessionManager( - options: CookieOptions, + sessionStore: SessionStore, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionManager { - const sessionStore = startSessionStoreManager(options, productKey, computeSessionState) - stopCallbacks.push(() => sessionStore.stop()) + const sessionStoreManager = startSessionStoreManager(sessionStore, productKey, computeSessionState) + stopCallbacks.push(() => sessionStoreManager.stop()) const sessionContextHistory = new ValueHistory>(SESSION_CONTEXT_TIMEOUT_DELAY) stopCallbacks.push(() => sessionContextHistory.stop()) - sessionStore.renewObservable.subscribe(() => { + sessionStoreManager.renewObservable.subscribe(() => { sessionContextHistory.add(buildSessionContext(), relativeNow()) }) - sessionStore.expireObservable.subscribe(() => { + sessionStoreManager.expireObservable.subscribe(() => { sessionContextHistory.closeActive(relativeNow()) }) - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative) - trackActivity(() => sessionStore.expandOrRenewSession()) - trackVisibility(() => sessionStore.expandSession()) + trackActivity(() => sessionStoreManager.expandOrRenewSession()) + trackVisibility(() => sessionStoreManager.expandSession()) function buildSessionContext() { return { - id: sessionStore.getSession().id!, - trackingType: sessionStore.getSession()[productKey] as TrackingType, + id: sessionStoreManager.getSession().id!, + trackingType: sessionStoreManager.getSession()[productKey] as TrackingType, } } return { findActiveSession: (startTime) => sessionContextHistory.find(startTime), - renewObservable: sessionStore.renewObservable, - expireObservable: sessionStore.expireObservable, - expire: sessionStore.expire, + renewObservable: sessionStoreManager.renewObservable, + expireObservable: sessionStoreManager.expireObservable, + expire: sessionStoreManager.expire, } } diff --git a/packages/core/src/domain/session/sessionStoreManager.spec.ts b/packages/core/src/domain/session/sessionStoreManager.spec.ts index 53b6faf90d..d3b2a20e61 100644 --- a/packages/core/src/domain/session/sessionStoreManager.spec.ts +++ b/packages/core/src/domain/session/sessionStoreManager.spec.ts @@ -3,8 +3,8 @@ import { mockClock } from '../../../test' import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import type { SessionStoreManager } from './sessionStoreManager' -import { startSessionStoreManager } from './sessionStoreManager' -import { SESSION_COOKIE_NAME } from './sessionCookieStore' +import { initSessionStore, startSessionStoreManager } from './sessionStoreManager' +import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' const enum FakeTrackingType { @@ -47,10 +47,67 @@ function resetSessionInStore() { } describe('session store', () => { + describe('initSessionStore', () => { + it('should initialize storage when cookies are available', () => { + const sessionStore = initSessionStore(COOKIE_OPTIONS, false) + expect(sessionStore).toBeDefined() + }) + + it('should report false when cookies are not available, and fallback is not allowed', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const sessionStore = initSessionStore(COOKIE_OPTIONS, false) + expect(sessionStore).not.toBeDefined() + }) + + it('should fallback to localStorage and report true when cookies are not available', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const sessionStore = initSessionStore(COOKIE_OPTIONS, true) + expect(sessionStore).toBeDefined() + }) + + it('should report false when no storage is available', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') + const sessionStore = initSessionStore(COOKIE_OPTIONS, true) + expect(sessionStore).not.toBeDefined() + }) + + describe('cookie options', () => { + ;[ + { + cookieOptions: {}, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict$/, + description: 'should set same-site to strict by default', + }, + { + cookieOptions: { crossSite: true }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=none$/, + description: 'should set same site to none for crossSite', + }, + { + cookieOptions: { secure: true }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;secure$/, + description: 'should add secure attribute when defined', + }, + { + cookieOptions: { domain: 'foo.bar' }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;domain=foo\.bar$/, + description: 'should set cookie domain when defined', + }, + ].forEach(({ description, cookieOptions, cookieString }) => { + it(description, () => { + const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') + initCookieStore(cookieOptions) + expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) + }) + }) + }) + }) + describe('session lifecyle mechanism', () => { let expireSpy: () => void let renewSpy: () => void - let sessionStore: SessionStoreManager + let sessionStoreManager: SessionStoreManager let clock: Clock function setupSessionStore( @@ -62,9 +119,14 @@ describe('session store', () => { trackingType: FakeTrackingType.TRACKED, }) ) { - sessionStore = startSessionStoreManager(COOKIE_OPTIONS, PRODUCT_KEY, computeSessionState) - sessionStore.expireObservable.subscribe(expireSpy) - sessionStore.renewObservable.subscribe(renewSpy) + const sessionStore = initSessionStore(COOKIE_OPTIONS, false) + if (!sessionStore) { + fail('Unable to initialize cookie storage') + return + } + sessionStoreManager = startSessionStoreManager(sessionStore, PRODUCT_KEY, computeSessionState) + sessionStoreManager.expireObservable.subscribe(expireSpy) + sessionStoreManager.renewObservable.subscribe(renewSpy) } beforeEach(() => { @@ -76,7 +138,7 @@ describe('session store', () => { afterEach(() => { resetSessionInStore() clock.cleanup() - sessionStore.stop() + sessionStoreManager.stop() }) describe('expand or renew session', () => { @@ -86,9 +148,9 @@ describe('session store', () => { () => { setupSessionStore() - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeDefined() + expect(sessionStoreManager.getSession().id).toBeDefined() expectTrackedSessionToBeInStore() expect(expireSpy).not.toHaveBeenCalled() expect(renewSpy).toHaveBeenCalled() @@ -101,9 +163,9 @@ describe('session store', () => { () => { setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).not.toHaveBeenCalled() expect(renewSpy).not.toHaveBeenCalled() @@ -114,9 +176,9 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) expectTrackedSessionToBeInStore(FIRST_ID) expect(expireSpy).not.toHaveBeenCalled() expect(renewSpy).toHaveBeenCalled() @@ -130,9 +192,9 @@ describe('session store', () => { setupSessionStore() resetSessionInStore() - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - const sessionId = sessionStore.getSession().id + const sessionId = sessionStoreManager.getSession().id expect(sessionId).toBeDefined() expect(sessionId).not.toBe(FIRST_ID) expectTrackedSessionToBeInStore(sessionId) @@ -149,10 +211,10 @@ describe('session store', () => { setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) resetSessionInStore() - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeUndefined() - expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() expect(renewSpy).not.toHaveBeenCalled() @@ -167,10 +229,10 @@ describe('session store', () => { setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) resetSessionInStore() - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeUndefined() - expect(sessionStore.getSession()[PRODUCT_KEY]).toBeDefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() expect(renewSpy).not.toHaveBeenCalled() @@ -182,10 +244,10 @@ describe('session store', () => { setupSessionStore() clock.tick(10) - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) expectTrackedSessionToBeInStore(FIRST_ID) expect(expireSpy).not.toHaveBeenCalled() expect(renewSpy).not.toHaveBeenCalled() @@ -199,9 +261,9 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBe(SECOND_ID) + expect(sessionStoreManager.getSession().id).toBe(SECOND_ID) expectTrackedSessionToBeInStore(SECOND_ID) expect(expireSpy).toHaveBeenCalled() expect(renewSpy).toHaveBeenCalled() @@ -219,9 +281,9 @@ describe('session store', () => { })) setSessionInStore(FakeTrackingType.NOT_TRACKED, '') - sessionStore.expandOrRenewSession() + sessionStoreManager.expandOrRenewSession() - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expectNotTrackedSessionToBeInStore() expect(expireSpy).toHaveBeenCalled() expect(renewSpy).not.toHaveBeenCalled() @@ -233,9 +295,9 @@ describe('session store', () => { it('when session not in cache and session not in store, should do nothing', () => { setupSessionStore() - sessionStore.expandSession() + sessionStoreManager.expandSession() - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).not.toHaveBeenCalled() }) @@ -243,9 +305,9 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - sessionStore.expandSession() + sessionStoreManager.expandSession() - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).not.toHaveBeenCalled() }) @@ -254,9 +316,9 @@ describe('session store', () => { setupSessionStore() resetSessionInStore() - sessionStore.expandSession() + sessionStoreManager.expandSession() - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() }) @@ -265,10 +327,10 @@ describe('session store', () => { setupSessionStore() clock.tick(10) - sessionStore.expandSession() + sessionStoreManager.expandSession() - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) expect(expireSpy).not.toHaveBeenCalled() }) @@ -277,9 +339,9 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - sessionStore.expandSession() + sessionStoreManager.expandSession() - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expectTrackedSessionToBeInStore(SECOND_ID) expect(expireSpy).toHaveBeenCalled() }) @@ -291,7 +353,7 @@ describe('session store', () => { clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).not.toHaveBeenCalled() }) @@ -301,7 +363,7 @@ describe('session store', () => { clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).not.toHaveBeenCalled() }) @@ -312,7 +374,7 @@ describe('session store', () => { clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() }) @@ -323,8 +385,8 @@ describe('session store', () => { clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBe(FIRST_ID) - expect(sessionStore.getSession().expire).toBe(getStoreExpiration()) + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) expect(expireSpy).not.toHaveBeenCalled() }) @@ -335,7 +397,7 @@ describe('session store', () => { clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() }) @@ -346,7 +408,7 @@ describe('session store', () => { clock.tick(COOKIE_ACCESS_DELAY) - expect(sessionStore.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() }) }) diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts index 5e70f68476..2984da06d0 100644 --- a/packages/core/src/domain/session/sessionStoreManager.ts +++ b/packages/core/src/domain/session/sessionStoreManager.ts @@ -5,7 +5,8 @@ import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { initCookieStore } from './sessionCookieStore' -import type { SessionState, StoreInitOptions } from './sessionStore' +import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' +import { initLocalStorage } from './sessionLocalStorageStore' import { processSessionStoreOperations } from './sessionStoreOperations' export interface SessionStoreManager { @@ -20,6 +21,22 @@ export interface SessionStoreManager { const POLL_DELAY = ONE_SECOND +/** + * Checks if cookies are available as the preferred storage + * Else, checks if LocalStorage is allowed and available + */ +export function initSessionStore( + storageInitOptions: StoreInitOptions, + allowFallbackToLocalStorage: boolean +): SessionStore | undefined { + let sessionStore = initCookieStore(storageInitOptions) + + if (!sessionStore && allowFallbackToLocalStorage) { + sessionStore = initLocalStorage(storageInitOptions) + } + return sessionStore +} + /** * Different session concepts: * - tracked, the session has an id and is updated along the user navigation @@ -27,14 +44,13 @@ const POLL_DELAY = ONE_SECOND * - inactive, no session in store or session expired, waiting for a renew session */ export function startSessionStoreManager( - options: StoreInitOptions, + sessionStore: SessionStore, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionStoreManager { const renewObservable = new Observable() const expireObservable = new Observable() - const sessionStore = initCookieStore(options) const { clearSession, retrieveSession } = sessionStore const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 1b1f1612e8..386475ceff 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -21,7 +21,7 @@ describe('process operations mechanism', () => { let cookieStorage: SessionStore beforeEach(() => { - cookieStorage = initCookieStore(COOKIE_OPTIONS) + cookieStorage = initCookieStore(COOKIE_OPTIONS)! initialSession = { id: '123', created: '0' } otherSession = { id: '456', created: '100' } processSpy = jasmine.createSpy('process') diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1e69af120c..b189b5d039 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -42,6 +42,7 @@ export { } from './domain/telemetry' export { monitored, monitor, callMonitored, setDebugMode } from './tools/monitor' export { Observable, Subscription } from './tools/observable' +export { initSessionStore } from './domain/session/sessionStoreManager' export { startSessionManager, SessionManager, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 5a4ef0c402..53154a1b41 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -4,7 +4,6 @@ import { createPageExitObservable, TelemetryService, willSyntheticsInjectRum, - areCookiesAuthorized, canUseEventBridge, getEventBridge, startTelemetry, @@ -55,7 +54,7 @@ export function startLogs( const pageExitObservable = createPageExitObservable() const session = - areCookiesAuthorized(configuration.cookieOptions) && !canUseEventBridge() && !willSyntheticsInjectRum() + configuration.sessionStore && !canUseEventBridge() && !willSyntheticsInjectRum() ? startLogsSessionManager(configuration) : startLogsSessionManagerStub(configuration) diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 14d51be950..3b5aee014b 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -1,5 +1,6 @@ import type { RelativeTime } from '@datadog/browser-core' import { + initSessionStore, COOKIE_ACCESS_DELAY, getCookie, SESSION_COOKIE_NAME, @@ -20,7 +21,10 @@ import { describe('logs session manager', () => { const DURATION = 123456 - const configuration: Partial = { sessionSampleRate: 0.5 } + const configuration: Partial = { + sessionSampleRate: 0.5, + sessionStore: initSessionStore({}, false), + } let clock: Clock let tracked: boolean diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index 9293e46f26..57c67ec70c 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -19,7 +19,11 @@ export const enum LoggerTrackingType { } export function startLogsSessionManager(configuration: LogsConfiguration): LogsSessionManager { - const sessionManager = startSessionManager(configuration.cookieOptions, LOGS_SESSION_KEY, (rawTrackingType) => + if (!configuration.sessionStore) { + throw new Error('Cannot initialize Logs Session Manager without a storage.') + } + + const sessionManager = startSessionManager(configuration.sessionStore, LOGS_SESSION_KEY, (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) return { diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 584e8341fb..bda5a5d9f2 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -5,7 +5,6 @@ import { willSyntheticsInjectRum, assign, BoundedBuffer, - buildCookieOptions, createContextManager, deepClone, makePublicApi, @@ -16,7 +15,6 @@ import { callMonitored, createHandlingStack, canUseEventBridge, - areCookiesAuthorized, checkUser, sanitizeUser, sanitize, @@ -110,10 +108,9 @@ export function makeRumPublicApi( return } - if (canUseEventBridge()) { + const eventBridgeAvailable = canUseEventBridge() + if (eventBridgeAvailable) { initConfiguration = overrideInitConfigurationForBridge(initConfiguration) - } else if (!canHandleSession(initConfiguration)) { - return } if (!canInitRum(initConfiguration)) { @@ -125,6 +122,16 @@ export function makeRumPublicApi( return } + if (!eventBridgeAvailable && !configuration.sessionStore) { + if (configuration.allowFallbackToLocalStorage) { + display.warn('No storage available for session. We will not send any data.') + } else { + // Keep until V5 to avoid breaking changes + display.warn('Cookies are not authorized, we will not send any data.') + } + return + } + if (!configuration.trackViewsManually) { doStartRum(initConfiguration, configuration) } else { @@ -273,19 +280,6 @@ export function makeRumPublicApi( return rumPublicApi - function canHandleSession(initConfiguration: RumInitConfiguration): boolean { - if (!areCookiesAuthorized(buildCookieOptions(initConfiguration))) { - display.warn('Cookies are not authorized, we will not send any data.') - return false - } - - if (isLocalFile()) { - display.error('Execution is not allowed in the current context.') - return false - } - return true - } - function canInitRum(initConfiguration: RumInitConfiguration) { if (isAlreadyInitialized) { if (!initConfiguration.silentMultipleInit) { @@ -303,8 +297,4 @@ export function makeRumPublicApi( sessionSampleRate: 100, }) } - - function isLocalFile() { - return window.location.protocol === 'file:' - } } diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index e78c294833..527e55c322 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -35,7 +35,11 @@ export const enum RumTrackingType { } export function startRumSessionManager(configuration: RumConfiguration, lifeCycle: LifeCycle): RumSessionManager { - const sessionManager = startSessionManager(configuration.cookieOptions, RUM_SESSION_KEY, (rawTrackingType) => + if (!configuration.sessionStore) { + throw new Error('Cannot initialize RUM Session Manager without a storage.') + } + + const sessionManager = startSessionManager(configuration.sessionStore, RUM_SESSION_KEY, (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) From 7b718f19963faa1512a21215e83b7388f0291b46 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 19 May 2023 16:41:16 +0200 Subject: [PATCH 21/40] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20localStor?= =?UTF-8?q?age=20in=20Session=20Store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/sessionLocalStorageStore.spec.ts | 2 +- .../session/sessionStoreOperations.spec.ts | 433 +++++++++--------- packages/core/test/emulate/cookie.ts | 13 - packages/core/test/emulate/stubStorages.ts | 33 ++ packages/core/test/index.ts | 2 +- 5 files changed, 261 insertions(+), 222 deletions(-) delete mode 100644 packages/core/test/emulate/cookie.ts create mode 100644 packages/core/test/emulate/stubStorages.ts diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts index 8d33f6e450..9b40be2f85 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts @@ -14,7 +14,7 @@ describe('session local storage store', () => { }) it('should report local storage as not available', () => { - spyOn(localStorage, 'getItem').and.throwError('Unavailable') + spyOn(Storage.prototype, 'getItem').and.throwError('Unavailable') const localStorageStore = initLocalStorage({}) expect(localStorageStore).not.toBeDefined() }) diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 386475ceff..456fc97884 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -1,8 +1,10 @@ -import { stubCookie, mockClock } from '../../../test' -import { isChromium } from '../../tools/utils/browserDetection' +import type { StubStorage } from '../../../test' +import { mockClock, stubCookieProvider, stubLocalStorageProvider } from '../../../test' +import type { CookieOptions } from '../../browser/cookie' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { initCookieStore, SESSION_COOKIE_NAME } from './sessionCookieStore' -import type { SessionState, SessionStore } from './sessionStore' +import { initLocalStorage, LOCAL_STORAGE_KEY } from './sessionLocalStorageStore' +import type { SessionState } from './sessionStore' import { toSessionString } from './sessionStore' import { processSessionStoreOperations, @@ -11,251 +13,268 @@ import { LOCK_RETRY_DELAY, } from './sessionStoreOperations' -describe('process operations mechanism', () => { - const COOKIE_OPTIONS = {} - let initialSession: SessionState - let otherSession: SessionState - let processSpy: jasmine.Spy - let afterSpy: jasmine.Spy - let cookie: ReturnType - let cookieStorage: SessionStore - - beforeEach(() => { - cookieStorage = initCookieStore(COOKIE_OPTIONS)! - initialSession = { id: '123', created: '0' } - otherSession = { id: '456', created: '100' } - processSpy = jasmine.createSpy('process') - afterSpy = jasmine.createSpy('after') - cookie = stubCookie() - }) +const COOKIE_OPTIONS: CookieOptions = {} + +;( + [ + { + title: 'Cookie Storage', + sessionStore: initCookieStore(COOKIE_OPTIONS)!, + stubStorageProvider: stubCookieProvider, + storageKey: SESSION_COOKIE_NAME, + }, + { + title: 'Local Storage', + sessionStore: initLocalStorage({})!, + stubStorageProvider: stubLocalStorageProvider, + storageKey: LOCAL_STORAGE_KEY, + }, + ] as const +).forEach(({ title, sessionStore, stubStorageProvider, storageKey }) => { + describe(`process operations mechanism with ${title}`, () => { + let initialSession: SessionState + let otherSession: SessionState + let processSpy: jasmine.Spy + let afterSpy: jasmine.Spy + let stubStorage: StubStorage - describe('with cookie-lock disabled', () => { beforeEach(() => { - isChromium() && pending('cookie-lock only disabled on non chromium browsers') + sessionStore.clearSession() + initialSession = { id: '123', created: '0' } + otherSession = { id: '456', created: '100' } + processSpy = jasmine.createSpy('process') + afterSpy = jasmine.createSpy('after') + stubStorage = stubStorageProvider.get() }) - it('should persist session when process returns a value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({ ...otherSession }) + describe('with lock access disabled', () => { + beforeEach(() => { + isLockEnabled() && pending('lock-access required') + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + it('should persist session when process returns a value', () => { + sessionStore.persistSession(initialSession) + processSpy.and.returnValue({ ...otherSession }) - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - it('should clear session when process return an empty value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({}) + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + it('should clear session when process returns an empty value', () => { + sessionStore.persistSession(initialSession) + processSpy.and.returnValue({}) - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = {} - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - it('should not persist session when process return undefined', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue(undefined) + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = {} + expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + it('should not persist session when process returns undefined', () => { + sessionStore.persistSession(initialSession) + processSpy.and.returnValue(undefined) - expect(processSpy).toHaveBeenCalledWith(initialSession) - expect(cookieStorage.retrieveSession()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - it('LOCK_MAX_TRIES value should not influence the behavior when lock mechanism is not enabled', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({ ...otherSession }) + expect(processSpy).toHaveBeenCalledWith(initialSession) + expect(sessionStore.retrieveSession()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage, LOCK_MAX_TRIES) + it('LOCK_MAX_TRIES value should not influence the behavior when lock mechanism is not enabled', () => { + sessionStore.persistSession(initialSession) + processSpy.and.returnValue({ ...otherSession }) - expect(processSpy).toHaveBeenCalledWith(initialSession) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore, LOCK_MAX_TRIES) - describe('with cookie-lock enabled', () => { - beforeEach(() => { - !isChromium() && pending('cookie-lock only enabled on chromium browsers') + expect(processSpy).toHaveBeenCalledWith(initialSession) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) }) - it('should persist session when process return a value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) + describe('with lock access enabled', () => { + beforeEach(() => { + !isLockEnabled() && pending('lock-access not enabled') + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + it('should persist session when process returns a value', () => { + sessionStore.persistSession(initialSession) + processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - it('should clear session when process return an empty value', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue({}) + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = { ...otherSession, expire: jasmine.any(String) } + expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + it('should clear session when process returns an empty value', () => { + sessionStore.persistSession(initialSession) + processSpy.and.returnValue({}) - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - const expectedSession = {} - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSpy).toHaveBeenCalledWith(expectedSession) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - it('should not persist session when process return undefined', () => { - cookieStorage.persistSession(initialSession) - processSpy.and.returnValue(undefined) + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + const expectedSession = {} + expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(afterSpy).toHaveBeenCalledWith(expectedSession) + }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + it('should not persist session when process returns undefined', () => { + sessionStore.persistSession(initialSession) + processSpy.and.returnValue(undefined) - expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - expect(cookieStorage.retrieveSession()).toEqual(initialSession) - expect(afterSpy).toHaveBeenCalledWith(initialSession) - }) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } - - function lockScenario({ - onInitialLockCheck, - onAcquiredLockCheck, - onPostProcessLockCheck, - onPostPersistLockCheck, - }: { - onInitialLockCheck?: OnLockCheck - onAcquiredLockCheck?: OnLockCheck - onPostProcessLockCheck?: OnLockCheck - onPostPersistLockCheck?: OnLockCheck - }) { - const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] - cookie.getSpy.and.callFake(() => { - const currentOnLockCheck = onLockChecks.shift() - if (!currentOnLockCheck) { - return cookie.currentValue() - } - const { currentState, retryState } = currentOnLockCheck() - cookie.setCurrentValue(buildSessionString(retryState)) - return buildSessionString(currentState) + expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) + expect(sessionStore.retrieveSession()).toEqual(initialSession) + expect(afterSpy).toHaveBeenCalledWith(initialSession) }) - } - - function buildSessionString(currentState: SessionState) { - return `${SESSION_COOKIE_NAME}=${toSessionString(currentState)}` - } - - ;[ - { - description: 'should wait for lock to be free', - lockConflict: 'onInitialLockCheck', - }, - { - description: 'should retry if lock was acquired before process', - lockConflict: 'onAcquiredLockCheck', - }, - { - description: 'should retry if lock was acquired after process', - lockConflict: 'onPostProcessLockCheck', - }, - { - description: 'should retry if lock was acquired after persist', - lockConflict: 'onPostPersistLockCheck', - }, - ].forEach(({ description, lockConflict }) => { - it(description, (done) => { - lockScenario({ - [lockConflict]: () => ({ - currentState: { ...initialSession, lock: 'locked' }, - retryState: { ...initialSession, other: 'other' }, - }), + + type OnLockCheck = () => { currentState: SessionState; retryState: SessionState } + + function lockScenario({ + onInitialLockCheck, + onAcquiredLockCheck, + onPostProcessLockCheck, + onPostPersistLockCheck, + }: { + onInitialLockCheck?: OnLockCheck + onAcquiredLockCheck?: OnLockCheck + onPostProcessLockCheck?: OnLockCheck + onPostPersistLockCheck?: OnLockCheck + }) { + const onLockChecks = [onInitialLockCheck, onAcquiredLockCheck, onPostProcessLockCheck, onPostPersistLockCheck] + stubStorage.getSpy.and.callFake(() => { + const currentOnLockCheck = onLockChecks.shift() + if (!currentOnLockCheck) { + return stubStorage.currentValue(storageKey) + } + const { currentState, retryState } = currentOnLockCheck() + stubStorage.setCurrentValue(storageKey, toSessionString(retryState)) + return buildSessionString(currentState) }) - initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) - cookieStorage.persistSession(initialSession) - processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) + } - processSessionStoreOperations( - { - process: processSpy, - after: (afterSession) => { - // session with 'other' value on process - expect(processSpy).toHaveBeenCalledWith({ - ...initialSession, - other: 'other', - lock: jasmine.any(String), - expire: jasmine.any(String), - }) - - // end state with session 'other' and 'processed' value - const expectedSession = { - ...initialSession, - other: 'other', - processed: 'processed', - expire: jasmine.any(String), - } - expect(cookieStorage.retrieveSession()).toEqual(expectedSession) - expect(afterSession).toEqual(expectedSession) - done() + function buildSessionString(currentState: SessionState) { + return `${storageKey}=${toSessionString(currentState)}` + } + + ;[ + { + description: 'should wait for lock to be free', + lockConflict: 'onInitialLockCheck', + }, + { + description: 'should retry if lock was acquired before process', + lockConflict: 'onAcquiredLockCheck', + }, + { + description: 'should retry if lock was acquired after process', + lockConflict: 'onPostProcessLockCheck', + }, + { + description: 'should retry if lock was acquired after persist', + lockConflict: 'onPostPersistLockCheck', + }, + ].forEach(({ description, lockConflict }) => { + it(description, (done) => { + lockScenario({ + [lockConflict]: () => ({ + currentState: { ...initialSession, lock: 'locked' }, + retryState: { ...initialSession, other: 'other' }, + }), + }) + initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) + sessionStore.persistSession(initialSession) + processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) + + processSessionStoreOperations( + { + process: processSpy, + after: (afterSession) => { + // session with 'other' value on process + expect(processSpy).toHaveBeenCalledWith({ + ...initialSession, + other: 'other', + lock: jasmine.any(String), + expire: jasmine.any(String), + }) + + // end state with session 'other' and 'processed' value + const expectedSession = { + ...initialSession, + other: 'other', + processed: 'processed', + expire: jasmine.any(String), + } + expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(afterSession).toEqual(expectedSession) + done() + }, }, - }, - cookieStorage - ) + sessionStore + ) + }) }) - }) - - it('should abort after a max number of retry', () => { - const clock = mockClock() - cookieStorage.persistSession(initialSession) - cookie.setSpy.calls.reset() + it('should abort after a max number of retry', () => { + const clock = mockClock() - cookie.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, cookieStorage) + sessionStore.persistSession(initialSession) + stubStorage.setSpy.calls.reset() - const lockMaxTries = isLockEnabled() ? LOCK_MAX_TRIES : 0 - const lockRetryDelay = isLockEnabled() ? LOCK_RETRY_DELAY : 0 + stubStorage.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) - clock.tick(lockMaxTries * lockRetryDelay) - expect(processSpy).not.toHaveBeenCalled() - expect(afterSpy).not.toHaveBeenCalled() - expect(cookie.setSpy).not.toHaveBeenCalled() + const lockMaxTries = isLockEnabled() ? LOCK_MAX_TRIES : 0 + const lockRetryDelay = isLockEnabled() ? LOCK_RETRY_DELAY : 0 - clock.cleanup() - }) + clock.tick(lockMaxTries * lockRetryDelay) + expect(processSpy).not.toHaveBeenCalled() + expect(afterSpy).not.toHaveBeenCalled() + expect(stubStorage.setSpy).not.toHaveBeenCalled() - it('should execute cookie accesses in order', (done) => { - lockScenario({ - onInitialLockCheck: () => ({ - currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later - retryState: initialSession, - }), + clock.cleanup() }) - cookieStorage.persistSession(initialSession) - processSessionStoreOperations( - { - process: (session) => ({ ...session, value: 'foo' }), - after: afterSpy, - }, - cookieStorage - ) - processSessionStoreOperations( - { - process: (session) => ({ ...session, value: `${session.value || ''}bar` }), - after: (session) => { - expect(session.value).toBe('foobar') - expect(afterSpy).toHaveBeenCalled() - done() + it('should execute cookie accesses in order', (done) => { + lockScenario({ + onInitialLockCheck: () => ({ + currentState: { ...initialSession, lock: 'locked' }, // force to retry the first access later + retryState: initialSession, + }), + }) + sessionStore.persistSession(initialSession) + + processSessionStoreOperations( + { + process: (session) => ({ ...session, value: 'foo' }), + after: afterSpy, }, - }, - cookieStorage - ) + sessionStore + ) + processSessionStoreOperations( + { + process: (session) => ({ ...session, value: `${session.value || ''}bar` }), + after: (session) => { + expect(session.value).toBe('foobar') + expect(afterSpy).toHaveBeenCalled() + done() + }, + }, + sessionStore + ) + }) }) }) }) diff --git a/packages/core/test/emulate/cookie.ts b/packages/core/test/emulate/cookie.ts deleted file mode 100644 index a053a560c8..0000000000 --- a/packages/core/test/emulate/cookie.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function stubCookie() { - let cookie = '' - return { - getSpy: spyOnProperty(Document.prototype, 'cookie', 'get').and.callFake(() => cookie), - setSpy: spyOnProperty(Document.prototype, 'cookie', 'set').and.callFake((newCookie) => { - cookie = newCookie - }), - currentValue: () => cookie, - setCurrentValue: (newCookie: string) => { - cookie = newCookie - }, - } -} diff --git a/packages/core/test/emulate/stubStorages.ts b/packages/core/test/emulate/stubStorages.ts new file mode 100644 index 0000000000..a0e030e3ca --- /dev/null +++ b/packages/core/test/emulate/stubStorages.ts @@ -0,0 +1,33 @@ +export interface StubStorage { + getSpy: jasmine.Spy + setSpy: jasmine.Spy + currentValue: (key: string) => string + setCurrentValue: (key: string, value: string) => void +} + +export const stubCookieProvider = { + get: (): StubStorage => { + let cookie = '' + return { + getSpy: spyOnProperty(Document.prototype, 'cookie', 'get').and.callFake(() => cookie), + setSpy: spyOnProperty(Document.prototype, 'cookie', 'set').and.callFake((newCookie) => (cookie = newCookie)), + currentValue: () => cookie, + setCurrentValue: (key, newCookie: string) => (cookie = `${key}=${newCookie}`), + } + }, +} + +export const stubLocalStorageProvider = { + get: (): StubStorage => { + const store: Record = {} + spyOn(Storage.prototype, 'removeItem').and.callFake((key) => { + delete store[key] + }) + return { + getSpy: spyOn(Storage.prototype, 'getItem').and.callFake((key) => store[key] || null), + setSpy: spyOn(Storage.prototype, 'setItem').and.callFake((key, newValue) => (store[key] = newValue)), + currentValue: (key) => store[key], + setCurrentValue: (key, newValue: string) => (store[key] = newValue), + } + }, +} diff --git a/packages/core/test/index.ts b/packages/core/test/index.ts index 452e6062c6..4e37843f2b 100644 --- a/packages/core/test/index.ts +++ b/packages/core/test/index.ts @@ -14,5 +14,5 @@ export * from './emulate/navigatorOnLine' export * from './emulate/eventBridge' export * from './emulate/eventBridge' export * from './emulate/windowOnError' -export * from './emulate/cookie' +export * from './emulate/stubStorages' export * from './emulate/mockFlushController' From fbc6d791ce8d3b66ef4f5941c61ec240c86638f5 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 1 Jun 2023 23:19:34 +0200 Subject: [PATCH 22/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Remove=20StoreIni?= =?UTF-8?q?tOptions=20and=20propagate=20initConfiguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/configuration/configuration.ts | 29 ++------ .../core/src/domain/configuration/index.ts | 1 - .../session/oldCookiesMigration.spec.ts | 2 +- .../domain/session/sessionCookieStore.spec.ts | 67 +++++++++++++++++-- .../src/domain/session/sessionCookieStore.ts | 20 +++++- .../session/sessionLocalStorageStore.spec.ts | 12 ++-- .../session/sessionLocalStorageStore.ts | 4 +- .../src/domain/session/sessionManager.spec.ts | 6 +- .../core/src/domain/session/sessionStore.ts | 3 - .../session/sessionStoreManager.spec.ts | 46 ++----------- .../src/domain/session/sessionStoreManager.ts | 16 ++--- .../session/sessionStoreOperations.spec.ts | 8 +-- packages/core/src/index.ts | 1 - .../src/domain/logsSessionManager.spec.ts | 2 +- packages/rum-core/src/boot/rumPublicApi.ts | 7 +- 15 files changed, 116 insertions(+), 108 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 2791cec7d6..3963b32ca4 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -1,5 +1,3 @@ -import type { CookieOptions } from '../../browser/cookie' -import { getCurrentSite } from '../../browser/cookie' import { catchUserErrors } from '../../tools/catchUserErrors' import { display } from '../../tools/display' import type { RawTelemetryConfiguration } from '../telemetry' @@ -14,6 +12,8 @@ import { initSessionStore } from '../session/sessionStoreManager' import type { SessionStore } from '../session/sessionStore' import type { TransportConfiguration } from './transportConfiguration' import { computeTransportConfiguration } from './transportConfiguration' +import { CookieOptions } from '../../browser/cookie' +import { buildCookieOptions } from '../session/sessionCookieStore' export const DefaultPrivacyLevel = { ALLOW: 'allow', @@ -123,10 +123,6 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati return } - const cookieOptions = buildCookieOptions(initConfiguration) - const allowFallbackToLocalStorage = initConfiguration.allowFallbackToLocalStorage || false - const sessionStore = initSessionStore(cookieOptions, allowFallbackToLocalStorage) - // Set the experimental feature flags as early as possible, so we can use them in most places if (Array.isArray(initConfiguration.enableExperimentalFeatures)) { addExperimentalFeatures( @@ -141,8 +137,8 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), cookieOptions: buildCookieOptions(initConfiguration), - allowFallbackToLocalStorage, - sessionStore, + allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, + sessionStore: initSessionStore(initConfiguration), sessionSampleRate: sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, @@ -174,23 +170,6 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati ) } -export function buildCookieOptions(initConfiguration: InitConfiguration) { - const cookieOptions: CookieOptions = {} - - cookieOptions.secure = mustUseSecureCookie(initConfiguration) - cookieOptions.crossSite = !!initConfiguration.useCrossSiteSessionCookie - - if (initConfiguration.trackSessionAcrossSubdomains) { - cookieOptions.domain = getCurrentSite() - } - - return cookieOptions -} - -function mustUseSecureCookie(initConfiguration: InitConfiguration) { - return !!initConfiguration.useSecureSessionCookie || !!initConfiguration.useCrossSiteSessionCookie -} - export function serializeConfiguration(configuration: InitConfiguration): Partial { const proxy = configuration.proxy ?? configuration.proxyUrl return { diff --git a/packages/core/src/domain/configuration/index.ts b/packages/core/src/domain/configuration/index.ts index eff00051cf..3ebfe3d4cd 100644 --- a/packages/core/src/domain/configuration/index.ts +++ b/packages/core/src/domain/configuration/index.ts @@ -1,7 +1,6 @@ export { Configuration, InitConfiguration, - buildCookieOptions, DefaultPrivacyLevel, validateAndBuildConfiguration, serializeConfiguration, diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index 0ee10ab043..6caddedc7a 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -9,7 +9,7 @@ import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' describe('old cookies migration', () => { - const sessionStore = initCookieStore({})! + const sessionStore = initCookieStore({ clientToken: '123' })! it('should not touch current cookie', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/sessionCookieStore.spec.ts index d8e14158a9..765ca55a72 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/sessionCookieStore.spec.ts @@ -1,16 +1,16 @@ -import type { CookieOptions } from '../../browser/cookie' -import { getCookie, setCookie, deleteCookie } from '../../browser/cookie' -import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' +import { setCookie, deleteCookie, getCurrentSite, getCookie } from '../../browser/cookie' +import type { InitConfiguration } from '../configuration' +import { SESSION_COOKIE_NAME, buildCookieOptions, initCookieStore } from './sessionCookieStore' import type { SessionState, SessionStore } from './sessionStore' describe('session cookie store', () => { const sessionState: SessionState = { id: '123', created: '0' } - const noOptions: CookieOptions = {} + const initConfiguration: InitConfiguration = { clientToken: 'abc' } let cookieStorage: SessionStore | undefined beforeEach(() => { - cookieStorage = initCookieStore(noOptions) + cookieStorage = initCookieStore(initConfiguration) }) afterEach(() => { @@ -37,4 +37,61 @@ describe('session cookie store', () => { const session = cookieStorage?.retrieveSession() expect(session).toEqual({}) }) + + describe('build cookie options', () => { + const clientToken = 'abc' + + it('should not be secure nor crossSite by default', () => { + const cookieOptions = buildCookieOptions({ clientToken })! + expect(cookieOptions).toEqual({ secure: false, crossSite: false }) + }) + + it('should be secure when `useSecureSessionCookie` is truthy', () => { + const cookieOptions = buildCookieOptions({ clientToken, useSecureSessionCookie: true })! + expect(cookieOptions).toEqual({ secure: true, crossSite: false }) + }) + + it('should be secure and crossSite when `useCrossSiteSessionCookie` is truthy', () => { + const cookieOptions = buildCookieOptions({ clientToken, useCrossSiteSessionCookie: true })! + expect(cookieOptions).toEqual({ secure: true, crossSite: true }) + }) + + it('should have domain when `trackSessionAcrossSubdomains` is truthy', () => { + const cookieOptions = buildCookieOptions({ clientToken, trackSessionAcrossSubdomains: true })! + expect(cookieOptions).toEqual({ secure: false, crossSite: false, domain: jasmine.any(String) }) + }) + }) + + describe('cookie options', () => { + ;[ + { + initConfiguration: { clientToken: 'abc' }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict$/, + description: 'should set samesite to strict by default', + }, + { + initConfiguration: { clientToken: 'abc', useCrossSiteSessionCookie: true }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=none;secure$/, + description: 'should set samesite to none and secure to true for crossSite', + }, + { + initConfiguration: { clientToken: 'abc', useSecureSessionCookie: true }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;secure$/, + description: 'should add secure attribute when defined', + }, + { + initConfiguration: { clientToken: 'abc', trackSessionAcrossSubdomains: true }, + cookieString: new RegExp( + `^dd_cookie_test_[\\w-]+=[^;]*;expires=[^;]+;path=\\/;samesite=strict;domain=${getCurrentSite()}$` + ), + description: 'should set cookie domain when tracking accross subdomains', + }, + ].forEach(({ description, initConfiguration, cookieString }) => { + it(description, () => { + const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') + initCookieStore(initConfiguration) + expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) + }) + }) + }) }) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/sessionCookieStore.ts index a5e7989042..70e6f7376d 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/sessionCookieStore.ts @@ -1,5 +1,6 @@ import type { CookieOptions } from '../../browser/cookie' -import { areCookiesAuthorized, deleteCookie, getCookie, setCookie } from '../../browser/cookie' +import { getCurrentSite, areCookiesAuthorized, deleteCookie, getCookie, setCookie } from '../../browser/cookie' +import type { InitConfiguration } from '../configuration' import { tryOldCookiesMigration } from './oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState, SessionStore } from './sessionStore' @@ -7,7 +8,9 @@ import { toSessionState, toSessionString } from './sessionStore' export const SESSION_COOKIE_NAME = '_dd_s' -export function initCookieStore(options: CookieOptions): SessionStore | undefined { +export function initCookieStore(initConfiguration: InitConfiguration): SessionStore | undefined { + const options = buildCookieOptions(initConfiguration) + if (!areCookiesAuthorized(options)) { return undefined } @@ -39,3 +42,16 @@ function deleteSessionCookie(options: CookieOptions) { deleteCookie(SESSION_COOKIE_NAME, options) } } + +export function buildCookieOptions(initConfiguration: InitConfiguration) { + const cookieOptions: CookieOptions = {} + + cookieOptions.secure = !!initConfiguration.useSecureSessionCookie || !!initConfiguration.useCrossSiteSessionCookie + cookieOptions.crossSite = !!initConfiguration.useCrossSiteSessionCookie + + if (initConfiguration.trackSessionAcrossSubdomains) { + cookieOptions.domain = getCurrentSite() + } + + return cookieOptions +} diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts index 9b40be2f85..cf8159f7a4 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts @@ -9,18 +9,18 @@ describe('session local storage store', () => { }) it('should report local storage as available', () => { - const localStorageStore = initLocalStorage({}) + const localStorageStore = initLocalStorage() expect(localStorageStore).toBeDefined() }) it('should report local storage as not available', () => { spyOn(Storage.prototype, 'getItem').and.throwError('Unavailable') - const localStorageStore = initLocalStorage({}) + const localStorageStore = initLocalStorage() expect(localStorageStore).not.toBeDefined() }) it('should persist a session in local storage', () => { - const localStorageStore = initLocalStorage({}) + const localStorageStore = initLocalStorage() localStorageStore?.persistSession(sessionState) const session = localStorageStore?.retrieveSession() expect(session).toEqual({ ...sessionState }) @@ -28,7 +28,7 @@ describe('session local storage store', () => { }) it('should delete the local storage item holding the session', () => { - const localStorageStore = initLocalStorage({}) + const localStorageStore = initLocalStorage() localStorageStore?.persistSession(sessionState) localStorageStore?.clearSession() const session = localStorageStore?.retrieveSession() @@ -38,7 +38,7 @@ describe('session local storage store', () => { it('should not interfere with other keys present in local storage', () => { window.localStorage.setItem('test', 'hello') - const localStorageStore = initLocalStorage({}) + const localStorageStore = initLocalStorage() localStorageStore?.persistSession(sessionState) localStorageStore?.retrieveSession() localStorageStore?.clearSession() @@ -46,7 +46,7 @@ describe('session local storage store', () => { }) it('should return an empty object if session string is invalid', () => { - const localStorageStore = initLocalStorage({}) + const localStorageStore = initLocalStorage() localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') const session = localStorageStore?.retrieveSession() expect(session).toEqual({}) diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.ts b/packages/core/src/domain/session/sessionLocalStorageStore.ts index 6a90772c72..cfebc29b04 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.ts +++ b/packages/core/src/domain/session/sessionLocalStorageStore.ts @@ -1,10 +1,10 @@ import { generateUUID } from '../../tools/utils/stringUtils' -import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' +import type { SessionState, SessionStore } from './sessionStore' import { toSessionString, toSessionState } from './sessionStore' export const LOCAL_STORAGE_KEY = '_dd_s' -export function initLocalStorage(_options: StoreInitOptions): SessionStore | undefined { +export function initLocalStorage(): SessionStore | undefined { if (!isLocalStorageAvailable()) { return undefined } diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 39735adf38..5102509f31 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -1,11 +1,11 @@ import { createNewEvent, mockClock, restorePageVisibility, setPageVisibility } from '../../../test' import type { Clock } from '../../../test' -import type { CookieOptions } from '../../browser/cookie' import { COOKIE_ACCESS_DELAY, getCookie, setCookie } from '../../browser/cookie' import type { RelativeTime } from '../../tools/utils/timeUtils' import { isIE } from '../../tools/utils/browserDetection' import { DOM_EVENT } from '../../browser/addEventListener' import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' +import type { InitConfiguration } from '../configuration' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' @@ -30,8 +30,8 @@ describe('startSessionManager', () => { const DURATION = 123456 const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' - const COOKIE_OPTIONS: CookieOptions = {} - const sessionStore = initCookieStore(COOKIE_OPTIONS)! + const initConfiguration: InitConfiguration = { clientToken: 'abc' } + const sessionStore = initCookieStore(initConfiguration)! let clock: Clock function expireSessionCookie() { diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 185e4e48a8..0f3dac1fd8 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -1,4 +1,3 @@ -import type { CookieOptions } from '../../browser/cookie' import { isEmptyObject } from '../../tools/utils/objectUtils' import { objectEntries } from '../../tools/utils/polyfills' @@ -14,8 +13,6 @@ export interface SessionState { [key: string]: string | undefined } -export type StoreInitOptions = CookieOptions - export interface SessionStore { persistSession: (session: SessionState) => void retrieveSession: () => SessionState diff --git a/packages/core/src/domain/session/sessionStoreManager.spec.ts b/packages/core/src/domain/session/sessionStoreManager.spec.ts index d3b2a20e61..c31abea544 100644 --- a/packages/core/src/domain/session/sessionStoreManager.spec.ts +++ b/packages/core/src/domain/session/sessionStoreManager.spec.ts @@ -1,10 +1,9 @@ import type { Clock } from '../../../test' import { mockClock } from '../../../test' -import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import type { SessionStoreManager } from './sessionStoreManager' import { initSessionStore, startSessionStoreManager } from './sessionStoreManager' -import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' +import { SESSION_COOKIE_NAME } from './sessionCookieStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' const enum FakeTrackingType { @@ -16,7 +15,7 @@ const DURATION = 123456 const PRODUCT_KEY = 'product' const FIRST_ID = 'first' const SECOND_ID = 'second' -const COOKIE_OPTIONS: CookieOptions = {} +const clientToken = 'abc' function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { setCookie( @@ -49,59 +48,28 @@ function resetSessionInStore() { describe('session store', () => { describe('initSessionStore', () => { it('should initialize storage when cookies are available', () => { - const sessionStore = initSessionStore(COOKIE_OPTIONS, false) + const sessionStore = initSessionStore({ clientToken }) expect(sessionStore).toBeDefined() }) it('should report false when cookies are not available, and fallback is not allowed', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStore = initSessionStore(COOKIE_OPTIONS, false) + const sessionStore = initSessionStore({ clientToken, allowFallbackToLocalStorage: false }) expect(sessionStore).not.toBeDefined() }) it('should fallback to localStorage and report true when cookies are not available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStore = initSessionStore(COOKIE_OPTIONS, true) + const sessionStore = initSessionStore({ clientToken, allowFallbackToLocalStorage: true }) expect(sessionStore).toBeDefined() }) it('should report false when no storage is available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') - const sessionStore = initSessionStore(COOKIE_OPTIONS, true) + const sessionStore = initSessionStore({ clientToken, allowFallbackToLocalStorage: true }) expect(sessionStore).not.toBeDefined() }) - - describe('cookie options', () => { - ;[ - { - cookieOptions: {}, - cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict$/, - description: 'should set same-site to strict by default', - }, - { - cookieOptions: { crossSite: true }, - cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=none$/, - description: 'should set same site to none for crossSite', - }, - { - cookieOptions: { secure: true }, - cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;secure$/, - description: 'should add secure attribute when defined', - }, - { - cookieOptions: { domain: 'foo.bar' }, - cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;domain=foo\.bar$/, - description: 'should set cookie domain when defined', - }, - ].forEach(({ description, cookieOptions, cookieString }) => { - it(description, () => { - const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') - initCookieStore(cookieOptions) - expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) - }) - }) - }) }) describe('session lifecyle mechanism', () => { @@ -119,7 +87,7 @@ describe('session store', () => { trackingType: FakeTrackingType.TRACKED, }) ) { - const sessionStore = initSessionStore(COOKIE_OPTIONS, false) + const sessionStore = initSessionStore({ clientToken }) if (!sessionStore) { fail('Unable to initialize cookie storage') return diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts index 2984da06d0..641e76372c 100644 --- a/packages/core/src/domain/session/sessionStoreManager.ts +++ b/packages/core/src/domain/session/sessionStoreManager.ts @@ -3,9 +3,10 @@ import { Observable } from '../../tools/observable' import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' +import type { InitConfiguration } from '../configuration' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { initCookieStore } from './sessionCookieStore' -import type { SessionState, SessionStore, StoreInitOptions } from './sessionStore' +import type { SessionState, SessionStore } from './sessionStore' import { initLocalStorage } from './sessionLocalStorageStore' import { processSessionStoreOperations } from './sessionStoreOperations' @@ -25,14 +26,11 @@ const POLL_DELAY = ONE_SECOND * Checks if cookies are available as the preferred storage * Else, checks if LocalStorage is allowed and available */ -export function initSessionStore( - storageInitOptions: StoreInitOptions, - allowFallbackToLocalStorage: boolean -): SessionStore | undefined { - let sessionStore = initCookieStore(storageInitOptions) - - if (!sessionStore && allowFallbackToLocalStorage) { - sessionStore = initLocalStorage(storageInitOptions) +export function initSessionStore(initConfiguration: InitConfiguration): SessionStore | undefined { + let sessionStore = initCookieStore(initConfiguration) + + if (!sessionStore && initConfiguration.allowFallbackToLocalStorage) { + sessionStore = initLocalStorage() } return sessionStore } diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 456fc97884..4c3400e160 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -1,6 +1,6 @@ import type { StubStorage } from '../../../test' import { mockClock, stubCookieProvider, stubLocalStorageProvider } from '../../../test' -import type { CookieOptions } from '../../browser/cookie' +import type { InitConfiguration } from '../configuration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { initCookieStore, SESSION_COOKIE_NAME } from './sessionCookieStore' import { initLocalStorage, LOCAL_STORAGE_KEY } from './sessionLocalStorageStore' @@ -13,19 +13,19 @@ import { LOCK_RETRY_DELAY, } from './sessionStoreOperations' -const COOKIE_OPTIONS: CookieOptions = {} +const initConfiguration: InitConfiguration = { clientToken: 'abc' } ;( [ { title: 'Cookie Storage', - sessionStore: initCookieStore(COOKIE_OPTIONS)!, + sessionStore: initCookieStore(initConfiguration)!, stubStorageProvider: stubCookieProvider, storageKey: SESSION_COOKIE_NAME, }, { title: 'Local Storage', - sessionStore: initLocalStorage({})!, + sessionStore: initLocalStorage()!, stubStorageProvider: stubLocalStorageProvider, storageKey: LOCAL_STORAGE_KEY, }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b189b5d039..1de20c1a1a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,6 @@ export { Configuration, InitConfiguration, - buildCookieOptions, validateAndBuildConfiguration, DefaultPrivacyLevel, EndpointBuilder, diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 3b5aee014b..8b4ebbba24 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -23,7 +23,7 @@ describe('logs session manager', () => { const DURATION = 123456 const configuration: Partial = { sessionSampleRate: 0.5, - sessionStore: initSessionStore({}, false), + sessionStore: initSessionStore({ clientToken: 'abc' }), } let clock: Clock let tracked: boolean diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index bda5a5d9f2..9b2119213b 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -123,12 +123,7 @@ export function makeRumPublicApi( } if (!eventBridgeAvailable && !configuration.sessionStore) { - if (configuration.allowFallbackToLocalStorage) { - display.warn('No storage available for session. We will not send any data.') - } else { - // Keep until V5 to avoid breaking changes - display.warn('Cookies are not authorized, we will not send any data.') - } + display.warn('No storage available for session. We will not send any data.') return } From 75c104d1771762056a9711e202e43a5ff9732940 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 2 Jun 2023 10:22:37 +0200 Subject: [PATCH 23/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Changed=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/configuration/configuration.ts | 12 +- .../session/oldCookiesMigration.spec.ts | 12 +- .../src/domain/session/oldCookiesMigration.ts | 9 +- .../src/domain/session/sessionManager.spec.ts | 4 +- .../core/src/domain/session/sessionManager.ts | 30 +- .../src/domain/session/sessionState.spec.ts | 44 ++ .../core/src/domain/session/sessionState.ts | 45 ++ .../src/domain/session/sessionStore.spec.ts | 392 ++++++++++++++++-- .../core/src/domain/session/sessionStore.ts | 202 +++++++-- .../session/sessionStoreManager.spec.ts | 384 ----------------- .../src/domain/session/sessionStoreManager.ts | 174 -------- .../session/sessionStoreOperations.spec.ts | 74 ++-- .../domain/session/sessionStoreOperations.ts | 29 +- .../sessionInCookie.spec.ts} | 27 +- .../sessionInCookie.ts} | 17 +- .../sessionInLocalStorage.spec.ts} | 16 +- .../sessionInLocalStorage.ts} | 9 +- .../storeStrategies/sessionStoreStrategy.ts | 7 + packages/core/src/index.ts | 4 +- 19 files changed, 751 insertions(+), 740 deletions(-) create mode 100644 packages/core/src/domain/session/sessionState.spec.ts create mode 100644 packages/core/src/domain/session/sessionState.ts delete mode 100644 packages/core/src/domain/session/sessionStoreManager.spec.ts delete mode 100644 packages/core/src/domain/session/sessionStoreManager.ts rename packages/core/src/domain/session/{sessionCookieStore.spec.ts => storeStrategies/sessionInCookie.spec.ts} (80%) rename packages/core/src/domain/session/{sessionCookieStore.ts => storeStrategies/sessionInCookie.ts} (70%) rename packages/core/src/domain/session/{sessionLocalStorageStore.spec.ts => storeStrategies/sessionInLocalStorage.spec.ts} (77%) rename packages/core/src/domain/session/{sessionLocalStorageStore.ts => storeStrategies/sessionInLocalStorage.ts} (74%) create mode 100644 packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 3963b32ca4..9eabada83e 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -8,12 +8,12 @@ import { isPercentage } from '../../tools/utils/numberUtils' import { ONE_KIBI_BYTE } from '../../tools/utils/byteUtils' import { objectHasValue } from '../../tools/utils/objectUtils' import { assign } from '../../tools/utils/polyfills' -import { initSessionStore } from '../session/sessionStoreManager' -import type { SessionStore } from '../session/sessionStore' +import { initSessionStoreStrategy } from '../session/sessionStore' +import type { SessionStoreStrategy } from '../session/storeStrategies/sessionStoreStrategy' +import type { CookieOptions } from '../../browser/cookie' +import { buildCookieOptions } from '../session/storeStrategies/sessionInCookie' import type { TransportConfiguration } from './transportConfiguration' import { computeTransportConfiguration } from './transportConfiguration' -import { CookieOptions } from '../../browser/cookie' -import { buildCookieOptions } from '../session/sessionCookieStore' export const DefaultPrivacyLevel = { ALLOW: 'allow', @@ -80,7 +80,7 @@ export interface Configuration extends TransportConfiguration { beforeSend: GenericBeforeSendCallback | undefined cookieOptions: CookieOptions allowFallbackToLocalStorage: boolean - sessionStore: SessionStore | undefined + sessionStore: SessionStoreStrategy | undefined sessionSampleRate: number telemetrySampleRate: number telemetryConfigurationSampleRate: number @@ -138,7 +138,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), cookieOptions: buildCookieOptions(initConfiguration), allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, - sessionStore: initSessionStore(initConfiguration), + sessionStore: initSessionStoreStrategy(initConfiguration), sessionSampleRate: sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index 6caddedc7a..551e429e3b 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -6,15 +6,15 @@ import { tryOldCookiesMigration, } from './oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStrategy } from './storeStrategies/sessionInCookie' describe('old cookies migration', () => { - const sessionStore = initCookieStore({ clientToken: '123' })! + const sessionStoreStrategy = initCookieStrategy({ clientToken: '123' })! it('should not touch current cookie', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1&expire=1234567890') }) @@ -24,7 +24,7 @@ describe('old cookies migration', () => { setCookie(OLD_LOGS_COOKIE_NAME, '1', SESSION_EXPIRATION_DELAY) setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcde') expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') @@ -35,7 +35,7 @@ describe('old cookies migration', () => { it('should create new cookie from a single old cookie', () => { setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') @@ -43,7 +43,7 @@ describe('old cookies migration', () => { }) it('should not create a new cookie if no old cookie is present', () => { - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStore) + tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) }) diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index 7573ba00c2..7e9511181d 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,7 +1,8 @@ import { getCookie } from '../../browser/cookie' import { dateNow } from '../../tools/utils/timeUtils' -import type { SessionState, SessionStore } from './sessionStore' -import { isSessionInExpiredState } from './sessionStore' +import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' +import type { SessionState } from './sessionState' +import { isSessionInExpiredState } from './sessionState' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' export const OLD_SESSION_COOKIE_NAME = '_dd' @@ -16,7 +17,7 @@ export const LOGS_SESSION_KEY = 'logs' * This migration should remain in the codebase as long as older versions are available/live * to allow older sdk versions to be upgraded to newer versions without compatibility issues. */ -export function tryOldCookiesMigration(cookieName: string, sessionStore: SessionStore) { +export function tryOldCookiesMigration(cookieName: string, cookieStoreStrategy: SessionStoreStrategy) { const sessionString = getCookie(cookieName) if (!sessionString) { const oldSessionId = getCookie(OLD_SESSION_COOKIE_NAME) @@ -36,7 +37,7 @@ export function tryOldCookiesMigration(cookieName: string, sessionStore: Session if (!isSessionInExpiredState(session)) { session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) - sessionStore.persistSession(session) + cookieStoreStrategy.persistSession(session) } } } diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 5102509f31..21ec3c3da1 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -8,7 +8,7 @@ import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' import type { InitConfiguration } from '../configuration' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' -import { SESSION_COOKIE_NAME, initCookieStore } from './sessionCookieStore' +import { SESSION_COOKIE_NAME, initCookieStrategy } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' const enum FakeTrackingType { @@ -31,7 +31,7 @@ describe('startSessionManager', () => { const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' const initConfiguration: InitConfiguration = { clientToken: 'abc' } - const sessionStore = initCookieStore(initConfiguration)! + const sessionStore = initCookieStrategy(initConfiguration)! let clock: Clock function expireSessionCookie() { diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 4d8ba66f9e..b87a837f88 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -6,8 +6,8 @@ import { relativeNow, clocksOrigin, ONE_MINUTE } from '../../tools/utils/timeUti import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/addEventListener' import { clearInterval, setInterval } from '../../tools/timer' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { startSessionStoreManager } from './sessionStoreManager' -import type { SessionStore } from './sessionStore' +import { startSessionStore } from './sessionStore' +import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' export interface SessionManager { findActiveSession: (startTime?: RelativeTime) => SessionContext | undefined @@ -26,41 +26,41 @@ const SESSION_CONTEXT_TIMEOUT_DELAY = SESSION_TIME_OUT_DELAY let stopCallbacks: Array<() => void> = [] export function startSessionManager( - sessionStore: SessionStore, + sessionStoreStrategy: SessionStoreStrategy, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionManager { - const sessionStoreManager = startSessionStoreManager(sessionStore, productKey, computeSessionState) - stopCallbacks.push(() => sessionStoreManager.stop()) + const sessionStore = startSessionStore(sessionStoreStrategy, productKey, computeSessionState) + stopCallbacks.push(() => sessionStore.stop()) const sessionContextHistory = new ValueHistory>(SESSION_CONTEXT_TIMEOUT_DELAY) stopCallbacks.push(() => sessionContextHistory.stop()) - sessionStoreManager.renewObservable.subscribe(() => { + sessionStore.renewObservable.subscribe(() => { sessionContextHistory.add(buildSessionContext(), relativeNow()) }) - sessionStoreManager.expireObservable.subscribe(() => { + sessionStore.expireObservable.subscribe(() => { sessionContextHistory.closeActive(relativeNow()) }) - sessionStoreManager.expandOrRenewSession() + sessionStore.expandOrRenewSession() sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative) - trackActivity(() => sessionStoreManager.expandOrRenewSession()) - trackVisibility(() => sessionStoreManager.expandSession()) + trackActivity(() => sessionStore.expandOrRenewSession()) + trackVisibility(() => sessionStore.expandSession()) function buildSessionContext() { return { - id: sessionStoreManager.getSession().id!, - trackingType: sessionStoreManager.getSession()[productKey] as TrackingType, + id: sessionStore.getSession().id!, + trackingType: sessionStore.getSession()[productKey] as TrackingType, } } return { findActiveSession: (startTime) => sessionContextHistory.find(startTime), - renewObservable: sessionStoreManager.renewObservable, - expireObservable: sessionStoreManager.expireObservable, - expire: sessionStoreManager.expire, + renewObservable: sessionStore.renewObservable, + expireObservable: sessionStore.expireObservable, + expire: sessionStore.expire, } } diff --git a/packages/core/src/domain/session/sessionState.spec.ts b/packages/core/src/domain/session/sessionState.spec.ts new file mode 100644 index 0000000000..253fb69d78 --- /dev/null +++ b/packages/core/src/domain/session/sessionState.spec.ts @@ -0,0 +1,44 @@ +import type { SessionState } from './sessionState' +import { isSessionInExpiredState, toSessionString, toSessionState } from './sessionState' + +describe('session state utilities', () => { + const EXPIRED_SESSION: SessionState = {} + const SERIALIZED_EXPIRED_SESSION = '' + const LIVE_SESSION: SessionState = { created: '0', id: '123' } + const SERIALIZED_LIVE_SESSION = 'created=0&id=123' + + describe('isSessionInExpiredState', () => { + it('should correctly identify a session in expired state', () => { + expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) + }) + + it('should correctly identify a session in live state', () => { + expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) + }) + }) + + describe('toSessionString', () => { + it('should serialize a sessionState to a string', () => { + expect(toSessionString(LIVE_SESSION)).toEqual(SERIALIZED_LIVE_SESSION) + }) + + it('should handle empty sessionStates', () => { + expect(toSessionString(EXPIRED_SESSION)).toEqual(SERIALIZED_EXPIRED_SESSION) + }) + }) + + describe('sessionStringToSessionState', () => { + it('should deserialize a session string to a sessionState', () => { + expect(toSessionState(SERIALIZED_LIVE_SESSION)).toEqual(LIVE_SESSION) + }) + + it('should handle empty session strings', () => { + expect(toSessionState(SERIALIZED_EXPIRED_SESSION)).toEqual(EXPIRED_SESSION) + }) + + it('should handle invalid session strings', () => { + const sessionString = '{invalid: true}' + expect(toSessionState(sessionString)).toEqual(EXPIRED_SESSION) + }) + }) +}) diff --git a/packages/core/src/domain/session/sessionState.ts b/packages/core/src/domain/session/sessionState.ts new file mode 100644 index 0000000000..2df2deb3a4 --- /dev/null +++ b/packages/core/src/domain/session/sessionState.ts @@ -0,0 +1,45 @@ +import { isEmptyObject } from '../../tools/utils/objectUtils' +import { objectEntries } from '../../tools/utils/polyfills' + +const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ +const SESSION_ENTRY_SEPARATOR = '&' + +export interface SessionState { + id?: string + created?: string + expire?: string + lock?: string + + [key: string]: string | undefined +} + +export function isSessionInExpiredState(session: SessionState) { + return isEmptyObject(session) +} + +export function toSessionString(session: SessionState) { + return objectEntries(session) + .map(([key, value]) => `${key}=${value as string}`) + .join(SESSION_ENTRY_SEPARATOR) +} + +export function toSessionState(sessionString: string | undefined | null) { + const session: SessionState = {} + if (isValidSessionString(sessionString)) { + sessionString.split(SESSION_ENTRY_SEPARATOR).forEach((entry) => { + const matches = SESSION_ENTRY_REGEXP.exec(entry) + if (matches !== null) { + const [, key, value] = matches + session[key] = value + } + }) + } + return session +} + +function isValidSessionString(sessionString: string | undefined | null): sessionString is string { + return ( + !!sessionString && + (sessionString.indexOf(SESSION_ENTRY_SEPARATOR) !== -1 || SESSION_ENTRY_REGEXP.test(sessionString)) + ) +} diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index f3e181d942..18fd372511 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,44 +1,384 @@ -import type { SessionState } from './sessionStore' -import { isSessionInExpiredState, toSessionString, toSessionState } from './sessionStore' +import type { Clock } from '../../../test' +import { mockClock } from '../../../test' +import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' +import type { SessionStore } from './sessionStore' +import { initSessionStoreStrategy, startSessionStore } from './sessionStore' +import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' +import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -describe('session store utilities', () => { - const EXPIRED_SESSION: SessionState = {} - const SERIALIZED_EXPIRED_SESSION = '' - const LIVE_SESSION: SessionState = { created: '0', id: '123' } - const SERIALIZED_LIVE_SESSION = 'created=0&id=123' +const enum FakeTrackingType { + TRACKED = 'tracked', + NOT_TRACKED = 'not-tracked', +} - describe('isSessionInExpiredState', () => { - it('should correctly identify a session in expired state', () => { - expect(isSessionInExpiredState(EXPIRED_SESSION)).toBe(true) +const DURATION = 123456 +const PRODUCT_KEY = 'product' +const FIRST_ID = 'first' +const SECOND_ID = 'second' +const clientToken = 'abc' + +function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { + setCookie( + SESSION_COOKIE_NAME, + `${id ? `id=${id}&` : ''}${PRODUCT_KEY}=${trackingType}&created=${Date.now()}&expire=${ + expire || Date.now() + SESSION_EXPIRATION_DELAY + }`, + DURATION + ) +} + +function expectTrackedSessionToBeInStore(id?: string) { + expect(getCookie(SESSION_COOKIE_NAME)).toMatch(new RegExp(`id=${id ? id : '[a-f0-9-]+'}`)) + expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.TRACKED}`) +} + +function expectNotTrackedSessionToBeInStore() { + expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') + expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`) +} + +function getStoreExpiration() { + return /expire=(\d+)/.exec(getCookie(SESSION_COOKIE_NAME)!)?.[1] +} + +function resetSessionInStore() { + setCookie(SESSION_COOKIE_NAME, '', DURATION) +} + +describe('session store', () => { + describe('initSessionStoreStrategy', () => { + it('should initialize storage when cookies are available', () => { + const sessionStore = initSessionStoreStrategy({ clientToken }) + expect(sessionStore).toBeDefined() }) - it('should correctly identify a session in live state', () => { - expect(isSessionInExpiredState(LIVE_SESSION)).toBe(false) + it('should report false when cookies are not available, and fallback is not allowed', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const sessionStore = initSessionStoreStrategy({ clientToken, allowFallbackToLocalStorage: false }) + expect(sessionStore).not.toBeDefined() }) - }) - describe('toSessionString', () => { - it('should serialize a sessionState to a string', () => { - expect(toSessionString(LIVE_SESSION)).toEqual(SERIALIZED_LIVE_SESSION) + it('should fallback to localStorage and report true when cookies are not available', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const sessionStore = initSessionStoreStrategy({ clientToken, allowFallbackToLocalStorage: true }) + expect(sessionStore).toBeDefined() }) - it('should handle empty sessionStates', () => { - expect(toSessionString(EXPIRED_SESSION)).toEqual(SERIALIZED_EXPIRED_SESSION) + it('should report false when no storage is available', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') + const sessionStore = initSessionStoreStrategy({ clientToken, allowFallbackToLocalStorage: true }) + expect(sessionStore).not.toBeDefined() }) }) - describe('sessionStringToSessionState', () => { - it('should deserialize a session string to a sessionState', () => { - expect(toSessionState(SERIALIZED_LIVE_SESSION)).toEqual(LIVE_SESSION) + describe('session lifecyle mechanism', () => { + let expireSpy: () => void + let renewSpy: () => void + let sessionStoreManager: SessionStore + let clock: Clock + + function setupSessionStore( + computeSessionState: (rawTrackingType?: string) => { + trackingType: FakeTrackingType + isTracked: boolean + } = () => ({ + isTracked: true, + trackingType: FakeTrackingType.TRACKED, + }) + ) { + const sessionStore = initSessionStoreStrategy({ clientToken }) + if (!sessionStore) { + fail('Unable to initialize cookie storage') + return + } + sessionStoreManager = startSessionStore(sessionStore, PRODUCT_KEY, computeSessionState) + sessionStoreManager.expireObservable.subscribe(expireSpy) + sessionStoreManager.renewObservable.subscribe(renewSpy) + } + + beforeEach(() => { + expireSpy = jasmine.createSpy('expire session') + renewSpy = jasmine.createSpy('renew session') + clock = mockClock() + }) + + afterEach(() => { + resetSessionInStore() + clock.cleanup() + sessionStoreManager.stop() + }) + + describe('expand or renew session', () => { + it( + 'when session not in cache, session not in store and new session tracked, ' + + 'should create new session and trigger renew session ', + () => { + setupSessionStore() + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBeDefined() + expectTrackedSessionToBeInStore() + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + it( + 'when session not in cache, session not in store and new session not tracked, ' + + 'should store not tracked session', + () => { + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it('when session not in cache and session in store, should expand session and trigger renew session', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expectTrackedSessionToBeInStore(FIRST_ID) + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + }) + + it( + 'when session in cache, session not in store and new session tracked, ' + + 'should expire session, create a new one and trigger renew session', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + sessionStoreManager.expandOrRenewSession() + + const sessionId = sessionStoreManager.getSession().id + expect(sessionId).toBeDefined() + expect(sessionId).not.toBe(FIRST_ID) + expectTrackedSessionToBeInStore(sessionId) + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + it( + 'when session in cache, session not in store and new session not tracked, ' + + 'should expire session and store not tracked session', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + resetSessionInStore() + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it( + 'when session not tracked in cache, session not in store and new session not tracked, ' + + 'should expire session and store not tracked session', + () => { + setSessionInStore(FakeTrackingType.NOT_TRACKED) + setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) + resetSessionInStore() + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) + + it('when session in cache is same session than in store, should expand session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + + clock.tick(10) + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) + expectTrackedSessionToBeInStore(FIRST_ID) + expect(expireSpy).not.toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + }) + + it( + 'when session in cache is different session than in store and store session is tracked, ' + + 'should expire session, expand store session and trigger renew', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBe(SECOND_ID) + expectTrackedSessionToBeInStore(SECOND_ID) + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).toHaveBeenCalled() + } + ) + + 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', + () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore((rawTrackingType) => ({ + isTracked: rawTrackingType === FakeTrackingType.TRACKED, + trackingType: rawTrackingType as FakeTrackingType, + })) + setSessionInStore(FakeTrackingType.NOT_TRACKED, '') + + sessionStoreManager.expandOrRenewSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expectNotTrackedSessionToBeInStore() + expect(expireSpy).toHaveBeenCalled() + expect(renewSpy).not.toHaveBeenCalled() + } + ) }) - it('should handle empty session strings', () => { - expect(toSessionState(SERIALIZED_EXPIRED_SESSION)).toEqual(EXPIRED_SESSION) + describe('expand session', () => { + it('when session not in cache and session not in store, should do nothing', () => { + setupSessionStore() + + sessionStoreManager.expandSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session not in cache and session in store, should do nothing', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + sessionStoreManager.expandSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache and session not in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + sessionStoreManager.expandSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + + it('when session in cache is same session than in store, should expand session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + + clock.tick(10) + sessionStoreManager.expandSession() + + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache is different session than in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + sessionStoreManager.expandSession() + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expectTrackedSessionToBeInStore(SECOND_ID) + expect(expireSpy).toHaveBeenCalled() + }) }) - it('should handle invalid session strings', () => { - const sessionString = '{invalid: true}' - expect(toSessionState(sessionString)).toEqual(EXPIRED_SESSION) + describe('regular watch', () => { + it('when session not in cache and session not in store, should do nothing', () => { + setupSessionStore() + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session not in cache and session in store, should do nothing', () => { + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session in cache and session not in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + resetSessionInStore() + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + + it('when session in cache is same session than in store, should synchronize session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) + expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) + expect(expireSpy).not.toHaveBeenCalled() + }) + + it('when session id in cache is different than session id in store, should expire session', () => { + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) + + it('when session type in cache is different than session type in store, should expire session', () => { + setSessionInStore(FakeTrackingType.NOT_TRACKED, FIRST_ID) + setupSessionStore() + setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) + + clock.tick(COOKIE_ACCESS_DELAY) + + expect(sessionStoreManager.getSession().id).toBeUndefined() + expect(expireSpy).toHaveBeenCalled() + }) }) }) }) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 0f3dac1fd8..a3f400ab9d 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -1,51 +1,175 @@ -import { isEmptyObject } from '../../tools/utils/objectUtils' -import { objectEntries } from '../../tools/utils/polyfills' +import { clearInterval, setInterval } from '../../tools/timer' +import { Observable } from '../../tools/observable' +import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' +import { throttle } from '../../tools/utils/functionUtils' +import { generateUUID } from '../../tools/utils/stringUtils' +import type { InitConfiguration } from '../configuration' +import { SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { initCookieStrategy } from './storeStrategies/sessionInCookie' +import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' +import type { SessionState } from './sessionState' +import { initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' +import { processSessionStoreOperations } from './sessionStoreOperations' -const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ -const SESSION_ENTRY_SEPARATOR = '&' +export interface SessionStore { + expandOrRenewSession: () => void + expandSession: () => void + getSession: () => SessionState + renewObservable: Observable + expireObservable: Observable + expire: () => void + stop: () => void +} -export interface SessionState { - id?: string - created?: string - expire?: string - lock?: string +const POLL_DELAY = ONE_SECOND - [key: string]: string | undefined -} +/** + * Checks if cookies are available as the preferred storage + * Else, checks if LocalStorage is allowed and available + */ +export function initSessionStoreStrategy(initConfiguration: InitConfiguration): SessionStoreStrategy | undefined { + let sessionStoreStrategy = initCookieStrategy(initConfiguration) -export interface SessionStore { - persistSession: (session: SessionState) => void - retrieveSession: () => SessionState - clearSession: () => void + if (!sessionStoreStrategy && initConfiguration.allowFallbackToLocalStorage) { + sessionStoreStrategy = initLocalStorageStrategy() + } + return sessionStoreStrategy } -export function isSessionInExpiredState(session: SessionState) { - return isEmptyObject(session) -} +/** + * Different session concepts: + * - tracked, the session has an id and is updated along the user navigation + * - not tracked, the session does not have an id but it is updated along the user navigation + * - inactive, no session in store or session expired, waiting for a renew session + */ +export function startSessionStore( + sessionStoreStrategy: SessionStoreStrategy, + productKey: string, + computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } +): SessionStore { + const renewObservable = new Observable() + const expireObservable = new Observable() -export function toSessionString(session: SessionState) { - return objectEntries(session) - .map(([key, value]) => `${key}=${value as string}`) - .join(SESSION_ENTRY_SEPARATOR) -} + const { clearSession, retrieveSession } = sessionStoreStrategy + + const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) + let sessionCache: SessionState = retrieveActiveSession() -export function toSessionState(sessionString: string | undefined | null) { - const session: SessionState = {} - if (isValidSessionString(sessionString)) { - sessionString.split(SESSION_ENTRY_SEPARATOR).forEach((entry) => { - const matches = SESSION_ENTRY_REGEXP.exec(entry) - if (matches !== null) { - const [, key, value] = matches - session[key] = value + function expandOrRenewSession() { + let isTracked: boolean + processSessionStoreOperations( + { + process: (sessionState) => { + const synchronizedSession = synchronizeSession(sessionState) + isTracked = expandOrRenewSessionState(synchronizedSession) + return synchronizedSession + }, + after: (sessionState) => { + if (isTracked && !hasSessionInCache()) { + renewSessionInCache(sessionState) + } + sessionCache = sessionState + }, + }, + sessionStoreStrategy + ) + } + + function expandSession() { + processSessionStoreOperations( + { + process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), + }, + sessionStoreStrategy + ) + } + + /** + * allows two behaviors: + * - if the session is active, synchronize the session cache without updating the session store + * - if the session is not active, clear the session store and expire the session cache + */ + function watchSession() { + processSessionStoreOperations( + { + process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), + after: synchronizeSession, + }, + sessionStoreStrategy + ) + } + + function synchronizeSession(sessionState: SessionState) { + if (!isActiveSession(sessionState)) { + sessionState = {} + } + if (hasSessionInCache()) { + if (isSessionInCacheOutdated(sessionState)) { + expireSessionInCache() + } else { + sessionCache = sessionState } - }) + } + return sessionState } - return session -} -function isValidSessionString(sessionString: string | undefined | null): sessionString is string { - return ( - !!sessionString && - (sessionString.indexOf(SESSION_ENTRY_SEPARATOR) !== -1 || SESSION_ENTRY_REGEXP.test(sessionString)) - ) + function expandOrRenewSessionState(sessionState: SessionState) { + const { trackingType, isTracked } = computeSessionState(sessionState[productKey]) + sessionState[productKey] = trackingType + if (isTracked && !sessionState.id) { + sessionState.id = generateUUID() + sessionState.created = String(dateNow()) + } + return isTracked + } + + function hasSessionInCache() { + return sessionCache[productKey] !== undefined + } + + function isSessionInCacheOutdated(sessionState: SessionState) { + return sessionCache.id !== sessionState.id || sessionCache[productKey] !== sessionState[productKey] + } + + function expireSessionInCache() { + sessionCache = {} + expireObservable.notify() + } + + function renewSessionInCache(sessionState: SessionState) { + sessionCache = sessionState + renewObservable.notify() + } + + function retrieveActiveSession(): SessionState { + const session = retrieveSession() + if (isActiveSession(session)) { + return session + } + return {} + } + + function isActiveSession(sessionDate: SessionState) { + // created and expire can be undefined for versions which was not storing them + // these checks could be removed when older versions will not be available/live anymore + return ( + (sessionDate.created === undefined || dateNow() - Number(sessionDate.created) < SESSION_TIME_OUT_DELAY) && + (sessionDate.expire === undefined || dateNow() < Number(sessionDate.expire)) + ) + } + + return { + expandOrRenewSession: throttle(expandOrRenewSession, POLL_DELAY).throttled, + expandSession, + getSession: () => sessionCache, + renewObservable, + expireObservable, + expire: () => { + clearSession() + synchronizeSession({}) + }, + stop: () => { + clearInterval(watchSessionTimeoutId) + }, + } } diff --git a/packages/core/src/domain/session/sessionStoreManager.spec.ts b/packages/core/src/domain/session/sessionStoreManager.spec.ts deleted file mode 100644 index c31abea544..0000000000 --- a/packages/core/src/domain/session/sessionStoreManager.spec.ts +++ /dev/null @@ -1,384 +0,0 @@ -import type { Clock } from '../../../test' -import { mockClock } from '../../../test' -import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' -import type { SessionStoreManager } from './sessionStoreManager' -import { initSessionStore, startSessionStoreManager } from './sessionStoreManager' -import { SESSION_COOKIE_NAME } from './sessionCookieStore' -import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' - -const enum FakeTrackingType { - TRACKED = 'tracked', - NOT_TRACKED = 'not-tracked', -} - -const DURATION = 123456 -const PRODUCT_KEY = 'product' -const FIRST_ID = 'first' -const SECOND_ID = 'second' -const clientToken = 'abc' - -function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { - setCookie( - SESSION_COOKIE_NAME, - `${id ? `id=${id}&` : ''}${PRODUCT_KEY}=${trackingType}&created=${Date.now()}&expire=${ - expire || Date.now() + SESSION_EXPIRATION_DELAY - }`, - DURATION - ) -} - -function expectTrackedSessionToBeInStore(id?: string) { - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(new RegExp(`id=${id ? id : '[a-f0-9-]+'}`)) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.TRACKED}`) -} - -function expectNotTrackedSessionToBeInStore() { - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`) -} - -function getStoreExpiration() { - return /expire=(\d+)/.exec(getCookie(SESSION_COOKIE_NAME)!)?.[1] -} - -function resetSessionInStore() { - setCookie(SESSION_COOKIE_NAME, '', DURATION) -} - -describe('session store', () => { - describe('initSessionStore', () => { - it('should initialize storage when cookies are available', () => { - const sessionStore = initSessionStore({ clientToken }) - expect(sessionStore).toBeDefined() - }) - - it('should report false when cookies are not available, and fallback is not allowed', () => { - spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStore = initSessionStore({ clientToken, allowFallbackToLocalStorage: false }) - expect(sessionStore).not.toBeDefined() - }) - - it('should fallback to localStorage and report true when cookies are not available', () => { - spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStore = initSessionStore({ clientToken, allowFallbackToLocalStorage: true }) - expect(sessionStore).toBeDefined() - }) - - it('should report false when no storage is available', () => { - spyOnProperty(document, 'cookie', 'get').and.returnValue('') - spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') - const sessionStore = initSessionStore({ clientToken, allowFallbackToLocalStorage: true }) - expect(sessionStore).not.toBeDefined() - }) - }) - - describe('session lifecyle mechanism', () => { - let expireSpy: () => void - let renewSpy: () => void - let sessionStoreManager: SessionStoreManager - let clock: Clock - - function setupSessionStore( - computeSessionState: (rawTrackingType?: string) => { - trackingType: FakeTrackingType - isTracked: boolean - } = () => ({ - isTracked: true, - trackingType: FakeTrackingType.TRACKED, - }) - ) { - const sessionStore = initSessionStore({ clientToken }) - if (!sessionStore) { - fail('Unable to initialize cookie storage') - return - } - sessionStoreManager = startSessionStoreManager(sessionStore, PRODUCT_KEY, computeSessionState) - sessionStoreManager.expireObservable.subscribe(expireSpy) - sessionStoreManager.renewObservable.subscribe(renewSpy) - } - - beforeEach(() => { - expireSpy = jasmine.createSpy('expire session') - renewSpy = jasmine.createSpy('renew session') - clock = mockClock() - }) - - afterEach(() => { - resetSessionInStore() - clock.cleanup() - sessionStoreManager.stop() - }) - - describe('expand or renew session', () => { - it( - 'when session not in cache, session not in store and new session tracked, ' + - 'should create new session and trigger renew session ', - () => { - setupSessionStore() - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBeDefined() - expectTrackedSessionToBeInStore() - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) - - it( - 'when session not in cache, session not in store and new session not tracked, ' + - 'should store not tracked session', - () => { - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it('when session not in cache and session in store, should expand session and trigger renew session', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) - expectTrackedSessionToBeInStore(FIRST_ID) - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - }) - - it( - 'when session in cache, session not in store and new session tracked, ' + - 'should expire session, create a new one and trigger renew session', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() - - sessionStoreManager.expandOrRenewSession() - - const sessionId = sessionStoreManager.getSession().id - expect(sessionId).toBeDefined() - expect(sessionId).not.toBe(FIRST_ID) - expectTrackedSessionToBeInStore(sessionId) - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) - - it( - 'when session in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - resetSessionInStore() - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it( - 'when session not tracked in cache, session not in store and new session not tracked, ' + - 'should expire session and store not tracked session', - () => { - setSessionInStore(FakeTrackingType.NOT_TRACKED) - setupSessionStore(() => ({ isTracked: false, trackingType: FakeTrackingType.NOT_TRACKED })) - resetSessionInStore() - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(sessionStoreManager.getSession()[PRODUCT_KEY]).toBeDefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - - it('when session in cache is same session than in store, should expand session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - - clock.tick(10) - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) - expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) - expectTrackedSessionToBeInStore(FIRST_ID) - expect(expireSpy).not.toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - }) - - it( - 'when session in cache is different session than in store and store session is tracked, ' + - 'should expire session, expand store session and trigger renew', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBe(SECOND_ID) - expectTrackedSessionToBeInStore(SECOND_ID) - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).toHaveBeenCalled() - } - ) - - 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', - () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore((rawTrackingType) => ({ - isTracked: rawTrackingType === FakeTrackingType.TRACKED, - trackingType: rawTrackingType as FakeTrackingType, - })) - setSessionInStore(FakeTrackingType.NOT_TRACKED, '') - - sessionStoreManager.expandOrRenewSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expectNotTrackedSessionToBeInStore() - expect(expireSpy).toHaveBeenCalled() - expect(renewSpy).not.toHaveBeenCalled() - } - ) - }) - - describe('expand session', () => { - it('when session not in cache and session not in store, should do nothing', () => { - setupSessionStore() - - sessionStoreManager.expandSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session not in cache and session in store, should do nothing', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - sessionStoreManager.expandSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session in cache and session not in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() - - sessionStoreManager.expandSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - - it('when session in cache is same session than in store, should expand session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - - clock.tick(10) - sessionStoreManager.expandSession() - - expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) - expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session in cache is different session than in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - sessionStoreManager.expandSession() - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expectTrackedSessionToBeInStore(SECOND_ID) - expect(expireSpy).toHaveBeenCalled() - }) - }) - - describe('regular watch', () => { - it('when session not in cache and session not in store, should do nothing', () => { - setupSessionStore() - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session not in cache and session in store, should do nothing', () => { - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session in cache and session not in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - resetSessionInStore() - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - - it('when session in cache is same session than in store, should synchronize session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) - expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) - expect(expireSpy).not.toHaveBeenCalled() - }) - - it('when session id in cache is different than session id in store, should expire session', () => { - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - - it('when session type in cache is different than session type in store, should expire session', () => { - setSessionInStore(FakeTrackingType.NOT_TRACKED, FIRST_ID) - setupSessionStore() - setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - - clock.tick(COOKIE_ACCESS_DELAY) - - expect(sessionStoreManager.getSession().id).toBeUndefined() - expect(expireSpy).toHaveBeenCalled() - }) - }) - }) -}) diff --git a/packages/core/src/domain/session/sessionStoreManager.ts b/packages/core/src/domain/session/sessionStoreManager.ts deleted file mode 100644 index 641e76372c..0000000000 --- a/packages/core/src/domain/session/sessionStoreManager.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { clearInterval, setInterval } from '../../tools/timer' -import { Observable } from '../../tools/observable' -import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' -import { throttle } from '../../tools/utils/functionUtils' -import { generateUUID } from '../../tools/utils/stringUtils' -import type { InitConfiguration } from '../configuration' -import { SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { initCookieStore } from './sessionCookieStore' -import type { SessionState, SessionStore } from './sessionStore' -import { initLocalStorage } from './sessionLocalStorageStore' -import { processSessionStoreOperations } from './sessionStoreOperations' - -export interface SessionStoreManager { - expandOrRenewSession: () => void - expandSession: () => void - getSession: () => SessionState - renewObservable: Observable - expireObservable: Observable - expire: () => void - stop: () => void -} - -const POLL_DELAY = ONE_SECOND - -/** - * Checks if cookies are available as the preferred storage - * Else, checks if LocalStorage is allowed and available - */ -export function initSessionStore(initConfiguration: InitConfiguration): SessionStore | undefined { - let sessionStore = initCookieStore(initConfiguration) - - if (!sessionStore && initConfiguration.allowFallbackToLocalStorage) { - sessionStore = initLocalStorage() - } - return sessionStore -} - -/** - * Different session concepts: - * - tracked, the session has an id and is updated along the user navigation - * - not tracked, the session does not have an id but it is updated along the user navigation - * - inactive, no session in store or session expired, waiting for a renew session - */ -export function startSessionStoreManager( - sessionStore: SessionStore, - productKey: string, - computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } -): SessionStoreManager { - const renewObservable = new Observable() - const expireObservable = new Observable() - - const { clearSession, retrieveSession } = sessionStore - - const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) - let sessionCache: SessionState = retrieveActiveSession() - - function expandOrRenewSession() { - let isTracked: boolean - processSessionStoreOperations( - { - process: (sessionState) => { - const synchronizedSession = synchronizeSession(sessionState) - isTracked = expandOrRenewSessionState(synchronizedSession) - return synchronizedSession - }, - after: (sessionState) => { - if (isTracked && !hasSessionInCache()) { - renewSessionInCache(sessionState) - } - sessionCache = sessionState - }, - }, - sessionStore - ) - } - - function expandSession() { - processSessionStoreOperations( - { - process: (sessionState) => (hasSessionInCache() ? synchronizeSession(sessionState) : undefined), - }, - sessionStore - ) - } - - /** - * allows two behaviors: - * - if the session is active, synchronize the session cache without updating the session store - * - if the session is not active, clear the session store and expire the session cache - */ - function watchSession() { - processSessionStoreOperations( - { - process: (sessionState) => (!isActiveSession(sessionState) ? {} : undefined), - after: synchronizeSession, - }, - sessionStore - ) - } - - function synchronizeSession(sessionState: SessionState) { - if (!isActiveSession(sessionState)) { - sessionState = {} - } - if (hasSessionInCache()) { - if (isSessionInCacheOutdated(sessionState)) { - expireSessionInCache() - } else { - sessionCache = sessionState - } - } - return sessionState - } - - function expandOrRenewSessionState(sessionState: SessionState) { - const { trackingType, isTracked } = computeSessionState(sessionState[productKey]) - sessionState[productKey] = trackingType - if (isTracked && !sessionState.id) { - sessionState.id = generateUUID() - sessionState.created = String(dateNow()) - } - return isTracked - } - - function hasSessionInCache() { - return sessionCache[productKey] !== undefined - } - - function isSessionInCacheOutdated(sessionState: SessionState) { - return sessionCache.id !== sessionState.id || sessionCache[productKey] !== sessionState[productKey] - } - - function expireSessionInCache() { - sessionCache = {} - expireObservable.notify() - } - - function renewSessionInCache(sessionState: SessionState) { - sessionCache = sessionState - renewObservable.notify() - } - - function retrieveActiveSession(): SessionState { - const session = retrieveSession() - if (isActiveSession(session)) { - return session - } - return {} - } - - function isActiveSession(sessionDate: SessionState) { - // created and expire can be undefined for versions which was not storing them - // these checks could be removed when older versions will not be available/live anymore - return ( - (sessionDate.created === undefined || dateNow() - Number(sessionDate.created) < SESSION_TIME_OUT_DELAY) && - (sessionDate.expire === undefined || dateNow() < Number(sessionDate.expire)) - ) - } - - return { - expandOrRenewSession: throttle(expandOrRenewSession, POLL_DELAY).throttled, - expandSession, - getSession: () => sessionCache, - renewObservable, - expireObservable, - expire: () => { - clearSession() - synchronizeSession({}) - }, - stop: () => { - clearInterval(watchSessionTimeoutId) - }, - } -} diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 4c3400e160..1fb73e93e3 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -2,10 +2,10 @@ import type { StubStorage } from '../../../test' import { mockClock, stubCookieProvider, stubLocalStorageProvider } from '../../../test' import type { InitConfiguration } from '../configuration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { initCookieStore, SESSION_COOKIE_NAME } from './sessionCookieStore' -import { initLocalStorage, LOCAL_STORAGE_KEY } from './sessionLocalStorageStore' -import type { SessionState } from './sessionStore' -import { toSessionString } from './sessionStore' +import { initCookieStrategy, SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' +import { initLocalStorageStrategy, LOCAL_STORAGE_KEY } from './storeStrategies/sessionInLocalStorage' +import type { SessionState } from './sessionState' +import { toSessionString } from './sessionState' import { processSessionStoreOperations, isLockEnabled, @@ -19,18 +19,18 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } [ { title: 'Cookie Storage', - sessionStore: initCookieStore(initConfiguration)!, + sessionStoreStrategy: initCookieStrategy(initConfiguration)!, stubStorageProvider: stubCookieProvider, storageKey: SESSION_COOKIE_NAME, }, { title: 'Local Storage', - sessionStore: initLocalStorage()!, + sessionStoreStrategy: initLocalStorageStrategy()!, stubStorageProvider: stubLocalStorageProvider, storageKey: LOCAL_STORAGE_KEY, }, ] as const -).forEach(({ title, sessionStore, stubStorageProvider, storageKey }) => { +).forEach(({ title, sessionStoreStrategy, stubStorageProvider, storageKey }) => { describe(`process operations mechanism with ${title}`, () => { let initialSession: SessionState let otherSession: SessionState @@ -39,7 +39,7 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } let stubStorage: StubStorage beforeEach(() => { - sessionStore.clearSession() + sessionStoreStrategy.clearSession() initialSession = { id: '123', created: '0' } otherSession = { id: '456', created: '100' } processSpy = jasmine.createSpy('process') @@ -53,49 +53,49 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } }) it('should persist session when process returns a value', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.returnValue({ ...otherSession }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) expect(processSpy).toHaveBeenCalledWith(initialSession) const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should clear session when process returns an empty value', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.returnValue({}) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) expect(processSpy).toHaveBeenCalledWith(initialSession) const expectedSession = {} - expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should not persist session when process returns undefined', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.returnValue(undefined) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) expect(processSpy).toHaveBeenCalledWith(initialSession) - expect(sessionStore.retrieveSession()).toEqual(initialSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(initialSession) expect(afterSpy).toHaveBeenCalledWith(initialSession) }) it('LOCK_MAX_TRIES value should not influence the behavior when lock mechanism is not enabled', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.returnValue({ ...otherSession }) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore, LOCK_MAX_TRIES) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy, LOCK_MAX_TRIES) expect(processSpy).toHaveBeenCalledWith(initialSession) const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) }) @@ -106,37 +106,37 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } }) it('should persist session when process returns a value', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.callFake((session) => ({ ...otherSession, lock: session.lock })) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) const expectedSession = { ...otherSession, expire: jasmine.any(String) } - expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should clear session when process returns an empty value', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.returnValue({}) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) const expectedSession = {} - expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession) expect(afterSpy).toHaveBeenCalledWith(expectedSession) }) it('should not persist session when process returns undefined', () => { - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.returnValue(undefined) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) expect(processSpy).toHaveBeenCalledWith({ ...initialSession, lock: jasmine.any(String) }) - expect(sessionStore.retrieveSession()).toEqual(initialSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(initialSession) expect(afterSpy).toHaveBeenCalledWith(initialSession) }) @@ -195,7 +195,7 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } }), }) initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) processSessionStoreOperations( @@ -217,12 +217,12 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } processed: 'processed', expire: jasmine.any(String), } - expect(sessionStore.retrieveSession()).toEqual(expectedSession) + expect(sessionStoreStrategy.retrieveSession()).toEqual(expectedSession) expect(afterSession).toEqual(expectedSession) done() }, }, - sessionStore + sessionStoreStrategy ) }) }) @@ -230,11 +230,11 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } it('should abort after a max number of retry', () => { const clock = mockClock() - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) stubStorage.setSpy.calls.reset() stubStorage.getSpy.and.returnValue(buildSessionString({ ...initialSession, lock: 'locked' })) - processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStore) + processSessionStoreOperations({ process: processSpy, after: afterSpy }, sessionStoreStrategy) const lockMaxTries = isLockEnabled() ? LOCK_MAX_TRIES : 0 const lockRetryDelay = isLockEnabled() ? LOCK_RETRY_DELAY : 0 @@ -254,14 +254,14 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } retryState: initialSession, }), }) - sessionStore.persistSession(initialSession) + sessionStoreStrategy.persistSession(initialSession) processSessionStoreOperations( { process: (session) => ({ ...session, value: 'foo' }), after: afterSpy, }, - sessionStore + sessionStoreStrategy ) processSessionStoreOperations( { @@ -272,7 +272,7 @@ const initConfiguration: InitConfiguration = { clientToken: 'abc' } done() }, }, - sessionStore + sessionStoreStrategy ) }) }) diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index a47558c0e0..d17e45d187 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -3,8 +3,9 @@ import { dateNow } from '../../tools/utils/timeUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { isChromium } from '../../tools/utils/browserDetection' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import type { SessionState, SessionStore } from './sessionStore' -import { isSessionInExpiredState } from './sessionStore' +import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' +import type { SessionState } from './sessionState' +import { isSessionInExpiredState } from './sessionState' type Operations = { process: (sessionState: SessionState) => SessionState | undefined @@ -16,8 +17,12 @@ export const LOCK_MAX_TRIES = 100 const bufferedOperations: Operations[] = [] let ongoingOperations: Operations | undefined -export function processSessionStoreOperations(operations: Operations, sessionStore: SessionStore, numberOfRetries = 0) { - const { retrieveSession, persistSession, clearSession } = sessionStore +export function processSessionStoreOperations( + operations: Operations, + sessionStoreStrategy: SessionStoreStrategy, + numberOfRetries = 0 +) { + const { retrieveSession, persistSession, clearSession } = sessionStoreStrategy const lockEnabled = isLockEnabled() if (!ongoingOperations) { @@ -28,7 +33,7 @@ export function processSessionStoreOperations(operations: Operations, sessionSto return } if (lockEnabled && numberOfRetries >= LOCK_MAX_TRIES) { - next(sessionStore) + next(sessionStoreStrategy) return } let currentLock: string @@ -36,7 +41,7 @@ export function processSessionStoreOperations(operations: Operations, sessionSto if (lockEnabled) { // if someone has lock, retry later if (currentSession.lock) { - retryLater(operations, sessionStore, numberOfRetries) + retryLater(operations, sessionStoreStrategy, numberOfRetries) return } // acquire lock @@ -46,7 +51,7 @@ export function processSessionStoreOperations(operations: Operations, sessionSto // if lock is not acquired, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock) { - retryLater(operations, sessionStore, numberOfRetries) + retryLater(operations, sessionStoreStrategy, numberOfRetries) return } } @@ -55,7 +60,7 @@ export function processSessionStoreOperations(operations: Operations, sessionSto // if lock corrupted after process, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStore, numberOfRetries) + retryLater(operations, sessionStoreStrategy, numberOfRetries) return } } @@ -74,7 +79,7 @@ export function processSessionStoreOperations(operations: Operations, sessionSto // if lock corrupted after persist, retry later currentSession = retrieveSession() if (currentSession.lock !== currentLock!) { - retryLater(operations, sessionStore, numberOfRetries) + retryLater(operations, sessionStoreStrategy, numberOfRetries) return } delete currentSession.lock @@ -85,7 +90,7 @@ export function processSessionStoreOperations(operations: Operations, sessionSto // call after even if session is not persisted in order to perform operations on // up-to-date session state value => the value could have been modified by another tab operations.after?.(processedSession || currentSession) - next(sessionStore) + next(sessionStoreStrategy) } /** @@ -94,13 +99,13 @@ export function processSessionStoreOperations(operations: Operations, sessionSto */ export const isLockEnabled = () => isChromium() -function retryLater(operations: Operations, sessionStore: SessionStore, currentNumberOfRetries: number) { +function retryLater(operations: Operations, sessionStore: SessionStoreStrategy, currentNumberOfRetries: number) { setTimeout(() => { processSessionStoreOperations(operations, sessionStore, currentNumberOfRetries + 1) }, LOCK_RETRY_DELAY) } -function next(sessionStore: SessionStore) { +function next(sessionStore: SessionStoreStrategy) { ongoingOperations = undefined const nextOperations = bufferedOperations.shift() if (nextOperations) { diff --git a/packages/core/src/domain/session/sessionCookieStore.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts similarity index 80% rename from packages/core/src/domain/session/sessionCookieStore.spec.ts rename to packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index 765ca55a72..731ef2797d 100644 --- a/packages/core/src/domain/session/sessionCookieStore.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,16 +1,17 @@ -import { setCookie, deleteCookie, getCurrentSite, getCookie } from '../../browser/cookie' -import type { InitConfiguration } from '../configuration' -import { SESSION_COOKIE_NAME, buildCookieOptions, initCookieStore } from './sessionCookieStore' +import { setCookie, deleteCookie, getCurrentSite, getCookie } from '../../../browser/cookie' +import type { InitConfiguration } from '../../configuration' +import type { SessionState } from '../sessionState' +import { SESSION_COOKIE_NAME, buildCookieOptions, initCookieStrategy } from './sessionInCookie' -import type { SessionState, SessionStore } from './sessionStore' +import type { SessionStoreStrategy } from './sessionStoreStrategy' describe('session cookie store', () => { const sessionState: SessionState = { id: '123', created: '0' } const initConfiguration: InitConfiguration = { clientToken: 'abc' } - let cookieStorage: SessionStore | undefined + let cookieStorageStrategy: SessionStoreStrategy beforeEach(() => { - cookieStorage = initCookieStore(initConfiguration) + cookieStorageStrategy = initCookieStrategy(initConfiguration)! }) afterEach(() => { @@ -18,23 +19,23 @@ describe('session cookie store', () => { }) it('should persist a session in a cookie', () => { - cookieStorage?.persistSession(sessionState) - const session = cookieStorage?.retrieveSession() + cookieStorageStrategy.persistSession(sessionState) + const session = cookieStorageStrategy?.retrieveSession() expect(session).toEqual({ ...sessionState }) expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=123&created=0') }) it('should delete the cookie holding the session', () => { - cookieStorage?.persistSession(sessionState) - cookieStorage?.clearSession() - const session = cookieStorage?.retrieveSession() + cookieStorageStrategy.persistSession(sessionState) + cookieStorageStrategy.clearSession() + const session = cookieStorageStrategy?.retrieveSession() expect(session).toEqual({}) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) it('should return an empty object if session string is invalid', () => { setCookie(SESSION_COOKIE_NAME, '{test:42}', 1000) - const session = cookieStorage?.retrieveSession() + const session = cookieStorageStrategy?.retrieveSession() expect(session).toEqual({}) }) @@ -89,7 +90,7 @@ describe('session cookie store', () => { ].forEach(({ description, initConfiguration, cookieString }) => { it(description, () => { const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') - initCookieStore(initConfiguration) + initCookieStrategy(initConfiguration) expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) }) }) diff --git a/packages/core/src/domain/session/sessionCookieStore.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts similarity index 70% rename from packages/core/src/domain/session/sessionCookieStore.ts rename to packages/core/src/domain/session/storeStrategies/sessionInCookie.ts index 70e6f7376d..e5ae5e10cf 100644 --- a/packages/core/src/domain/session/sessionCookieStore.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts @@ -1,14 +1,15 @@ -import type { CookieOptions } from '../../browser/cookie' -import { getCurrentSite, areCookiesAuthorized, deleteCookie, getCookie, setCookie } from '../../browser/cookie' -import type { InitConfiguration } from '../configuration' -import { tryOldCookiesMigration } from './oldCookiesMigration' -import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import type { SessionState, SessionStore } from './sessionStore' -import { toSessionState, toSessionString } from './sessionStore' +import type { CookieOptions } from '../../../browser/cookie' +import { getCurrentSite, areCookiesAuthorized, deleteCookie, getCookie, setCookie } from '../../../browser/cookie' +import type { InitConfiguration } from '../../configuration' +import { tryOldCookiesMigration } from '../oldCookiesMigration' +import { SESSION_EXPIRATION_DELAY } from '../sessionConstants' +import type { SessionState } from '../sessionState' +import { toSessionString, toSessionState } from '../sessionState' +import type { SessionStoreStrategy } from './sessionStoreStrategy' export const SESSION_COOKIE_NAME = '_dd_s' -export function initCookieStore(initConfiguration: InitConfiguration): SessionStore | undefined { +export function initCookieStrategy(initConfiguration: InitConfiguration): SessionStoreStrategy | undefined { const options = buildCookieOptions(initConfiguration) if (!areCookiesAuthorized(options)) { diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts similarity index 77% rename from packages/core/src/domain/session/sessionLocalStorageStore.spec.ts rename to packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts index cf8159f7a4..2b5f96f92b 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts @@ -1,5 +1,5 @@ -import { LOCAL_STORAGE_KEY, initLocalStorage } from './sessionLocalStorageStore' -import type { SessionState } from './sessionStore' +import type { SessionState } from '../sessionState' +import { LOCAL_STORAGE_KEY, initLocalStorageStrategy } from './sessionInLocalStorage' describe('session local storage store', () => { const sessionState: SessionState = { id: '123', created: '0' } @@ -9,18 +9,18 @@ describe('session local storage store', () => { }) it('should report local storage as available', () => { - const localStorageStore = initLocalStorage() + const localStorageStore = initLocalStorageStrategy() expect(localStorageStore).toBeDefined() }) it('should report local storage as not available', () => { spyOn(Storage.prototype, 'getItem').and.throwError('Unavailable') - const localStorageStore = initLocalStorage() + const localStorageStore = initLocalStorageStrategy() expect(localStorageStore).not.toBeDefined() }) it('should persist a session in local storage', () => { - const localStorageStore = initLocalStorage() + const localStorageStore = initLocalStorageStrategy() localStorageStore?.persistSession(sessionState) const session = localStorageStore?.retrieveSession() expect(session).toEqual({ ...sessionState }) @@ -28,7 +28,7 @@ describe('session local storage store', () => { }) it('should delete the local storage item holding the session', () => { - const localStorageStore = initLocalStorage() + const localStorageStore = initLocalStorageStrategy() localStorageStore?.persistSession(sessionState) localStorageStore?.clearSession() const session = localStorageStore?.retrieveSession() @@ -38,7 +38,7 @@ describe('session local storage store', () => { it('should not interfere with other keys present in local storage', () => { window.localStorage.setItem('test', 'hello') - const localStorageStore = initLocalStorage() + const localStorageStore = initLocalStorageStrategy() localStorageStore?.persistSession(sessionState) localStorageStore?.retrieveSession() localStorageStore?.clearSession() @@ -46,7 +46,7 @@ describe('session local storage store', () => { }) it('should return an empty object if session string is invalid', () => { - const localStorageStore = initLocalStorage() + const localStorageStore = initLocalStorageStrategy() localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') const session = localStorageStore?.retrieveSession() expect(session).toEqual({}) diff --git a/packages/core/src/domain/session/sessionLocalStorageStore.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts similarity index 74% rename from packages/core/src/domain/session/sessionLocalStorageStore.ts rename to packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index cfebc29b04..b931719a5d 100644 --- a/packages/core/src/domain/session/sessionLocalStorageStore.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -1,10 +1,11 @@ -import { generateUUID } from '../../tools/utils/stringUtils' -import type { SessionState, SessionStore } from './sessionStore' -import { toSessionString, toSessionState } from './sessionStore' +import { generateUUID } from '../../../tools/utils/stringUtils' +import type { SessionState } from '../sessionState' +import { toSessionString, toSessionState } from '../sessionState' +import type { SessionStoreStrategy } from './sessionStoreStrategy' export const LOCAL_STORAGE_KEY = '_dd_s' -export function initLocalStorage(): SessionStore | undefined { +export function initLocalStorageStrategy(): SessionStoreStrategy | undefined { if (!isLocalStorageAvailable()) { return undefined } diff --git a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts new file mode 100644 index 0000000000..333af3adbc --- /dev/null +++ b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts @@ -0,0 +1,7 @@ +import type { SessionState } from '../sessionState' + +export interface SessionStoreStrategy { + persistSession: (session: SessionState) => void + retrieveSession: () => SessionState + clearSession: () => void +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1de20c1a1a..e6cf9489c3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -41,7 +41,7 @@ export { } from './domain/telemetry' export { monitored, monitor, callMonitored, setDebugMode } from './tools/monitor' export { Observable, Subscription } from './tools/observable' -export { initSessionStore } from './domain/session/sessionStoreManager' +export { initSessionStoreStrategy as initSessionStore } from './domain/session/sessionStore' export { startSessionManager, SessionManager, @@ -102,7 +102,7 @@ export { } from './tools/serialisation/heavyCustomerDataWarning' export { ValueHistory, ValueHistoryEntry, CLEAR_OLD_VALUES_INTERVAL } from './tools/valueHistory' export { readBytesFromStream } from './tools/readBytesFromStream' -export { SESSION_COOKIE_NAME } from './domain/session/sessionCookieStore' +export { SESSION_COOKIE_NAME } from './domain/session/storeStrategies/sessionInCookie' export { willSyntheticsInjectRum, getSyntheticsTestId, From 83acc68f6c5d807ba1cc6e7bda31dde5dde02245 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 5 Jun 2023 23:13:46 +0200 Subject: [PATCH 24/40] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20Removed=20session?= =?UTF-8?q?Store=20from=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/configuration.spec.ts | 12 +- .../src/domain/configuration/configuration.ts | 21 +- .../session/oldCookiesMigration.spec.ts | 2 +- .../src/domain/session/sessionManager.spec.ts | 210 +++++++++++++++--- .../core/src/domain/session/sessionManager.ts | 7 +- .../src/domain/session/sessionStore.spec.ts | 57 +++-- .../core/src/domain/session/sessionStore.ts | 29 ++- .../session/sessionStoreOperations.spec.ts | 8 +- .../storeStrategies/sessionInCookie.spec.ts | 28 +-- .../storeStrategies/sessionInCookie.ts | 14 +- .../sessionInLocalStorage.spec.ts | 38 ++-- .../storeStrategies/sessionInLocalStorage.ts | 26 +-- .../storeStrategies/sessionStoreStrategy.ts | 8 + packages/core/src/index.ts | 1 - packages/logs/src/boot/startLogs.ts | 2 +- .../src/domain/logsSessionManager.spec.ts | 4 +- .../logs/src/domain/logsSessionManager.ts | 9 +- packages/rum-core/src/boot/rumPublicApi.ts | 2 +- .../rum-core/src/domain/rumSessionManager.ts | 9 +- 19 files changed, 333 insertions(+), 154 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index b0b6d69045..5ea483684b 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -120,22 +120,26 @@ describe('validateAndBuildConfiguration', () => { describe('cookie options', () => { it('should not be secure nor crossSite by default', () => { const configuration = validateAndBuildConfiguration({ clientToken })! - expect(configuration.cookieOptions).toEqual({ secure: false, crossSite: false }) + expect(configuration.sessionStoreOptions.cookie).toEqual({ secure: false, crossSite: false }) }) it('should be secure when `useSecureSessionCookie` is truthy', () => { const configuration = validateAndBuildConfiguration({ clientToken, useSecureSessionCookie: true })! - expect(configuration.cookieOptions).toEqual({ secure: true, crossSite: false }) + expect(configuration.sessionStoreOptions.cookie).toEqual({ secure: true, crossSite: false }) }) it('should be secure and crossSite when `useCrossSiteSessionCookie` is truthy', () => { const configuration = validateAndBuildConfiguration({ clientToken, useCrossSiteSessionCookie: true })! - expect(configuration.cookieOptions).toEqual({ secure: true, crossSite: true }) + expect(configuration.sessionStoreOptions.cookie).toEqual({ secure: true, crossSite: true }) }) it('should have domain when `trackSessionAcrossSubdomains` is truthy', () => { const configuration = validateAndBuildConfiguration({ clientToken, trackSessionAcrossSubdomains: true })! - expect(configuration.cookieOptions).toEqual({ secure: false, crossSite: false, domain: jasmine.any(String) }) + expect(configuration.sessionStoreOptions.cookie).toEqual({ + secure: false, + crossSite: false, + domain: jasmine.any(String), + }) }) }) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 9eabada83e..a1e1632714 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -8,9 +8,8 @@ import { isPercentage } from '../../tools/utils/numberUtils' import { ONE_KIBI_BYTE } from '../../tools/utils/byteUtils' import { objectHasValue } from '../../tools/utils/objectUtils' import { assign } from '../../tools/utils/polyfills' -import { initSessionStoreStrategy } from '../session/sessionStore' -import type { SessionStoreStrategy } from '../session/storeStrategies/sessionStoreStrategy' -import type { CookieOptions } from '../../browser/cookie' +import { getSessionStoreStrategyType } from '../session/sessionStore' +import type { SessionStoreOptions, SessionStoreStrategyType } from '../session/storeStrategies/sessionStoreStrategy' import { buildCookieOptions } from '../session/storeStrategies/sessionInCookie' import type { TransportConfiguration } from './transportConfiguration' import { computeTransportConfiguration } from './transportConfiguration' @@ -78,9 +77,8 @@ interface ReplicaUserConfiguration { export interface Configuration extends TransportConfiguration { // Built from init configuration beforeSend: GenericBeforeSendCallback | undefined - cookieOptions: CookieOptions - allowFallbackToLocalStorage: boolean - sessionStore: SessionStoreStrategy | undefined + sessionStoreOptions: SessionStoreOptions + sessionStoreStrategyType: SessionStoreStrategyType | undefined sessionSampleRate: number telemetrySampleRate: number telemetryConfigurationSampleRate: number @@ -132,13 +130,18 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati ) } + // Build Session Store options + const sessionStoreOptions: SessionStoreOptions = { + cookie: buildCookieOptions(initConfiguration), + allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, + } + return assign( { beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), - cookieOptions: buildCookieOptions(initConfiguration), - allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, - sessionStore: initSessionStoreStrategy(initConfiguration), + sessionStoreOptions, + sessionStoreStrategyType: getSessionStoreStrategyType(sessionStoreOptions), sessionSampleRate: sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index 551e429e3b..791d49b38d 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -9,7 +9,7 @@ import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { SESSION_COOKIE_NAME, initCookieStrategy } from './storeStrategies/sessionInCookie' describe('old cookies migration', () => { - const sessionStoreStrategy = initCookieStrategy({ clientToken: '123' })! + const sessionStoreStrategy = initCookieStrategy({}) it('should not touch current cookie', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 21ec3c3da1..8ec97ffbe3 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -5,11 +5,11 @@ import type { RelativeTime } from '../../tools/utils/timeUtils' import { isIE } from '../../tools/utils/browserDetection' import { DOM_EVENT } from '../../browser/addEventListener' import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' -import type { InitConfiguration } from '../configuration' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' -import { SESSION_COOKIE_NAME, initCookieStrategy } from './storeStrategies/sessionInCookie' +import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import type { SessionStoreOptions, SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' const enum FakeTrackingType { NOT_TRACKED = 'not-tracked', @@ -30,8 +30,8 @@ describe('startSessionManager', () => { const DURATION = 123456 const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' - const initConfiguration: InitConfiguration = { clientToken: 'abc' } - const sessionStore = initCookieStrategy(initConfiguration)! + const sessionStoreStrategyType: SessionStoreStrategyType = 'COOKIE' + const sessionStoreOptions: SessionStoreOptions = { cookie: {}, allowFallbackToLocalStorage: false } let clock: Clock function expireSessionCookie() { @@ -85,14 +85,24 @@ describe('startSessionManager', () => { describe('cookie management', () => { it('when tracked, should store tracking type and session id', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) expectSessionIdToBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) it('when not tracked should store tracking type', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => NOT_TRACKED_SESSION_STATE + ) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -101,7 +111,12 @@ describe('startSessionManager', () => { it('when tracked should keep existing tracking type and session id', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcdef&first=tracked', DURATION) - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) expectSessionIdToBe(sessionManager, 'abcdef') expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) @@ -110,7 +125,12 @@ describe('startSessionManager', () => { it('when not tracked should keep existing tracking type', () => { setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => NOT_TRACKED_SESSION_STATE + ) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -125,32 +145,37 @@ describe('startSessionManager', () => { }) it('should be called with an empty value if the cookie is not defined', () => { - startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(undefined) }) it('should be called with an invalid value if the cookie has an invalid value', () => { setCookie(SESSION_COOKIE_NAME, 'first=invalid', DURATION) - startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith('invalid') }) it('should be called with TRACKED', () => { setCookie(SESSION_COOKIE_NAME, 'first=tracked', DURATION) - startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) it('should be called with NOT_TRACKED', () => { setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) - startSessionManager(sessionStore, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) }) describe('session renewal', () => { it('should renew on activity after expiration', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -168,7 +193,12 @@ describe('startSessionManager', () => { }) it('should not renew on visibility after expiration', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -183,17 +213,27 @@ describe('startSessionManager', () => { describe('multiple startSessionManager calls', () => { it('should re-use the same session id', () => { - const firstSessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const idA = firstSessionManager.findActiveSession()!.id - const secondSessionManager = startSessionManager(sessionStore, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + SECOND_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const idB = secondSessionManager.findActiveSession()!.id expect(idA).toBe(idB) }) it('should not erase other session type', () => { - startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // schedule an expandOrRenewSession document.dispatchEvent(new CustomEvent('click')) @@ -203,7 +243,12 @@ describe('startSessionManager', () => { // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) - startSessionManager(sessionStore, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + SECOND_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) // cookie correctly set expect(getCookie(SESSION_COOKIE_NAME)).toContain('first') @@ -217,9 +262,15 @@ describe('startSessionManager', () => { }) it('should have independent tracking types', () => { - const firstSessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const secondSessionManager = startSessionManager( - sessionStore, + sessionStoreStrategyType, + sessionStoreOptions, SECOND_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -229,13 +280,23 @@ describe('startSessionManager', () => { }) it('should notify each expire and renew observables', () => { - const firstSessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const firstSessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionASpy = jasmine.createSpy() firstSessionManager.expireObservable.subscribe(expireSessionASpy) const renewSessionASpy = jasmine.createSpy() firstSessionManager.renewObservable.subscribe(renewSessionASpy) - const secondSessionManager = startSessionManager(sessionStore, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + SECOND_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionBSpy = jasmine.createSpy() secondSessionManager.expireObservable.subscribe(expireSessionBSpy) const renewSessionBSpy = jasmine.createSpy() @@ -257,7 +318,12 @@ describe('startSessionManager', () => { describe('session timeout', () => { it('should expire the session when the time out delay is reached', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -273,7 +339,12 @@ describe('startSessionManager', () => { it('should renew an existing timed out session', () => { setCookie(SESSION_COOKIE_NAME, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -285,7 +356,12 @@ describe('startSessionManager', () => { it('should not add created date to an existing session from an older versions', () => { setCookie(SESSION_COOKIE_NAME, 'id=abcde&first=tracked', DURATION) - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) expect(sessionManager.findActiveSession()!.id).toBe('abcde') expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('created=') @@ -302,7 +378,12 @@ describe('startSessionManager', () => { }) it('should expire the session after expiration delay', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -314,7 +395,12 @@ describe('startSessionManager', () => { }) it('should expand duration on activity', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -333,7 +419,12 @@ describe('startSessionManager', () => { }) it('should expand not tracked session duration on activity', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => NOT_TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -354,7 +445,12 @@ describe('startSessionManager', () => { it('should expand session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -375,7 +471,12 @@ describe('startSessionManager', () => { it('should expand not tracked session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => NOT_TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -396,7 +497,12 @@ describe('startSessionManager', () => { describe('manual session expiration', () => { it('expires the session when calling expire()', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -407,7 +513,12 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() multiple times', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -419,7 +530,12 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() after the session has been expired', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -431,7 +547,12 @@ describe('startSessionManager', () => { }) it('renew the session on user activity', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) clock.tick(COOKIE_ACCESS_DELAY) sessionManager.expire() @@ -444,21 +565,36 @@ describe('startSessionManager', () => { describe('session history', () => { it('should return undefined when there is no current session and no startTime', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) expireSessionCookie() expect(sessionManager.findActiveSession()).toBeUndefined() }) it('should return the current session context when there is no start time', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) expect(sessionManager.findActiveSession()!.id).toBeDefined() expect(sessionManager.findActiveSession()!.trackingType).toBeDefined() }) it('should return the session context corresponding to startTime', () => { - const sessionManager = startSessionManager(sessionStore, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const sessionManager = startSessionManager( + sessionStoreStrategyType, + sessionStoreOptions, + FIRST_PRODUCT_KEY, + () => TRACKED_SESSION_STATE + ) // 0s to 10s: first session clock.tick(10 * ONE_SECOND - COOKIE_ACCESS_DELAY) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index b87a837f88..7bd6be3891 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -7,7 +7,7 @@ import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/ad import { clearInterval, setInterval } from '../../tools/timer' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { startSessionStore } from './sessionStore' -import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' +import type { SessionStoreOptions, SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' export interface SessionManager { findActiveSession: (startTime?: RelativeTime) => SessionContext | undefined @@ -26,11 +26,12 @@ const SESSION_CONTEXT_TIMEOUT_DELAY = SESSION_TIME_OUT_DELAY let stopCallbacks: Array<() => void> = [] export function startSessionManager( - sessionStoreStrategy: SessionStoreStrategy, + sessionStoreStrategyType: SessionStoreStrategyType, + sessionStoreOptions: SessionStoreOptions, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionManager { - const sessionStore = startSessionStore(sessionStoreStrategy, productKey, computeSessionState) + const sessionStore = startSessionStore(sessionStoreStrategyType, sessionStoreOptions, productKey, computeSessionState) stopCallbacks.push(() => sessionStore.stop()) const sessionContextHistory = new ValueHistory>(SESSION_CONTEXT_TIMEOUT_DELAY) diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 18fd372511..c435d2730a 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,10 +1,12 @@ import type { Clock } from '../../../test' import { mockClock } from '../../../test' +import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import type { SessionStore } from './sessionStore' -import { initSessionStoreStrategy, startSessionStore } from './sessionStore' +import { startSessionStore, getSessionStoreStrategyType } from './sessionStore' import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import type { SessionStoreOptions } from './storeStrategies/sessionStoreStrategy' const enum FakeTrackingType { TRACKED = 'tracked', @@ -15,7 +17,6 @@ const DURATION = 123456 const PRODUCT_KEY = 'product' const FIRST_ID = 'first' const SECOND_ID = 'second' -const clientToken = 'abc' function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { setCookie( @@ -46,29 +47,43 @@ function resetSessionInStore() { } describe('session store', () => { - describe('initSessionStoreStrategy', () => { - it('should initialize storage when cookies are available', () => { - const sessionStore = initSessionStoreStrategy({ clientToken }) - expect(sessionStore).toBeDefined() + const cookieOptions: CookieOptions = {} + + describe('getSessionStoreStrategyType', () => { + it('should return "COOKIE" when cookies are available', () => { + const sessionStoreStrategyType = getSessionStoreStrategyType({ + cookie: cookieOptions, + allowFallbackToLocalStorage: true, + }) + expect(sessionStoreStrategyType).toBe('COOKIE') }) - it('should report false when cookies are not available, and fallback is not allowed', () => { + it('should report "NO_STORAGE_AVAILABLE" when cookies are not available, and fallback is not allowed', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStore = initSessionStoreStrategy({ clientToken, allowFallbackToLocalStorage: false }) - expect(sessionStore).not.toBeDefined() + const sessionStoreStrategyType = getSessionStoreStrategyType({ + cookie: cookieOptions, + allowFallbackToLocalStorage: false, + }) + expect(sessionStoreStrategyType).toBeUndefined() }) - it('should fallback to localStorage and report true when cookies are not available', () => { + it('should fallback to localStorage when cookies are not available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStore = initSessionStoreStrategy({ clientToken, allowFallbackToLocalStorage: true }) - expect(sessionStore).toBeDefined() + const sessionStoreStrategyType = getSessionStoreStrategyType({ + cookie: cookieOptions, + allowFallbackToLocalStorage: true, + }) + expect(sessionStoreStrategyType).toBe('LOCAL_STORAGE') }) - it('should report false when no storage is available', () => { + it('should report "NO_STORAGE_AVAILABLE" when no storage is available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') - const sessionStore = initSessionStoreStrategy({ clientToken, allowFallbackToLocalStorage: true }) - expect(sessionStore).not.toBeDefined() + const sessionStoreStrategyType = getSessionStoreStrategyType({ + cookie: cookieOptions, + allowFallbackToLocalStorage: true, + }) + expect(sessionStoreStrategyType).toBeUndefined() }) }) @@ -87,12 +102,18 @@ describe('session store', () => { trackingType: FakeTrackingType.TRACKED, }) ) { - const sessionStore = initSessionStoreStrategy({ clientToken }) - if (!sessionStore) { + const sessionStoreOptions: SessionStoreOptions = { cookie: cookieOptions, allowFallbackToLocalStorage: false } + const sessionStoreStrategyType = getSessionStoreStrategyType(sessionStoreOptions) + if (sessionStoreStrategyType !== 'COOKIE') { fail('Unable to initialize cookie storage') return } - sessionStoreManager = startSessionStore(sessionStore, PRODUCT_KEY, computeSessionState) + sessionStoreManager = startSessionStore( + sessionStoreStrategyType, + sessionStoreOptions, + PRODUCT_KEY, + computeSessionState + ) sessionStoreManager.expireObservable.subscribe(expireSpy) sessionStoreManager.renewObservable.subscribe(renewSpy) } diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index a3f400ab9d..b27f87c5f8 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -3,12 +3,11 @@ import { Observable } from '../../tools/observable' import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' -import type { InitConfiguration } from '../configuration' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { initCookieStrategy } from './storeStrategies/sessionInCookie' -import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' +import { checkCookieAvailability, initCookieStrategy } from './storeStrategies/sessionInCookie' +import type { SessionStoreOptions, SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' -import { initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' +import { checkLocalStorageAvailability, initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' import { processSessionStoreOperations } from './sessionStoreOperations' export interface SessionStore { @@ -27,13 +26,18 @@ const POLL_DELAY = ONE_SECOND * Checks if cookies are available as the preferred storage * Else, checks if LocalStorage is allowed and available */ -export function initSessionStoreStrategy(initConfiguration: InitConfiguration): SessionStoreStrategy | undefined { - let sessionStoreStrategy = initCookieStrategy(initConfiguration) - - if (!sessionStoreStrategy && initConfiguration.allowFallbackToLocalStorage) { - sessionStoreStrategy = initLocalStorageStrategy() +export function getSessionStoreStrategyType( + sessionStoreOptions: SessionStoreOptions +): SessionStoreStrategyType | undefined { + let sessionStoreStrategyType: SessionStoreStrategyType | undefined + + if (checkCookieAvailability(sessionStoreOptions.cookie)) { + sessionStoreStrategyType = 'COOKIE' + } else if (sessionStoreOptions.allowFallbackToLocalStorage && checkLocalStorageAvailability()) { + sessionStoreStrategyType = 'LOCAL_STORAGE' } - return sessionStoreStrategy + + return sessionStoreStrategyType } /** @@ -43,13 +47,16 @@ export function initSessionStoreStrategy(initConfiguration: InitConfiguration): * - inactive, no session in store or session expired, waiting for a renew session */ export function startSessionStore( - sessionStoreStrategy: SessionStoreStrategy, + sessionStoreStrategyType: SessionStoreStrategyType, + sessionStoreOptions: SessionStoreOptions, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionStore { const renewObservable = new Observable() const expireObservable = new Observable() + const sessionStoreStrategy = + sessionStoreStrategyType === 'COOKIE' ? initCookieStrategy(sessionStoreOptions.cookie) : initLocalStorageStrategy() const { clearSession, retrieveSession } = sessionStoreStrategy const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 1fb73e93e3..396516e551 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -1,6 +1,6 @@ import type { StubStorage } from '../../../test' import { mockClock, stubCookieProvider, stubLocalStorageProvider } from '../../../test' -import type { InitConfiguration } from '../configuration' +import type { CookieOptions } from '../../browser/cookie' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { initCookieStrategy, SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { initLocalStorageStrategy, LOCAL_STORAGE_KEY } from './storeStrategies/sessionInLocalStorage' @@ -13,19 +13,19 @@ import { LOCK_RETRY_DELAY, } from './sessionStoreOperations' -const initConfiguration: InitConfiguration = { clientToken: 'abc' } +const cookieOptions: CookieOptions = {} ;( [ { title: 'Cookie Storage', - sessionStoreStrategy: initCookieStrategy(initConfiguration)!, + sessionStoreStrategy: initCookieStrategy(cookieOptions), stubStorageProvider: stubCookieProvider, storageKey: SESSION_COOKIE_NAME, }, { title: 'Local Storage', - sessionStoreStrategy: initLocalStorageStrategy()!, + sessionStoreStrategy: initLocalStorageStrategy(), stubStorageProvider: stubLocalStorageProvider, storageKey: LOCAL_STORAGE_KEY, }, diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index 731ef2797d..b384b676f9 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,17 +1,15 @@ -import { setCookie, deleteCookie, getCurrentSite, getCookie } from '../../../browser/cookie' -import type { InitConfiguration } from '../../configuration' +import { setCookie, deleteCookie, getCookie } from '../../../browser/cookie' import type { SessionState } from '../sessionState' -import { SESSION_COOKIE_NAME, buildCookieOptions, initCookieStrategy } from './sessionInCookie' +import { SESSION_COOKIE_NAME, buildCookieOptions, checkCookieAvailability, initCookieStrategy } from './sessionInCookie' import type { SessionStoreStrategy } from './sessionStoreStrategy' -describe('session cookie store', () => { +describe('session in cookie strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } - const initConfiguration: InitConfiguration = { clientToken: 'abc' } let cookieStorageStrategy: SessionStoreStrategy beforeEach(() => { - cookieStorageStrategy = initCookieStrategy(initConfiguration)! + cookieStorageStrategy = initCookieStrategy({}) }) afterEach(() => { @@ -20,7 +18,7 @@ describe('session cookie store', () => { it('should persist a session in a cookie', () => { cookieStorageStrategy.persistSession(sessionState) - const session = cookieStorageStrategy?.retrieveSession() + const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState }) expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=123&created=0') }) @@ -28,14 +26,14 @@ describe('session cookie store', () => { it('should delete the cookie holding the session', () => { cookieStorageStrategy.persistSession(sessionState) cookieStorageStrategy.clearSession() - const session = cookieStorageStrategy?.retrieveSession() + const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({}) expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() }) it('should return an empty object if session string is invalid', () => { setCookie(SESSION_COOKIE_NAME, '{test:42}', 1000) - const session = cookieStorageStrategy?.retrieveSession() + const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({}) }) @@ -67,30 +65,32 @@ describe('session cookie store', () => { ;[ { initConfiguration: { clientToken: 'abc' }, + cookieOptions: {}, cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict$/, description: 'should set samesite to strict by default', }, { initConfiguration: { clientToken: 'abc', useCrossSiteSessionCookie: true }, + cookieOptions: { crossSite: true, secure: true }, cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=none;secure$/, description: 'should set samesite to none and secure to true for crossSite', }, { initConfiguration: { clientToken: 'abc', useSecureSessionCookie: true }, + cookieOptions: { secure: true }, cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;secure$/, description: 'should add secure attribute when defined', }, { initConfiguration: { clientToken: 'abc', trackSessionAcrossSubdomains: true }, - cookieString: new RegExp( - `^dd_cookie_test_[\\w-]+=[^;]*;expires=[^;]+;path=\\/;samesite=strict;domain=${getCurrentSite()}$` - ), + cookieOptions: { domain: 'foo.bar' }, + cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;domain=foo.bar$/, description: 'should set cookie domain when tracking accross subdomains', }, - ].forEach(({ description, initConfiguration, cookieString }) => { + ].forEach(({ description, cookieOptions, cookieString }) => { it(description, () => { const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') - initCookieStrategy(initConfiguration) + checkCookieAvailability(cookieOptions) expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) }) }) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts index e5ae5e10cf..d5d1a8dae5 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts @@ -9,17 +9,15 @@ import type { SessionStoreStrategy } from './sessionStoreStrategy' export const SESSION_COOKIE_NAME = '_dd_s' -export function initCookieStrategy(initConfiguration: InitConfiguration): SessionStoreStrategy | undefined { - const options = buildCookieOptions(initConfiguration) - - if (!areCookiesAuthorized(options)) { - return undefined - } +export function checkCookieAvailability(cookieOptions: CookieOptions) { + return areCookiesAuthorized(cookieOptions) +} +export function initCookieStrategy(cookieOptions: CookieOptions): SessionStoreStrategy { const cookieStore = { - persistSession: persistSessionCookie(options), + persistSession: persistSessionCookie(cookieOptions), retrieveSession: retrieveSessionCookie, - clearSession: deleteSessionCookie(options), + clearSession: deleteSessionCookie(cookieOptions), } tryOldCookiesMigration(SESSION_COOKIE_NAME, cookieStore) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts index 2b5f96f92b..0af493b5d2 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts @@ -1,7 +1,7 @@ import type { SessionState } from '../sessionState' -import { LOCAL_STORAGE_KEY, initLocalStorageStrategy } from './sessionInLocalStorage' +import { LOCAL_STORAGE_KEY, checkLocalStorageAvailability, initLocalStorageStrategy } from './sessionInLocalStorage' -describe('session local storage store', () => { +describe('session in local storage strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } afterEach(() => { @@ -9,46 +9,46 @@ describe('session local storage store', () => { }) it('should report local storage as available', () => { - const localStorageStore = initLocalStorageStrategy() - expect(localStorageStore).toBeDefined() + const available = checkLocalStorageAvailability() + expect(available).toBe(true) }) it('should report local storage as not available', () => { spyOn(Storage.prototype, 'getItem').and.throwError('Unavailable') - const localStorageStore = initLocalStorageStrategy() - expect(localStorageStore).not.toBeDefined() + const available = checkLocalStorageAvailability() + expect(available).toBe(false) }) it('should persist a session in local storage', () => { - const localStorageStore = initLocalStorageStrategy() - localStorageStore?.persistSession(sessionState) - const session = localStorageStore?.retrieveSession() + const localStorageStrategy = initLocalStorageStrategy() + localStorageStrategy.persistSession(sessionState) + const session = localStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState }) expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toMatch(/.*id=.*created/) }) it('should delete the local storage item holding the session', () => { - const localStorageStore = initLocalStorageStrategy() - localStorageStore?.persistSession(sessionState) - localStorageStore?.clearSession() - const session = localStorageStore?.retrieveSession() + const localStorageStrategy = initLocalStorageStrategy() + localStorageStrategy.persistSession(sessionState) + localStorageStrategy.clearSession() + const session = localStorageStrategy?.retrieveSession() expect(session).toEqual({}) expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull() }) it('should not interfere with other keys present in local storage', () => { window.localStorage.setItem('test', 'hello') - const localStorageStore = initLocalStorageStrategy() - localStorageStore?.persistSession(sessionState) - localStorageStore?.retrieveSession() - localStorageStore?.clearSession() + const localStorageStrategy = initLocalStorageStrategy() + localStorageStrategy.persistSession(sessionState) + localStorageStrategy.retrieveSession() + localStorageStrategy.clearSession() expect(window.localStorage.getItem('test')).toEqual('hello') }) it('should return an empty object if session string is invalid', () => { - const localStorageStore = initLocalStorageStrategy() + const localStorageStrategy = initLocalStorageStrategy() localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') - const session = localStorageStore?.retrieveSession() + const session = localStorageStrategy?.retrieveSession() expect(session).toEqual({}) }) }) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index b931719a5d..d008fc2ae8 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -5,11 +5,19 @@ import type { SessionStoreStrategy } from './sessionStoreStrategy' export const LOCAL_STORAGE_KEY = '_dd_s' -export function initLocalStorageStrategy(): SessionStoreStrategy | undefined { - if (!isLocalStorageAvailable()) { - return undefined +export function checkLocalStorageAvailability() { + try { + const id = generateUUID() + localStorage.setItem(`_dd_s_${id}`, id) + const retrievedId = localStorage.getItem(`_dd_s_${id}`) + localStorage.removeItem(`_dd_s_${id}`) + return id === retrievedId + } catch (e) { + return false } +} +export function initLocalStorageStrategy(): SessionStoreStrategy { return { persistSession: persistInLocalStorage, retrieveSession: retrieveSessionFromLocalStorage, @@ -29,15 +37,3 @@ function retrieveSessionFromLocalStorage(): SessionState { function clearSessionFromLocalStorage() { localStorage.removeItem(LOCAL_STORAGE_KEY) } - -function isLocalStorageAvailable() { - try { - const id = generateUUID() - localStorage.setItem(`_dd_s_${id}`, id) - const retrievedId = localStorage.getItem(`_dd_s_${id}`) - localStorage.removeItem(`_dd_s_${id}`) - return id === retrievedId - } catch (e) { - return false - } -} diff --git a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts index 333af3adbc..3fe2d8b5f3 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts @@ -1,5 +1,13 @@ +import type { CookieOptions } from '../../../browser/cookie' import type { SessionState } from '../sessionState' +export type SessionStoreStrategyType = 'COOKIE' | 'LOCAL_STORAGE' + +export interface SessionStoreOptions { + allowFallbackToLocalStorage: boolean + cookie: CookieOptions +} + export interface SessionStoreStrategy { persistSession: (session: SessionState) => void retrieveSession: () => SessionState diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e6cf9489c3..0816102421 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -41,7 +41,6 @@ export { } from './domain/telemetry' export { monitored, monitor, callMonitored, setDebugMode } from './tools/monitor' export { Observable, Subscription } from './tools/observable' -export { initSessionStoreStrategy as initSessionStore } from './domain/session/sessionStore' export { startSessionManager, SessionManager, diff --git a/packages/logs/src/boot/startLogs.ts b/packages/logs/src/boot/startLogs.ts index 53154a1b41..b436893a80 100644 --- a/packages/logs/src/boot/startLogs.ts +++ b/packages/logs/src/boot/startLogs.ts @@ -54,7 +54,7 @@ export function startLogs( const pageExitObservable = createPageExitObservable() const session = - configuration.sessionStore && !canUseEventBridge() && !willSyntheticsInjectRum() + configuration.sessionStoreStrategyType && !canUseEventBridge() && !willSyntheticsInjectRum() ? startLogsSessionManager(configuration) : startLogsSessionManagerStub(configuration) diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 8b4ebbba24..6d4d01a378 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -1,6 +1,5 @@ import type { RelativeTime } from '@datadog/browser-core' import { - initSessionStore, COOKIE_ACCESS_DELAY, getCookie, SESSION_COOKIE_NAME, @@ -23,7 +22,8 @@ describe('logs session manager', () => { const DURATION = 123456 const configuration: Partial = { sessionSampleRate: 0.5, - sessionStore: initSessionStore({ clientToken: 'abc' }), + sessionStoreStrategyType: 'COOKIE', + sessionStoreOptions: { cookie: {}, allowFallbackToLocalStorage: false }, } let clock: Clock let tracked: boolean diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index 57c67ec70c..e63f74b8db 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -19,12 +19,15 @@ export const enum LoggerTrackingType { } export function startLogsSessionManager(configuration: LogsConfiguration): LogsSessionManager { - if (!configuration.sessionStore) { + if (!configuration.sessionStoreStrategyType) { throw new Error('Cannot initialize Logs Session Manager without a storage.') } - const sessionManager = startSessionManager(configuration.sessionStore, LOGS_SESSION_KEY, (rawTrackingType) => - computeSessionState(configuration, rawTrackingType) + const sessionManager = startSessionManager( + configuration.sessionStoreStrategyType, + configuration.sessionStoreOptions, + LOGS_SESSION_KEY, + (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) return { findTrackedSession: (startTime) => { diff --git a/packages/rum-core/src/boot/rumPublicApi.ts b/packages/rum-core/src/boot/rumPublicApi.ts index 9b2119213b..5727916997 100644 --- a/packages/rum-core/src/boot/rumPublicApi.ts +++ b/packages/rum-core/src/boot/rumPublicApi.ts @@ -122,7 +122,7 @@ export function makeRumPublicApi( return } - if (!eventBridgeAvailable && !configuration.sessionStore) { + if (!eventBridgeAvailable && !configuration.sessionStoreStrategyType) { display.warn('No storage available for session. We will not send any data.') return } diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index 527e55c322..cd8d525f38 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -35,12 +35,15 @@ export const enum RumTrackingType { } export function startRumSessionManager(configuration: RumConfiguration, lifeCycle: LifeCycle): RumSessionManager { - if (!configuration.sessionStore) { + if (!configuration.sessionStoreStrategyType) { throw new Error('Cannot initialize RUM Session Manager without a storage.') } - const sessionManager = startSessionManager(configuration.sessionStore, RUM_SESSION_KEY, (rawTrackingType) => - computeSessionState(configuration, rawTrackingType) + const sessionManager = startSessionManager( + configuration.sessionStoreStrategyType, + configuration.sessionStoreOptions, + RUM_SESSION_KEY, + (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) sessionManager.expireObservable.subscribe(() => { From 7159723338bceba03fcba723a0a1245ca7bf0b5e Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Wed, 7 Jun 2023 10:13:22 +0200 Subject: [PATCH 25/40] =?UTF-8?q?=F0=9F=91=8C=20Remove=20allowFallbackToLo?= =?UTF-8?q?calStorage=20from=20SessionStoreOptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit to rebase --- .../src/domain/configuration/configuration.ts | 8 ++-- .../src/domain/session/sessionManager.spec.ts | 2 +- .../src/domain/session/sessionStore.spec.ts | 46 +++++++++++-------- .../core/src/domain/session/sessionStore.ts | 7 +-- .../storeStrategies/sessionStoreStrategy.ts | 1 - .../src/domain/logsSessionManager.spec.ts | 2 +- 6 files changed, 38 insertions(+), 28 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index a1e1632714..df6023c49b 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -8,7 +8,7 @@ import { isPercentage } from '../../tools/utils/numberUtils' import { ONE_KIBI_BYTE } from '../../tools/utils/byteUtils' import { objectHasValue } from '../../tools/utils/objectUtils' import { assign } from '../../tools/utils/polyfills' -import { getSessionStoreStrategyType } from '../session/sessionStore' +import { selectSessionStoreStrategyType } from '../session/sessionStore' import type { SessionStoreOptions, SessionStoreStrategyType } from '../session/storeStrategies/sessionStoreStrategy' import { buildCookieOptions } from '../session/storeStrategies/sessionInCookie' import type { TransportConfiguration } from './transportConfiguration' @@ -133,7 +133,6 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati // Build Session Store options const sessionStoreOptions: SessionStoreOptions = { cookie: buildCookieOptions(initConfiguration), - allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, } return assign( @@ -141,7 +140,10 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), sessionStoreOptions, - sessionStoreStrategyType: getSessionStoreStrategyType(sessionStoreOptions), + sessionStoreStrategyType: selectSessionStoreStrategyType( + sessionStoreOptions, + !!initConfiguration.allowFallbackToLocalStorage + ), sessionSampleRate: sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 8ec97ffbe3..2f700d94ff 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -31,7 +31,7 @@ describe('startSessionManager', () => { const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' const sessionStoreStrategyType: SessionStoreStrategyType = 'COOKIE' - const sessionStoreOptions: SessionStoreOptions = { cookie: {}, allowFallbackToLocalStorage: false } + const sessionStoreOptions: SessionStoreOptions = { cookie: {} } let clock: Clock function expireSessionCookie() { diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index c435d2730a..5bbc486435 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -3,7 +3,7 @@ import { mockClock } from '../../../test' import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import type { SessionStore } from './sessionStore' -import { startSessionStore, getSessionStoreStrategyType } from './sessionStore' +import { startSessionStore, selectSessionStoreStrategyType } from './sessionStore' import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import type { SessionStoreOptions } from './storeStrategies/sessionStoreStrategy' @@ -51,38 +51,46 @@ describe('session store', () => { describe('getSessionStoreStrategyType', () => { it('should return "COOKIE" when cookies are available', () => { - const sessionStoreStrategyType = getSessionStoreStrategyType({ - cookie: cookieOptions, - allowFallbackToLocalStorage: true, - }) + const sessionStoreStrategyType = selectSessionStoreStrategyType( + { + cookie: cookieOptions, + }, + true + ) expect(sessionStoreStrategyType).toBe('COOKIE') }) it('should report "NO_STORAGE_AVAILABLE" when cookies are not available, and fallback is not allowed', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStoreStrategyType = getSessionStoreStrategyType({ - cookie: cookieOptions, - allowFallbackToLocalStorage: false, - }) + const sessionStoreStrategyType = selectSessionStoreStrategyType( + { + cookie: cookieOptions, + }, + false + ) expect(sessionStoreStrategyType).toBeUndefined() }) it('should fallback to localStorage when cookies are not available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStoreStrategyType = getSessionStoreStrategyType({ - cookie: cookieOptions, - allowFallbackToLocalStorage: true, - }) + const sessionStoreStrategyType = selectSessionStoreStrategyType( + { + cookie: cookieOptions, + }, + true + ) expect(sessionStoreStrategyType).toBe('LOCAL_STORAGE') }) it('should report "NO_STORAGE_AVAILABLE" when no storage is available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') - const sessionStoreStrategyType = getSessionStoreStrategyType({ - cookie: cookieOptions, - allowFallbackToLocalStorage: true, - }) + const sessionStoreStrategyType = selectSessionStoreStrategyType( + { + cookie: cookieOptions, + }, + true + ) expect(sessionStoreStrategyType).toBeUndefined() }) }) @@ -102,8 +110,8 @@ describe('session store', () => { trackingType: FakeTrackingType.TRACKED, }) ) { - const sessionStoreOptions: SessionStoreOptions = { cookie: cookieOptions, allowFallbackToLocalStorage: false } - const sessionStoreStrategyType = getSessionStoreStrategyType(sessionStoreOptions) + const sessionStoreOptions: SessionStoreOptions = { cookie: cookieOptions } + const sessionStoreStrategyType = selectSessionStoreStrategyType(sessionStoreOptions, false) if (sessionStoreStrategyType !== 'COOKIE') { fail('Unable to initialize cookie storage') return diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index b27f87c5f8..e08d8366a7 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -26,14 +26,15 @@ const POLL_DELAY = ONE_SECOND * Checks if cookies are available as the preferred storage * Else, checks if LocalStorage is allowed and available */ -export function getSessionStoreStrategyType( - sessionStoreOptions: SessionStoreOptions +export function selectSessionStoreStrategyType( + sessionStoreOptions: SessionStoreOptions, + allowFallbackToLocalStorage: boolean ): SessionStoreStrategyType | undefined { let sessionStoreStrategyType: SessionStoreStrategyType | undefined if (checkCookieAvailability(sessionStoreOptions.cookie)) { sessionStoreStrategyType = 'COOKIE' - } else if (sessionStoreOptions.allowFallbackToLocalStorage && checkLocalStorageAvailability()) { + } else if (allowFallbackToLocalStorage && checkLocalStorageAvailability()) { sessionStoreStrategyType = 'LOCAL_STORAGE' } diff --git a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts index 3fe2d8b5f3..f14403cfd7 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts @@ -4,7 +4,6 @@ import type { SessionState } from '../sessionState' export type SessionStoreStrategyType = 'COOKIE' | 'LOCAL_STORAGE' export interface SessionStoreOptions { - allowFallbackToLocalStorage: boolean cookie: CookieOptions } diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 6d4d01a378..2cfd582b97 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -23,7 +23,7 @@ describe('logs session manager', () => { const configuration: Partial = { sessionSampleRate: 0.5, sessionStoreStrategyType: 'COOKIE', - sessionStoreOptions: { cookie: {}, allowFallbackToLocalStorage: false }, + sessionStoreOptions: { cookie: {} }, } let clock: Clock let tracked: boolean From c25cc2c55746e964464f04f8d644c13c967f7024 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Wed, 7 Jun 2023 20:30:12 +0200 Subject: [PATCH 26/40] =?UTF-8?q?=F0=9F=91=8C=20Match=20cookie=20test=20ke?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/storeStrategies/sessionInLocalStorage.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index d008fc2ae8..43fda3fa34 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -4,13 +4,14 @@ import { toSessionString, toSessionState } from '../sessionState' import type { SessionStoreStrategy } from './sessionStoreStrategy' export const LOCAL_STORAGE_KEY = '_dd_s' +const LOCAL_STORAGE_TEST_KEY = '_dd_test_' export function checkLocalStorageAvailability() { try { const id = generateUUID() - localStorage.setItem(`_dd_s_${id}`, id) - const retrievedId = localStorage.getItem(`_dd_s_${id}`) - localStorage.removeItem(`_dd_s_${id}`) + localStorage.setItem(`${LOCAL_STORAGE_TEST_KEY}${id}`, id) + const retrievedId = localStorage.getItem(`${LOCAL_STORAGE_TEST_KEY}${id}`) + localStorage.removeItem(`${LOCAL_STORAGE_TEST_KEY}${id}`) return id === retrievedId } catch (e) { return false From 8ca99ae5c2f73ca1fd81146b8dbb762eeef6f306 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Wed, 7 Jun 2023 22:06:56 +0200 Subject: [PATCH 27/40] =?UTF-8?q?=F0=9F=91=8C=20Merge=20SessionStoreOption?= =?UTF-8?q?s=20in=20SessionStoreStrategyType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/configuration.spec.ts | 26 +++++-- .../src/domain/configuration/configuration.ts | 15 +--- .../src/domain/session/sessionManager.spec.ts | 49 +++---------- .../core/src/domain/session/sessionManager.ts | 5 +- .../src/domain/session/sessionStore.spec.ts | 69 ++++++++----------- .../core/src/domain/session/sessionStore.ts | 25 +++---- .../storeStrategies/sessionInCookie.spec.ts | 12 ++-- .../storeStrategies/sessionInCookie.ts | 7 +- .../sessionInLocalStorage.spec.ts | 10 +-- .../storeStrategies/sessionInLocalStorage.ts | 8 +-- .../storeStrategies/sessionStoreStrategy.ts | 6 +- .../src/domain/logsSessionManager.spec.ts | 3 +- .../logs/src/domain/logsSessionManager.ts | 1 - .../rum-core/src/domain/rumSessionManager.ts | 1 - 14 files changed, 91 insertions(+), 146 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index 5ea483684b..8c7dd8802b 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -120,25 +120,37 @@ describe('validateAndBuildConfiguration', () => { describe('cookie options', () => { it('should not be secure nor crossSite by default', () => { const configuration = validateAndBuildConfiguration({ clientToken })! - expect(configuration.sessionStoreOptions.cookie).toEqual({ secure: false, crossSite: false }) + expect(configuration.sessionStoreStrategyType).toEqual({ + type: 'Cookie', + cookieOptions: { secure: false, crossSite: false }, + }) }) it('should be secure when `useSecureSessionCookie` is truthy', () => { const configuration = validateAndBuildConfiguration({ clientToken, useSecureSessionCookie: true })! - expect(configuration.sessionStoreOptions.cookie).toEqual({ secure: true, crossSite: false }) + expect(configuration.sessionStoreStrategyType).toEqual({ + type: 'Cookie', + cookieOptions: { secure: true, crossSite: false }, + }) }) it('should be secure and crossSite when `useCrossSiteSessionCookie` is truthy', () => { const configuration = validateAndBuildConfiguration({ clientToken, useCrossSiteSessionCookie: true })! - expect(configuration.sessionStoreOptions.cookie).toEqual({ secure: true, crossSite: true }) + expect(configuration.sessionStoreStrategyType).toEqual({ + type: 'Cookie', + cookieOptions: { secure: true, crossSite: true }, + }) }) it('should have domain when `trackSessionAcrossSubdomains` is truthy', () => { const configuration = validateAndBuildConfiguration({ clientToken, trackSessionAcrossSubdomains: true })! - expect(configuration.sessionStoreOptions.cookie).toEqual({ - secure: false, - crossSite: false, - domain: jasmine.any(String), + expect(configuration.sessionStoreStrategyType).toEqual({ + type: 'Cookie', + cookieOptions: { + secure: false, + crossSite: false, + domain: jasmine.any(String), + }, }) }) }) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index df6023c49b..876e679cbc 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -9,8 +9,7 @@ import { ONE_KIBI_BYTE } from '../../tools/utils/byteUtils' import { objectHasValue } from '../../tools/utils/objectUtils' import { assign } from '../../tools/utils/polyfills' import { selectSessionStoreStrategyType } from '../session/sessionStore' -import type { SessionStoreOptions, SessionStoreStrategyType } from '../session/storeStrategies/sessionStoreStrategy' -import { buildCookieOptions } from '../session/storeStrategies/sessionInCookie' +import type { SessionStoreStrategyType } from '../session/storeStrategies/sessionStoreStrategy' import type { TransportConfiguration } from './transportConfiguration' import { computeTransportConfiguration } from './transportConfiguration' @@ -77,7 +76,6 @@ interface ReplicaUserConfiguration { export interface Configuration extends TransportConfiguration { // Built from init configuration beforeSend: GenericBeforeSendCallback | undefined - sessionStoreOptions: SessionStoreOptions sessionStoreStrategyType: SessionStoreStrategyType | undefined sessionSampleRate: number telemetrySampleRate: number @@ -130,20 +128,11 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati ) } - // Build Session Store options - const sessionStoreOptions: SessionStoreOptions = { - cookie: buildCookieOptions(initConfiguration), - } - return assign( { beforeSend: initConfiguration.beforeSend && catchUserErrors(initConfiguration.beforeSend, 'beforeSend threw an error:'), - sessionStoreOptions, - sessionStoreStrategyType: selectSessionStoreStrategyType( - sessionStoreOptions, - !!initConfiguration.allowFallbackToLocalStorage - ), + sessionStoreStrategyType: selectSessionStoreStrategyType(initConfiguration), sessionSampleRate: sessionSampleRate ?? 100, telemetrySampleRate: initConfiguration.telemetrySampleRate ?? 20, telemetryConfigurationSampleRate: initConfiguration.telemetryConfigurationSampleRate ?? 5, diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 2f700d94ff..e1c99963d2 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -9,7 +9,7 @@ import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import type { SessionStoreOptions, SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' +import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' const enum FakeTrackingType { NOT_TRACKED = 'not-tracked', @@ -30,8 +30,7 @@ describe('startSessionManager', () => { const DURATION = 123456 const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' - const sessionStoreStrategyType: SessionStoreStrategyType = 'COOKIE' - const sessionStoreOptions: SessionStoreOptions = { cookie: {} } + const sessionStoreStrategyType: SessionStoreStrategyType = { type: 'Cookie', cookieOptions: {} } let clock: Clock function expireSessionCookie() { @@ -87,7 +86,6 @@ describe('startSessionManager', () => { it('when tracked, should store tracking type and session id', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -99,7 +97,6 @@ describe('startSessionManager', () => { it('when not tracked should store tracking type', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -113,7 +110,6 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -127,7 +123,6 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -145,25 +140,25 @@ describe('startSessionManager', () => { }) it('should be called with an empty value if the cookie is not defined', () => { - startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(undefined) }) it('should be called with an invalid value if the cookie has an invalid value', () => { setCookie(SESSION_COOKIE_NAME, 'first=invalid', DURATION) - startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith('invalid') }) it('should be called with TRACKED', () => { setCookie(SESSION_COOKIE_NAME, 'first=tracked', DURATION) - startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) it('should be called with NOT_TRACKED', () => { setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) - startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, spy) + startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) }) @@ -172,7 +167,6 @@ describe('startSessionManager', () => { it('should renew on activity after expiration', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -195,7 +189,6 @@ describe('startSessionManager', () => { it('should not renew on visibility after expiration', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -215,7 +208,6 @@ describe('startSessionManager', () => { it('should re-use the same session id', () => { const firstSessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -223,7 +215,6 @@ describe('startSessionManager', () => { const secondSessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -233,7 +224,7 @@ describe('startSessionManager', () => { }) it('should not erase other session type', () => { - startSessionManager(sessionStoreStrategyType, sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // schedule an expandOrRenewSession document.dispatchEvent(new CustomEvent('click')) @@ -243,12 +234,7 @@ describe('startSessionManager', () => { // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) - startSessionManager( - sessionStoreStrategyType, - sessionStoreOptions, - SECOND_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + startSessionManager(sessionStoreStrategyType, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // cookie correctly set expect(getCookie(SESSION_COOKIE_NAME)).toContain('first') @@ -264,13 +250,11 @@ describe('startSessionManager', () => { it('should have independent tracking types', () => { const firstSessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) const secondSessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, SECOND_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -282,7 +266,6 @@ describe('startSessionManager', () => { it('should notify each expire and renew observables', () => { const firstSessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -293,7 +276,6 @@ describe('startSessionManager', () => { const secondSessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -320,7 +302,6 @@ describe('startSessionManager', () => { it('should expire the session when the time out delay is reached', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -341,7 +322,6 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -358,7 +338,6 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -380,7 +359,6 @@ describe('startSessionManager', () => { it('should expire the session after expiration delay', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -397,7 +375,6 @@ describe('startSessionManager', () => { it('should expand duration on activity', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -421,7 +398,6 @@ describe('startSessionManager', () => { it('should expand not tracked session duration on activity', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -447,7 +423,6 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -473,7 +448,6 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE ) @@ -499,7 +473,6 @@ describe('startSessionManager', () => { it('expires the session when calling expire()', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -515,7 +488,6 @@ describe('startSessionManager', () => { it('notifies expired session only once when calling expire() multiple times', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -532,7 +504,6 @@ describe('startSessionManager', () => { it('notifies expired session only once when calling expire() after the session has been expired', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -549,7 +520,6 @@ describe('startSessionManager', () => { it('renew the session on user activity', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -567,7 +537,6 @@ describe('startSessionManager', () => { it('should return undefined when there is no current session and no startTime', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -579,7 +548,6 @@ describe('startSessionManager', () => { it('should return the current session context when there is no start time', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) @@ -591,7 +559,6 @@ describe('startSessionManager', () => { it('should return the session context corresponding to startTime', () => { const sessionManager = startSessionManager( sessionStoreStrategyType, - sessionStoreOptions, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE ) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 7bd6be3891..c23c8f4ffc 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -7,7 +7,7 @@ import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/ad import { clearInterval, setInterval } from '../../tools/timer' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' import { startSessionStore } from './sessionStore' -import type { SessionStoreOptions, SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' +import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' export interface SessionManager { findActiveSession: (startTime?: RelativeTime) => SessionContext | undefined @@ -27,11 +27,10 @@ let stopCallbacks: Array<() => void> = [] export function startSessionManager( sessionStoreStrategyType: SessionStoreStrategyType, - sessionStoreOptions: SessionStoreOptions, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionManager { - const sessionStore = startSessionStore(sessionStoreStrategyType, sessionStoreOptions, productKey, computeSessionState) + const sessionStore = startSessionStore(sessionStoreStrategyType, productKey, computeSessionState) stopCallbacks.push(() => sessionStore.stop()) const sessionContextHistory = new ValueHistory>(SESSION_CONTEXT_TIMEOUT_DELAY) diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 5bbc486435..fc36dbe995 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,12 +1,10 @@ import type { Clock } from '../../../test' import { mockClock } from '../../../test' -import type { CookieOptions } from '../../browser/cookie' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import type { SessionStore } from './sessionStore' import { startSessionStore, selectSessionStoreStrategyType } from './sessionStore' import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' -import type { SessionStoreOptions } from './storeStrategies/sessionStoreStrategy' const enum FakeTrackingType { TRACKED = 'tracked', @@ -47,50 +45,40 @@ function resetSessionInStore() { } describe('session store', () => { - const cookieOptions: CookieOptions = {} - describe('getSessionStoreStrategyType', () => { - it('should return "COOKIE" when cookies are available', () => { - const sessionStoreStrategyType = selectSessionStoreStrategyType( - { - cookie: cookieOptions, - }, - true - ) - expect(sessionStoreStrategyType).toBe('COOKIE') + it('should return a type cookie when cookies are available', () => { + const sessionStoreStrategyType = selectSessionStoreStrategyType({ + clientToken: 'abc', + allowFallbackToLocalStorage: true, + }) + expect(sessionStoreStrategyType).toEqual(jasmine.objectContaining({ type: 'Cookie' })) }) - it('should report "NO_STORAGE_AVAILABLE" when cookies are not available, and fallback is not allowed', () => { + it('should report undefined when cookies are not available, and fallback is not allowed', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStoreStrategyType = selectSessionStoreStrategyType( - { - cookie: cookieOptions, - }, - false - ) + const sessionStoreStrategyType = selectSessionStoreStrategyType({ + clientToken: 'abc', + allowFallbackToLocalStorage: false, + }) expect(sessionStoreStrategyType).toBeUndefined() }) it('should fallback to localStorage when cookies are not available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') - const sessionStoreStrategyType = selectSessionStoreStrategyType( - { - cookie: cookieOptions, - }, - true - ) - expect(sessionStoreStrategyType).toBe('LOCAL_STORAGE') + const sessionStoreStrategyType = selectSessionStoreStrategyType({ + clientToken: 'abc', + allowFallbackToLocalStorage: true, + }) + expect(sessionStoreStrategyType).toEqual({ type: 'LocalStorage' }) }) - it('should report "NO_STORAGE_AVAILABLE" when no storage is available', () => { + it('should report undefined when no storage is available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') - const sessionStoreStrategyType = selectSessionStoreStrategyType( - { - cookie: cookieOptions, - }, - true - ) + const sessionStoreStrategyType = selectSessionStoreStrategyType({ + clientToken: 'abc', + allowFallbackToLocalStorage: true, + }) expect(sessionStoreStrategyType).toBeUndefined() }) }) @@ -110,18 +98,15 @@ describe('session store', () => { trackingType: FakeTrackingType.TRACKED, }) ) { - const sessionStoreOptions: SessionStoreOptions = { cookie: cookieOptions } - const sessionStoreStrategyType = selectSessionStoreStrategyType(sessionStoreOptions, false) - if (sessionStoreStrategyType !== 'COOKIE') { + const sessionStoreStrategyType = selectSessionStoreStrategyType({ + clientToken: 'abc', + allowFallbackToLocalStorage: false, + }) + if (sessionStoreStrategyType?.type !== 'Cookie') { fail('Unable to initialize cookie storage') return } - sessionStoreManager = startSessionStore( - sessionStoreStrategyType, - sessionStoreOptions, - PRODUCT_KEY, - computeSessionState - ) + sessionStoreManager = startSessionStore(sessionStoreStrategyType, PRODUCT_KEY, computeSessionState) sessionStoreManager.expireObservable.subscribe(expireSpy) sessionStoreManager.renewObservable.subscribe(renewSpy) } diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index e08d8366a7..f3daa6fdec 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -3,11 +3,12 @@ import { Observable } from '../../tools/observable' import { ONE_SECOND, dateNow } from '../../tools/utils/timeUtils' import { throttle } from '../../tools/utils/functionUtils' import { generateUUID } from '../../tools/utils/stringUtils' +import type { InitConfiguration } from '../configuration' import { SESSION_TIME_OUT_DELAY } from './sessionConstants' -import { checkCookieAvailability, initCookieStrategy } from './storeStrategies/sessionInCookie' -import type { SessionStoreOptions, SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' +import { selectCookieStrategy, initCookieStrategy } from './storeStrategies/sessionInCookie' +import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' -import { checkLocalStorageAvailability, initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' +import { initLocalStorageStrategy, selectLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' import { processSessionStoreOperations } from './sessionStoreOperations' export interface SessionStore { @@ -27,17 +28,12 @@ const POLL_DELAY = ONE_SECOND * Else, checks if LocalStorage is allowed and available */ export function selectSessionStoreStrategyType( - sessionStoreOptions: SessionStoreOptions, - allowFallbackToLocalStorage: boolean + initConfiguration: InitConfiguration ): SessionStoreStrategyType | undefined { - let sessionStoreStrategyType: SessionStoreStrategyType | undefined - - if (checkCookieAvailability(sessionStoreOptions.cookie)) { - sessionStoreStrategyType = 'COOKIE' - } else if (allowFallbackToLocalStorage && checkLocalStorageAvailability()) { - sessionStoreStrategyType = 'LOCAL_STORAGE' + let sessionStoreStrategyType = selectCookieStrategy(initConfiguration) + if (!sessionStoreStrategyType && initConfiguration.allowFallbackToLocalStorage) { + sessionStoreStrategyType = selectLocalStorageStrategy() } - return sessionStoreStrategyType } @@ -49,7 +45,6 @@ export function selectSessionStoreStrategyType( */ export function startSessionStore( sessionStoreStrategyType: SessionStoreStrategyType, - sessionStoreOptions: SessionStoreOptions, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionStore { @@ -57,7 +52,9 @@ export function startSessionStore( const expireObservable = new Observable() const sessionStoreStrategy = - sessionStoreStrategyType === 'COOKIE' ? initCookieStrategy(sessionStoreOptions.cookie) : initLocalStorageStrategy() + sessionStoreStrategyType.type === 'Cookie' + ? initCookieStrategy(sessionStoreStrategyType.cookieOptions) + : initLocalStorageStrategy() const { clearSession, retrieveSession } = sessionStoreStrategy const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index b384b676f9..6af16ec201 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,6 +1,6 @@ -import { setCookie, deleteCookie, getCookie } from '../../../browser/cookie' +import { setCookie, deleteCookie, getCookie, getCurrentSite } from '../../../browser/cookie' import type { SessionState } from '../sessionState' -import { SESSION_COOKIE_NAME, buildCookieOptions, checkCookieAvailability, initCookieStrategy } from './sessionInCookie' +import { SESSION_COOKIE_NAME, buildCookieOptions, selectCookieStrategy, initCookieStrategy } from './sessionInCookie' import type { SessionStoreStrategy } from './sessionStoreStrategy' @@ -84,13 +84,15 @@ describe('session in cookie strategy', () => { { initConfiguration: { clientToken: 'abc', trackSessionAcrossSubdomains: true }, cookieOptions: { domain: 'foo.bar' }, - cookieString: /^dd_cookie_test_[\w-]+=[^;]*;expires=[^;]+;path=\/;samesite=strict;domain=foo.bar$/, + cookieString: new RegExp( + `^dd_cookie_test_[\\w-]+=[^;]*;expires=[^;]+;path=\\/;samesite=strict;domain=${getCurrentSite()}$` + ), description: 'should set cookie domain when tracking accross subdomains', }, - ].forEach(({ description, cookieOptions, cookieString }) => { + ].forEach(({ description, initConfiguration, cookieString }) => { it(description, () => { const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') - checkCookieAvailability(cookieOptions) + selectCookieStrategy(initConfiguration) expect(cookieSetSpy.calls.argsFor(0)[0]).toMatch(cookieString) }) }) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts index d5d1a8dae5..713c3f3bb3 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts @@ -5,12 +5,13 @@ import { tryOldCookiesMigration } from '../oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from '../sessionConstants' import type { SessionState } from '../sessionState' import { toSessionString, toSessionState } from '../sessionState' -import type { SessionStoreStrategy } from './sessionStoreStrategy' +import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' export const SESSION_COOKIE_NAME = '_dd_s' -export function checkCookieAvailability(cookieOptions: CookieOptions) { - return areCookiesAuthorized(cookieOptions) +export function selectCookieStrategy(initConfiguration: InitConfiguration): SessionStoreStrategyType | undefined { + const cookieOptions = buildCookieOptions(initConfiguration) + return areCookiesAuthorized(cookieOptions) ? { type: 'Cookie', cookieOptions } : undefined } export function initCookieStrategy(cookieOptions: CookieOptions): SessionStoreStrategy { diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts index 0af493b5d2..acc2272f02 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts @@ -1,5 +1,5 @@ import type { SessionState } from '../sessionState' -import { LOCAL_STORAGE_KEY, checkLocalStorageAvailability, initLocalStorageStrategy } from './sessionInLocalStorage' +import { LOCAL_STORAGE_KEY, selectLocalStorageStrategy, initLocalStorageStrategy } from './sessionInLocalStorage' describe('session in local storage strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } @@ -9,14 +9,14 @@ describe('session in local storage strategy', () => { }) it('should report local storage as available', () => { - const available = checkLocalStorageAvailability() - expect(available).toBe(true) + const available = selectLocalStorageStrategy() + expect(available).toEqual({ type: 'LocalStorage' }) }) it('should report local storage as not available', () => { spyOn(Storage.prototype, 'getItem').and.throwError('Unavailable') - const available = checkLocalStorageAvailability() - expect(available).toBe(false) + const available = selectLocalStorageStrategy() + expect(available).toBeUndefined() }) it('should persist a session in local storage', () => { diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index 43fda3fa34..8f07ea73bc 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -1,20 +1,20 @@ import { generateUUID } from '../../../tools/utils/stringUtils' import type { SessionState } from '../sessionState' import { toSessionString, toSessionState } from '../sessionState' -import type { SessionStoreStrategy } from './sessionStoreStrategy' +import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' export const LOCAL_STORAGE_KEY = '_dd_s' const LOCAL_STORAGE_TEST_KEY = '_dd_test_' -export function checkLocalStorageAvailability() { +export function selectLocalStorageStrategy(): SessionStoreStrategyType | undefined { try { const id = generateUUID() localStorage.setItem(`${LOCAL_STORAGE_TEST_KEY}${id}`, id) const retrievedId = localStorage.getItem(`${LOCAL_STORAGE_TEST_KEY}${id}`) localStorage.removeItem(`${LOCAL_STORAGE_TEST_KEY}${id}`) - return id === retrievedId + return id === retrievedId ? { type: 'LocalStorage' } : undefined } catch (e) { - return false + return undefined } } diff --git a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts index f14403cfd7..286c63ff49 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts @@ -1,11 +1,7 @@ import type { CookieOptions } from '../../../browser/cookie' import type { SessionState } from '../sessionState' -export type SessionStoreStrategyType = 'COOKIE' | 'LOCAL_STORAGE' - -export interface SessionStoreOptions { - cookie: CookieOptions -} +export type SessionStoreStrategyType = { type: 'Cookie'; cookieOptions: CookieOptions } | { type: 'LocalStorage' } export interface SessionStoreStrategy { persistSession: (session: SessionState) => void diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 2cfd582b97..d7e9e55640 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -22,8 +22,7 @@ describe('logs session manager', () => { const DURATION = 123456 const configuration: Partial = { sessionSampleRate: 0.5, - sessionStoreStrategyType: 'COOKIE', - sessionStoreOptions: { cookie: {} }, + sessionStoreStrategyType: { type: 'Cookie', cookieOptions: {} }, } let clock: Clock let tracked: boolean diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index e63f74b8db..5c86c336a8 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -25,7 +25,6 @@ export function startLogsSessionManager(configuration: LogsConfiguration): LogsS const sessionManager = startSessionManager( configuration.sessionStoreStrategyType, - configuration.sessionStoreOptions, LOGS_SESSION_KEY, (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index cd8d525f38..f768f79333 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -41,7 +41,6 @@ export function startRumSessionManager(configuration: RumConfiguration, lifeCycl const sessionManager = startSessionManager( configuration.sessionStoreStrategyType, - configuration.sessionStoreOptions, RUM_SESSION_KEY, (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) From b73cec6aa9297e58dace3582ebb3a043fbb95e8d Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 8 Jun 2023 14:18:19 +0200 Subject: [PATCH 28/40] =?UTF-8?q?=F0=9F=91=8C=20Non-null=20assertion=20for?= =?UTF-8?q?=20sessionManager=20init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logs/src/domain/logsSessionManager.ts | 7 ++----- packages/rum-core/src/domain/rumSessionManager.ts | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index 5c86c336a8..26fe80a6b0 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -19,12 +19,9 @@ export const enum LoggerTrackingType { } export function startLogsSessionManager(configuration: LogsConfiguration): LogsSessionManager { - if (!configuration.sessionStoreStrategyType) { - throw new Error('Cannot initialize Logs Session Manager without a storage.') - } - const sessionManager = startSessionManager( - configuration.sessionStoreStrategyType, + // TODO - Improve configuration type and remove assertion + configuration.sessionStoreStrategyType!, LOGS_SESSION_KEY, (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) diff --git a/packages/rum-core/src/domain/rumSessionManager.ts b/packages/rum-core/src/domain/rumSessionManager.ts index f768f79333..b232475695 100644 --- a/packages/rum-core/src/domain/rumSessionManager.ts +++ b/packages/rum-core/src/domain/rumSessionManager.ts @@ -35,12 +35,9 @@ export const enum RumTrackingType { } export function startRumSessionManager(configuration: RumConfiguration, lifeCycle: LifeCycle): RumSessionManager { - if (!configuration.sessionStoreStrategyType) { - throw new Error('Cannot initialize RUM Session Manager without a storage.') - } - const sessionManager = startSessionManager( - configuration.sessionStoreStrategyType, + // TODO - Improve configuration type and remove assertion + configuration.sessionStoreStrategyType!, RUM_SESSION_KEY, (rawTrackingType) => computeSessionState(configuration, rawTrackingType) ) From 8ba024e92687ffec91684e549902e8ad98642504 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Thu, 8 Jun 2023 14:35:03 +0200 Subject: [PATCH 29/40] =?UTF-8?q?=F0=9F=91=8C=20Added=20tests=20for=20conf?= =?UTF-8?q?iguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/configuration.spec.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index 8c7dd8802b..9a76b4fb6f 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -155,6 +155,35 @@ describe('validateAndBuildConfiguration', () => { }) }) + describe('allowFallbackToLocalStorage', () => { + it('should not be enabled (false) by default', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const configuration = validateAndBuildConfiguration({ clientToken }) + expect(configuration?.sessionStoreStrategyType).toBeUndefined() + }) + + it('should contain cookie in the configuration by default', () => { + const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true }) + expect(configuration?.sessionStoreStrategyType).toEqual({ + type: 'Cookie', + cookieOptions: { secure: false, crossSite: false }, + }) + }) + + it('should contain local storage in the configuration when enabled and cookies are not available', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true }) + expect(configuration?.sessionStoreStrategyType).toEqual({ type: 'LocalStorage' }) + }) + + it('should not contain any available storage if both cookies and local storage are unavailable', () => { + spyOnProperty(document, 'cookie', 'get').and.returnValue('') + spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') + const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true }) + expect(configuration?.sessionStoreStrategyType).toBeUndefined() + }) + }) + describe('beforeSend', () => { it('should be undefined when beforeSend is missing on user configuration', () => { const configuration = validateAndBuildConfiguration({ clientToken })! From 2ab51bd4213487464b87082dfae9ef5208c013a7 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 9 Jun 2023 09:14:19 +0200 Subject: [PATCH 30/40] =?UTF-8?q?=F0=9F=91=8C=20Add=20e2e=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/scenario/sessionStore.scenario.ts | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 test/e2e/scenario/sessionStore.scenario.ts diff --git a/test/e2e/scenario/sessionStore.scenario.ts b/test/e2e/scenario/sessionStore.scenario.ts new file mode 100644 index 0000000000..ce863b8b17 --- /dev/null +++ b/test/e2e/scenario/sessionStore.scenario.ts @@ -0,0 +1,64 @@ +import { createTest } from '../lib/framework' + +const SESSION_COOKIE_NAME = '_dd_s' +const SESSION_LOCAL_STORAGE_KEY = '_dd_s' + +describe('Session Stores', () => { + describe('Cookies', () => { + createTest('Cookie Initialization') + .withLogs() + .withRum() + .run(async () => { + const [cookie] = await browser.getCookies([SESSION_COOKIE_NAME]) + const cookieSessionId = cookie.value.match(/id=([\w-]+)/)![1] + + const logsContext = await browser.execute(() => window.DD_LOGS.getInternalContext()) + const rumContext = await browser.execute(() => window.DD_RUM.getInternalContext()) + + expect(logsContext.session_id).toBe(cookieSessionId) + expect(rumContext.session_id).toBe(cookieSessionId) + }) + }) + + describe('Local Storage', () => { + createTest('Local Storage Initialization') + .withLogs({ allowFallbackToLocalStorage: true }) + .withRum({ allowFallbackToLocalStorage: true }) + // This will force the SDKs to initialize using local storage + .withHead('') + .run(async () => { + const sessionStateString = await browser.execute( + (key) => window.localStorage.getItem(key), + SESSION_LOCAL_STORAGE_KEY + ) + const sessionId = sessionStateString?.match(/id=([\w-]+)/)![1] + + const logsContext = await browser.execute(() => window.DD_LOGS.getInternalContext()) + const rumContext = await browser.execute(() => window.DD_RUM.getInternalContext()) + + expect(logsContext.session_id).toBe(sessionId) + expect(rumContext.session_id).toBe(sessionId) + }) + }) + + describe('No storage available', () => { + createTest('RUM should fail init / Logs should succeed') + .withLogs({ allowFallbackToLocalStorage: true }) + .withRum({ allowFallbackToLocalStorage: true }) + // This will ensure no storage is available + .withHead( + ` + ` + ) + .run(async () => { + const logsContext = await browser.execute(() => window.DD_LOGS.getInternalContext()) + const rumContext = await browser.execute(() => window.DD_RUM.getInternalContext()) + + expect(logsContext).not.toBeNull() + expect(rumContext).toBeNull() + }) + }) +}) From df3d491d5093bb89042667affe863671328fe28a Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 9 Jun 2023 09:19:16 +0200 Subject: [PATCH 31/40] =?UTF-8?q?=F0=9F=91=8C=20Report=20allowFallbackToLo?= =?UTF-8?q?calStorage=20in=20configuration=20telemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/configuration/configuration.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 876e679cbc..8202399086 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -164,19 +164,20 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati ) } -export function serializeConfiguration(configuration: InitConfiguration): Partial { - const proxy = configuration.proxy ?? configuration.proxyUrl +export function serializeConfiguration(initConfiguration: InitConfiguration): Partial { + const proxy = initConfiguration.proxy ?? initConfiguration.proxyUrl return { - session_sample_rate: configuration.sessionSampleRate ?? configuration.sampleRate, - telemetry_sample_rate: configuration.telemetrySampleRate, - telemetry_configuration_sample_rate: configuration.telemetryConfigurationSampleRate, - use_before_send: !!configuration.beforeSend, - use_cross_site_session_cookie: configuration.useCrossSiteSessionCookie, - use_secure_session_cookie: configuration.useSecureSessionCookie, + session_sample_rate: initConfiguration.sessionSampleRate ?? initConfiguration.sampleRate, + telemetry_sample_rate: initConfiguration.telemetrySampleRate, + telemetry_configuration_sample_rate: initConfiguration.telemetryConfigurationSampleRate, + use_before_send: !!initConfiguration.beforeSend, + use_cross_site_session_cookie: initConfiguration.useCrossSiteSessionCookie, + use_secure_session_cookie: initConfiguration.useSecureSessionCookie, use_proxy: proxy !== undefined ? !!proxy : undefined, - silent_multiple_init: configuration.silentMultipleInit, - track_session_across_subdomains: configuration.trackSessionAcrossSubdomains, - track_resources: configuration.trackResources, - track_long_task: configuration.trackLongTasks, + silent_multiple_init: initConfiguration.silentMultipleInit, + track_session_across_subdomains: initConfiguration.trackSessionAcrossSubdomains, + track_resources: initConfiguration.trackResources, + track_long_task: initConfiguration.trackLongTasks, + allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, } } From 306e128209e301eae7cb58af93fb5b1d214ef700 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 9 Jun 2023 09:28:54 +0200 Subject: [PATCH 32/40] =?UTF-8?q?=F0=9F=91=8C=20Fix=20E2E=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/scenario/sessionStore.scenario.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/e2e/scenario/sessionStore.scenario.ts b/test/e2e/scenario/sessionStore.scenario.ts index ce863b8b17..c53d96587a 100644 --- a/test/e2e/scenario/sessionStore.scenario.ts +++ b/test/e2e/scenario/sessionStore.scenario.ts @@ -12,11 +12,11 @@ describe('Session Stores', () => { const [cookie] = await browser.getCookies([SESSION_COOKIE_NAME]) const cookieSessionId = cookie.value.match(/id=([\w-]+)/)![1] - const logsContext = await browser.execute(() => window.DD_LOGS.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM.getInternalContext()) + const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) - expect(logsContext.session_id).toBe(cookieSessionId) - expect(rumContext.session_id).toBe(cookieSessionId) + expect(logsContext?.session_id).toBe(cookieSessionId) + expect(rumContext?.session_id).toBe(cookieSessionId) }) }) @@ -33,11 +33,11 @@ describe('Session Stores', () => { ) const sessionId = sessionStateString?.match(/id=([\w-]+)/)![1] - const logsContext = await browser.execute(() => window.DD_LOGS.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM.getInternalContext()) + const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) - expect(logsContext.session_id).toBe(sessionId) - expect(rumContext.session_id).toBe(sessionId) + expect(logsContext?.session_id).toBe(sessionId) + expect(rumContext?.session_id).toBe(sessionId) }) }) @@ -54,8 +54,8 @@ describe('Session Stores', () => { ` ) .run(async () => { - const logsContext = await browser.execute(() => window.DD_LOGS.getInternalContext()) - const rumContext = await browser.execute(() => window.DD_RUM.getInternalContext()) + const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) + const rumContext = await browser.execute(() => window.DD_RUM?.getInternalContext()) expect(logsContext).not.toBeNull() expect(rumContext).toBeNull() From c04135474043a1cabacacedece72dbd26327c3ee Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 9 Jun 2023 14:42:38 +0200 Subject: [PATCH 33/40] =?UTF-8?q?=F0=9F=91=8C=20Remove=20redundant=20unit?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/configuration.spec.ts | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index 9a76b4fb6f..344951a7fc 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -117,46 +117,8 @@ describe('validateAndBuildConfiguration', () => { }) }) - describe('cookie options', () => { - it('should not be secure nor crossSite by default', () => { - const configuration = validateAndBuildConfiguration({ clientToken })! - expect(configuration.sessionStoreStrategyType).toEqual({ - type: 'Cookie', - cookieOptions: { secure: false, crossSite: false }, - }) - }) - - it('should be secure when `useSecureSessionCookie` is truthy', () => { - const configuration = validateAndBuildConfiguration({ clientToken, useSecureSessionCookie: true })! - expect(configuration.sessionStoreStrategyType).toEqual({ - type: 'Cookie', - cookieOptions: { secure: true, crossSite: false }, - }) - }) - - it('should be secure and crossSite when `useCrossSiteSessionCookie` is truthy', () => { - const configuration = validateAndBuildConfiguration({ clientToken, useCrossSiteSessionCookie: true })! - expect(configuration.sessionStoreStrategyType).toEqual({ - type: 'Cookie', - cookieOptions: { secure: true, crossSite: true }, - }) - }) - - it('should have domain when `trackSessionAcrossSubdomains` is truthy', () => { - const configuration = validateAndBuildConfiguration({ clientToken, trackSessionAcrossSubdomains: true })! - expect(configuration.sessionStoreStrategyType).toEqual({ - type: 'Cookie', - cookieOptions: { - secure: false, - crossSite: false, - domain: jasmine.any(String), - }, - }) - }) - }) - - describe('allowFallbackToLocalStorage', () => { - it('should not be enabled (false) by default', () => { + describe('sessionStoreStrategyType', () => { + it('allowFallbackToLocalStorage should not be enabled by default', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') const configuration = validateAndBuildConfiguration({ clientToken }) expect(configuration?.sessionStoreStrategyType).toBeUndefined() @@ -176,7 +138,7 @@ describe('validateAndBuildConfiguration', () => { expect(configuration?.sessionStoreStrategyType).toEqual({ type: 'LocalStorage' }) }) - it('should not contain any available storage if both cookies and local storage are unavailable', () => { + it('should not contain any storage if both cookies and local storage are unavailable', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') spyOn(Storage.prototype, 'getItem').and.throwError('unavailable') const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true }) From 5dcf9905697c9b4d363ca66c242e3611859b6517 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Fri, 9 Jun 2023 14:44:38 +0200 Subject: [PATCH 34/40] =?UTF-8?q?=F0=9F=91=8C=20Use=20snake=20case=20for?= =?UTF-8?q?=20telemetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/domain/configuration/configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 8202399086..2be145377d 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -178,6 +178,6 @@ export function serializeConfiguration(initConfiguration: InitConfiguration): Pa track_session_across_subdomains: initConfiguration.trackSessionAcrossSubdomains, track_resources: initConfiguration.trackResources, track_long_task: initConfiguration.trackLongTasks, - allowFallbackToLocalStorage: !!initConfiguration.allowFallbackToLocalStorage, + allow_fallback_to_local_storage: !!initConfiguration.allowFallbackToLocalStorage, } } From f49e86ae8d33b4c439e2f2c986db2670e1e46f9a Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 12 Jun 2023 15:59:06 +0200 Subject: [PATCH 35/40] =?UTF-8?q?=F0=9F=91=8C=20Use=20the=20same=20key=20f?= =?UTF-8?q?or=20cookie=20and=20localStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/oldCookiesMigration.spec.ts | 31 ++++++------ .../src/domain/session/oldCookiesMigration.ts | 5 +- .../src/domain/session/sessionManager.spec.ts | 44 ++++++++--------- .../src/domain/session/sessionStore.spec.ts | 16 +++---- .../session/sessionStoreOperations.spec.ts | 9 ++-- .../storeStrategies/sessionInCookie.spec.ts | 12 ++--- .../storeStrategies/sessionInCookie.ts | 11 ++--- .../sessionInLocalStorage.spec.ts | 9 ++-- .../storeStrategies/sessionInLocalStorage.ts | 8 ++-- .../storeStrategies/sessionStoreStrategy.ts | 2 + packages/core/src/index.ts | 2 +- packages/logs/src/boot/startLogs.spec.ts | 8 ++-- .../src/domain/logsSessionManager.spec.ts | 38 +++++++-------- .../src/domain/rumSessionManager.spec.ts | 48 +++++++++---------- test/e2e/lib/helpers/session.ts | 4 +- test/e2e/scenario/sessionStore.scenario.ts | 11 ++--- 16 files changed, 129 insertions(+), 129 deletions(-) diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index 791d49b38d..f34f61f7c7 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -6,17 +6,18 @@ import { tryOldCookiesMigration, } from './oldCookiesMigration' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { SESSION_COOKIE_NAME, initCookieStrategy } from './storeStrategies/sessionInCookie' +import { initCookieStrategy } from './storeStrategies/sessionInCookie' +import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' describe('old cookies migration', () => { const sessionStoreStrategy = initCookieStrategy({}) it('should not touch current cookie', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) + setCookie(SESSION_STORE_KEY, 'id=abcde&rum=0&logs=1&expire=1234567890', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) + tryOldCookiesMigration(sessionStoreStrategy) - expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=abcde&rum=0&logs=1&expire=1234567890') + expect(getCookie(SESSION_STORE_KEY)).toBe('id=abcde&rum=0&logs=1&expire=1234567890') }) it('should create new cookie from old cookie values', () => { @@ -24,26 +25,26 @@ describe('old cookies migration', () => { setCookie(OLD_LOGS_COOKIE_NAME, '1', SESSION_EXPIRATION_DELAY) setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) + tryOldCookiesMigration(sessionStoreStrategy) - expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcde') - expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') - expect(getCookie(SESSION_COOKIE_NAME)).toContain('logs=1') - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/expire=\d+/) + expect(getCookie(SESSION_STORE_KEY)).toContain('id=abcde') + expect(getCookie(SESSION_STORE_KEY)).toContain('rum=0') + expect(getCookie(SESSION_STORE_KEY)).toContain('logs=1') + expect(getCookie(SESSION_STORE_KEY)).toMatch(/expire=\d+/) }) it('should create new cookie from a single old cookie', () => { setCookie(OLD_RUM_COOKIE_NAME, '0', SESSION_EXPIRATION_DELAY) - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) + tryOldCookiesMigration(sessionStoreStrategy) - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') - expect(getCookie(SESSION_COOKIE_NAME)).toContain('rum=0') - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/expire=\d+/) + expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') + expect(getCookie(SESSION_STORE_KEY)).toContain('rum=0') + expect(getCookie(SESSION_STORE_KEY)).toMatch(/expire=\d+/) }) it('should not create a new cookie if no old cookie is present', () => { - tryOldCookiesMigration(SESSION_COOKIE_NAME, sessionStoreStrategy) - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + tryOldCookiesMigration(sessionStoreStrategy) + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) }) diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index 7e9511181d..6b9314a7cf 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,6 +1,7 @@ import { getCookie } from '../../browser/cookie' import { dateNow } from '../../tools/utils/timeUtils' import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' +import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' import { isSessionInExpiredState } from './sessionState' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' @@ -17,8 +18,8 @@ export const LOGS_SESSION_KEY = 'logs' * This migration should remain in the codebase as long as older versions are available/live * to allow older sdk versions to be upgraded to newer versions without compatibility issues. */ -export function tryOldCookiesMigration(cookieName: string, cookieStoreStrategy: SessionStoreStrategy) { - const sessionString = getCookie(cookieName) +export function tryOldCookiesMigration(cookieStoreStrategy: SessionStoreStrategy) { + const sessionString = getCookie(SESSION_STORE_KEY) if (!sessionString) { const oldSessionId = getCookie(OLD_SESSION_COOKIE_NAME) const oldRumType = getCookie(OLD_RUM_COOKIE_NAME) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index e1c99963d2..bcbaddf74e 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -7,9 +7,9 @@ import { DOM_EVENT } from '../../browser/addEventListener' import { ONE_HOUR, ONE_SECOND } from '../../tools/utils/timeUtils' import type { SessionManager } from './sessionManager' import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from './sessionManager' -import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' +import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' const enum FakeTrackingType { NOT_TRACKED = 'not-tracked', @@ -34,23 +34,23 @@ describe('startSessionManager', () => { let clock: Clock function expireSessionCookie() { - setCookie(SESSION_COOKIE_NAME, '', DURATION) + setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(COOKIE_ACCESS_DELAY) } function expectSessionIdToBe(sessionManager: SessionManager, sessionId: string) { expect(sessionManager.findActiveSession()!.id).toBe(sessionId) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`id=${sessionId}`) + expect(getCookie(SESSION_STORE_KEY)).toContain(`id=${sessionId}`) } function expectSessionIdToBeDefined(sessionManager: SessionManager) { expect(sessionManager.findActiveSession()!.id).toMatch(/^[a-f0-9-]+$/) - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]+/) + expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) } function expectSessionIdToNotBeDefined(sessionManager: SessionManager) { expect(sessionManager.findActiveSession()?.id).toBeUndefined() - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') + expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') } function expectTrackingTypeToBe( @@ -59,12 +59,12 @@ describe('startSessionManager', () => { trackingType: FakeTrackingType ) { expect(sessionManager.findActiveSession()!.trackingType).toEqual(trackingType) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${productKey}=${trackingType}`) + expect(getCookie(SESSION_STORE_KEY)).toContain(`${productKey}=${trackingType}`) } function expectTrackingTypeToNotBeDefined(sessionManager: SessionManager, productKey: string) { expect(sessionManager.findActiveSession()?.trackingType).toBeUndefined() - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain(`${productKey}=`) + expect(getCookie(SESSION_STORE_KEY)).not.toContain(`${productKey}=`) } beforeEach(() => { @@ -106,7 +106,7 @@ describe('startSessionManager', () => { }) it('when tracked should keep existing tracking type and session id', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&first=tracked', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&first=tracked', DURATION) const sessionManager = startSessionManager( sessionStoreStrategyType, @@ -119,7 +119,7 @@ describe('startSessionManager', () => { }) it('when not tracked should keep existing tracking type', () => { - setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) + setCookie(SESSION_STORE_KEY, 'first=not-tracked', DURATION) const sessionManager = startSessionManager( sessionStoreStrategyType, @@ -145,19 +145,19 @@ describe('startSessionManager', () => { }) it('should be called with an invalid value if the cookie has an invalid value', () => { - setCookie(SESSION_COOKIE_NAME, 'first=invalid', DURATION) + setCookie(SESSION_STORE_KEY, 'first=invalid', DURATION) startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith('invalid') }) it('should be called with TRACKED', () => { - setCookie(SESSION_COOKIE_NAME, 'first=tracked', DURATION) + setCookie(SESSION_STORE_KEY, 'first=tracked', DURATION) startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) it('should be called with NOT_TRACKED', () => { - setCookie(SESSION_COOKIE_NAME, 'first=not-tracked', DURATION) + setCookie(SESSION_STORE_KEY, 'first=not-tracked', DURATION) startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) @@ -237,14 +237,14 @@ describe('startSessionManager', () => { startSessionManager(sessionStoreStrategyType, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // cookie correctly set - expect(getCookie(SESSION_COOKIE_NAME)).toContain('first') - expect(getCookie(SESSION_COOKIE_NAME)).toContain('second') + expect(getCookie(SESSION_STORE_KEY)).toContain('first') + expect(getCookie(SESSION_STORE_KEY)).toContain('second') clock.tick(COOKIE_ACCESS_DELAY / 2) // scheduled expandOrRenewSession should not use cached value - expect(getCookie(SESSION_COOKIE_NAME)).toContain('first') - expect(getCookie(SESSION_COOKIE_NAME)).toContain('second') + expect(getCookie(SESSION_STORE_KEY)).toContain('first') + expect(getCookie(SESSION_STORE_KEY)).toContain('second') }) it('should have independent tracking types', () => { @@ -309,16 +309,16 @@ describe('startSessionManager', () => { sessionManager.expireObservable.subscribe(expireSessionSpy) expect(sessionManager.findActiveSession()).toBeDefined() - expect(getCookie(SESSION_COOKIE_NAME)).toBeDefined() + expect(getCookie(SESSION_STORE_KEY)).toBeDefined() clock.tick(SESSION_TIME_OUT_DELAY) expect(sessionManager.findActiveSession()).toBeUndefined() - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() expect(expireSessionSpy).toHaveBeenCalled() }) it('should renew an existing timed out session', () => { - setCookie(SESSION_COOKIE_NAME, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) + setCookie(SESSION_STORE_KEY, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) const sessionManager = startSessionManager( sessionStoreStrategyType, @@ -329,12 +329,12 @@ describe('startSessionManager', () => { sessionManager.expireObservable.subscribe(expireSessionSpy) expect(sessionManager.findActiveSession()!.id).not.toBe('abcde') - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`created=${Date.now()}`) + expect(getCookie(SESSION_STORE_KEY)).toContain(`created=${Date.now()}`) 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', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcde&first=tracked', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcde&first=tracked', DURATION) const sessionManager = startSessionManager( sessionStoreStrategyType, @@ -343,7 +343,7 @@ describe('startSessionManager', () => { ) expect(sessionManager.findActiveSession()!.id).toBe('abcde') - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('created=') + expect(getCookie(SESSION_STORE_KEY)).not.toContain('created=') }) }) diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index fc36dbe995..802370addc 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -3,8 +3,8 @@ import { mockClock } from '../../../test' import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' import type { SessionStore } from './sessionStore' import { startSessionStore, selectSessionStoreStrategyType } from './sessionStore' -import { SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' +import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' const enum FakeTrackingType { TRACKED = 'tracked', @@ -18,7 +18,7 @@ const SECOND_ID = 'second' function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { setCookie( - SESSION_COOKIE_NAME, + SESSION_STORE_KEY, `${id ? `id=${id}&` : ''}${PRODUCT_KEY}=${trackingType}&created=${Date.now()}&expire=${ expire || Date.now() + SESSION_EXPIRATION_DELAY }`, @@ -27,21 +27,21 @@ function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRA } function expectTrackedSessionToBeInStore(id?: string) { - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(new RegExp(`id=${id ? id : '[a-f0-9-]+'}`)) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).toMatch(new RegExp(`id=${id ? id : '[a-f0-9-]+'}`)) + expect(getCookie(SESSION_STORE_KEY)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.TRACKED}`) } function expectNotTrackedSessionToBeInStore() { - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') + expect(getCookie(SESSION_STORE_KEY)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`) } function getStoreExpiration() { - return /expire=(\d+)/.exec(getCookie(SESSION_COOKIE_NAME)!)?.[1] + return /expire=(\d+)/.exec(getCookie(SESSION_STORE_KEY)!)?.[1] } function resetSessionInStore() { - setCookie(SESSION_COOKIE_NAME, '', DURATION) + setCookie(SESSION_STORE_KEY, '', DURATION) } describe('session store', () => { diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 396516e551..7eb31d9455 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -2,8 +2,8 @@ import type { StubStorage } from '../../../test' import { mockClock, stubCookieProvider, stubLocalStorageProvider } from '../../../test' import type { CookieOptions } from '../../browser/cookie' import { SESSION_EXPIRATION_DELAY } from './sessionConstants' -import { initCookieStrategy, SESSION_COOKIE_NAME } from './storeStrategies/sessionInCookie' -import { initLocalStorageStrategy, LOCAL_STORAGE_KEY } from './storeStrategies/sessionInLocalStorage' +import { initCookieStrategy } from './storeStrategies/sessionInCookie' +import { initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' import type { SessionState } from './sessionState' import { toSessionString } from './sessionState' import { @@ -12,6 +12,7 @@ import { LOCK_MAX_TRIES, LOCK_RETRY_DELAY, } from './sessionStoreOperations' +import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' const cookieOptions: CookieOptions = {} @@ -21,13 +22,13 @@ const cookieOptions: CookieOptions = {} title: 'Cookie Storage', sessionStoreStrategy: initCookieStrategy(cookieOptions), stubStorageProvider: stubCookieProvider, - storageKey: SESSION_COOKIE_NAME, + storageKey: SESSION_STORE_KEY, }, { title: 'Local Storage', sessionStoreStrategy: initLocalStorageStrategy(), stubStorageProvider: stubLocalStorageProvider, - storageKey: LOCAL_STORAGE_KEY, + storageKey: SESSION_STORE_KEY, }, ] as const ).forEach(({ title, sessionStoreStrategy, stubStorageProvider, storageKey }) => { diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index 6af16ec201..ce5b91cfda 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,8 +1,8 @@ import { setCookie, deleteCookie, getCookie, getCurrentSite } from '../../../browser/cookie' import type { SessionState } from '../sessionState' -import { SESSION_COOKIE_NAME, buildCookieOptions, selectCookieStrategy, initCookieStrategy } from './sessionInCookie' - +import { buildCookieOptions, selectCookieStrategy, initCookieStrategy } from './sessionInCookie' import type { SessionStoreStrategy } from './sessionStoreStrategy' +import { SESSION_STORE_KEY } from './sessionStoreStrategy' describe('session in cookie strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } @@ -13,14 +13,14 @@ describe('session in cookie strategy', () => { }) afterEach(() => { - deleteCookie(SESSION_COOKIE_NAME) + deleteCookie(SESSION_STORE_KEY) }) it('should persist a session in a cookie', () => { cookieStorageStrategy.persistSession(sessionState) const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState }) - expect(getCookie(SESSION_COOKIE_NAME)).toBe('id=123&created=0') + expect(getCookie(SESSION_STORE_KEY)).toBe('id=123&created=0') }) it('should delete the cookie holding the session', () => { @@ -28,11 +28,11 @@ describe('session in cookie strategy', () => { cookieStorageStrategy.clearSession() const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({}) - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) it('should return an empty object if session string is invalid', () => { - setCookie(SESSION_COOKIE_NAME, '{test:42}', 1000) + setCookie(SESSION_STORE_KEY, '{test:42}', 1000) const session = cookieStorageStrategy.retrieveSession() expect(session).toEqual({}) }) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts index 713c3f3bb3..afb7d6b508 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts @@ -6,8 +6,7 @@ import { SESSION_EXPIRATION_DELAY } from '../sessionConstants' import type { SessionState } from '../sessionState' import { toSessionString, toSessionState } from '../sessionState' import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' - -export const SESSION_COOKIE_NAME = '_dd_s' +import { SESSION_STORE_KEY } from './sessionStoreStrategy' export function selectCookieStrategy(initConfiguration: InitConfiguration): SessionStoreStrategyType | undefined { const cookieOptions = buildCookieOptions(initConfiguration) @@ -21,25 +20,25 @@ export function initCookieStrategy(cookieOptions: CookieOptions): SessionStoreSt clearSession: deleteSessionCookie(cookieOptions), } - tryOldCookiesMigration(SESSION_COOKIE_NAME, cookieStore) + tryOldCookiesMigration(cookieStore) return cookieStore } function persistSessionCookie(options: CookieOptions) { return (session: SessionState) => { - setCookie(SESSION_COOKIE_NAME, toSessionString(session), SESSION_EXPIRATION_DELAY, options) + setCookie(SESSION_STORE_KEY, toSessionString(session), SESSION_EXPIRATION_DELAY, options) } } function retrieveSessionCookie(): SessionState { - const sessionString = getCookie(SESSION_COOKIE_NAME) + const sessionString = getCookie(SESSION_STORE_KEY) return toSessionState(sessionString) } function deleteSessionCookie(options: CookieOptions) { return () => { - deleteCookie(SESSION_COOKIE_NAME, options) + deleteCookie(SESSION_STORE_KEY, options) } } diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts index acc2272f02..3e0b1ddc8a 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts @@ -1,5 +1,6 @@ import type { SessionState } from '../sessionState' -import { LOCAL_STORAGE_KEY, selectLocalStorageStrategy, initLocalStorageStrategy } from './sessionInLocalStorage' +import { selectLocalStorageStrategy, initLocalStorageStrategy } from './sessionInLocalStorage' +import { SESSION_STORE_KEY } from './sessionStoreStrategy' describe('session in local storage strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } @@ -24,7 +25,7 @@ describe('session in local storage strategy', () => { localStorageStrategy.persistSession(sessionState) const session = localStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState }) - expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toMatch(/.*id=.*created/) + expect(window.localStorage.getItem(SESSION_STORE_KEY)).toMatch(/.*id=.*created/) }) it('should delete the local storage item holding the session', () => { @@ -33,7 +34,7 @@ describe('session in local storage strategy', () => { localStorageStrategy.clearSession() const session = localStorageStrategy?.retrieveSession() expect(session).toEqual({}) - expect(window.localStorage.getItem(LOCAL_STORAGE_KEY)).toBeNull() + expect(window.localStorage.getItem(SESSION_STORE_KEY)).toBeNull() }) it('should not interfere with other keys present in local storage', () => { @@ -47,7 +48,7 @@ describe('session in local storage strategy', () => { it('should return an empty object if session string is invalid', () => { const localStorageStrategy = initLocalStorageStrategy() - localStorage.setItem(LOCAL_STORAGE_KEY, '{test:42}') + localStorage.setItem(SESSION_STORE_KEY, '{test:42}') const session = localStorageStrategy?.retrieveSession() expect(session).toEqual({}) }) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index 8f07ea73bc..35532a2822 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -2,8 +2,8 @@ import { generateUUID } from '../../../tools/utils/stringUtils' import type { SessionState } from '../sessionState' import { toSessionString, toSessionState } from '../sessionState' import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' +import { SESSION_STORE_KEY } from './sessionStoreStrategy' -export const LOCAL_STORAGE_KEY = '_dd_s' const LOCAL_STORAGE_TEST_KEY = '_dd_test_' export function selectLocalStorageStrategy(): SessionStoreStrategyType | undefined { @@ -27,14 +27,14 @@ export function initLocalStorageStrategy(): SessionStoreStrategy { } function persistInLocalStorage(sessionState: SessionState) { - localStorage.setItem(LOCAL_STORAGE_KEY, toSessionString(sessionState)) + localStorage.setItem(SESSION_STORE_KEY, toSessionString(sessionState)) } function retrieveSessionFromLocalStorage(): SessionState { - const sessionString = localStorage.getItem(LOCAL_STORAGE_KEY) + const sessionString = localStorage.getItem(SESSION_STORE_KEY) return toSessionState(sessionString) } function clearSessionFromLocalStorage() { - localStorage.removeItem(LOCAL_STORAGE_KEY) + localStorage.removeItem(SESSION_STORE_KEY) } diff --git a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts index 286c63ff49..d4c9fab265 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionStoreStrategy.ts @@ -1,6 +1,8 @@ import type { CookieOptions } from '../../../browser/cookie' import type { SessionState } from '../sessionState' +export const SESSION_STORE_KEY = '_dd_s' + export type SessionStoreStrategyType = { type: 'Cookie'; cookieOptions: CookieOptions } | { type: 'LocalStorage' } export interface SessionStoreStrategy { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0816102421..a12b247026 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -101,7 +101,7 @@ export { } from './tools/serialisation/heavyCustomerDataWarning' export { ValueHistory, ValueHistoryEntry, CLEAR_OLD_VALUES_INTERVAL } from './tools/valueHistory' export { readBytesFromStream } from './tools/readBytesFromStream' -export { SESSION_COOKIE_NAME } from './domain/session/storeStrategies/sessionInCookie' +export { SESSION_STORE_KEY } from './domain/session/storeStrategies/sessionStoreStrategy' export { willSyntheticsInjectRum, getSyntheticsTestId, diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index 3c0b482007..cca957bc5c 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -1,4 +1,4 @@ -import { ErrorSource, display, stopSessionManager, getCookie, SESSION_COOKIE_NAME } from '@datadog/browser-core' +import { ErrorSource, display, stopSessionManager, getCookie, SESSION_STORE_KEY } from '@datadog/browser-core' import type { Request } from '@datadog/browser-core/test' import { interceptRequests, @@ -163,21 +163,21 @@ describe('logs', () => { it('creates a session on normal conditions', () => { ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) - expect(getCookie(SESSION_COOKIE_NAME)).not.toBeUndefined() + expect(getCookie(SESSION_STORE_KEY)).not.toBeUndefined() }) it('does not create a session if event bridge is present', () => { initEventBridgeStub() ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) it('does not create a session if synthetics worker will inject RUM', () => { mockSyntheticsWorkerValues({ injectsRum: true }) ;({ handleLog } = startLogs(initConfiguration, baseConfiguration, () => COMMON_CONTEXT, logger)) - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() }) }) }) diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index d7e9e55640..485d7bad95 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -1,8 +1,8 @@ import type { RelativeTime } from '@datadog/browser-core' import { + SESSION_STORE_KEY, COOKIE_ACCESS_DELAY, getCookie, - SESSION_COOKIE_NAME, setCookie, stopSessionManager, ONE_SECOND, @@ -46,8 +46,8 @@ describe('logs session manager', () => { startLogsSessionManager(configuration as LogsConfiguration) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]+/) + expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) }) it('when not tracked should store tracking type', () => { @@ -55,66 +55,66 @@ describe('logs session manager', () => { startLogsSessionManager(configuration as LogsConfiguration) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.NOT_TRACKED}`) - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') + expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.NOT_TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') }) it('when tracked should keep existing tracking type and session id', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&logs=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) startLogsSessionManager(configuration as LogsConfiguration) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) - expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcdef') + expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).toContain('id=abcdef') }) it('when not tracked should keep existing tracking type', () => { - setCookie(SESSION_COOKIE_NAME, 'logs=0', DURATION) + setCookie(SESSION_STORE_KEY, 'logs=0', DURATION) startLogsSessionManager(configuration as LogsConfiguration) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.NOT_TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.NOT_TRACKED}`) }) it('should renew on activity after expiration', () => { startLogsSessionManager(configuration as LogsConfiguration) - setCookie(SESSION_COOKIE_NAME, '', DURATION) - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + setCookie(SESSION_STORE_KEY, '', DURATION) + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() clock.tick(COOKIE_ACCESS_DELAY) tracked = true document.body.click() - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]+/) - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]+/) + expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`) }) describe('findSession', () => { it('should return the current session', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&logs=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) expect(logsSessionManager.findTrackedSession()!.id).toBe('abcdef') }) it('should return undefined if the session is not tracked', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&logs=0', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=0', DURATION) const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) expect(logsSessionManager.findTrackedSession()).toBe(undefined) }) it('should return undefined if the session has expired', () => { const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) - setCookie(SESSION_COOKIE_NAME, '', DURATION) + setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(COOKIE_ACCESS_DELAY) expect(logsSessionManager.findTrackedSession()).toBe(undefined) }) it('should return session corresponding to start time', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&logs=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION) const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) clock.tick(10 * ONE_SECOND) - setCookie(SESSION_COOKIE_NAME, '', DURATION) + setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(COOKIE_ACCESS_DELAY) expect(logsSessionManager.findTrackedSession()).toBeUndefined() expect(logsSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index c59eccbb84..89e6aeefcb 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -1,9 +1,9 @@ import type { RelativeTime } from '@datadog/browser-core' import { + SESSION_STORE_KEY, COOKIE_ACCESS_DELAY, getCookie, isIE, - SESSION_COOKIE_NAME, setCookie, stopSessionManager, ONE_SECOND, @@ -70,10 +70,10 @@ describe('rum session manager', () => { expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() - expect(getCookie(SESSION_COOKIE_NAME)).toContain( + expect(getCookie(SESSION_STORE_KEY)).toContain( `${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_WITH_SESSION_REPLAY}` ) - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/) + expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]/) }) it('when tracked without session replay should store session type and id', () => { @@ -83,10 +83,10 @@ describe('rum session manager', () => { expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() - expect(getCookie(SESSION_COOKIE_NAME)).toContain( + expect(getCookie(SESSION_STORE_KEY)).toContain( `${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_WITHOUT_SESSION_REPLAY}` ) - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/) + expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]/) }) it('when not tracked should store session type', () => { @@ -96,39 +96,39 @@ describe('rum session manager', () => { expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.NOT_TRACKED}`) - expect(getCookie(SESSION_COOKIE_NAME)).not.toContain('id=') + expect(getCookie(SESSION_STORE_KEY)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.NOT_TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=') }) it('when tracked should keep existing session type and id', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) startRumSessionManager(configuration, lifeCycle) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() - expect(getCookie(SESSION_COOKIE_NAME)).toContain( + expect(getCookie(SESSION_STORE_KEY)).toContain( `${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_WITH_SESSION_REPLAY}` ) - expect(getCookie(SESSION_COOKIE_NAME)).toContain('id=abcdef') + expect(getCookie(SESSION_STORE_KEY)).toContain('id=abcdef') }) it('when not tracked should keep existing session type', () => { - setCookie(SESSION_COOKIE_NAME, 'rum=0', DURATION) + setCookie(SESSION_STORE_KEY, 'rum=0', DURATION) startRumSessionManager(configuration, lifeCycle) expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() - expect(getCookie(SESSION_COOKIE_NAME)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.NOT_TRACKED}`) + expect(getCookie(SESSION_STORE_KEY)).toContain(`${RUM_SESSION_KEY}=${RumTrackingType.NOT_TRACKED}`) }) it('should renew on activity after expiration', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) startRumSessionManager(configuration, lifeCycle) - setCookie(SESSION_COOKIE_NAME, '', DURATION) - expect(getCookie(SESSION_COOKIE_NAME)).toBeUndefined() + setCookie(SESSION_STORE_KEY, '', DURATION) + expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() clock.tick(COOKIE_ACCESS_DELAY) @@ -138,51 +138,51 @@ describe('rum session manager', () => { expect(expireSessionSpy).toHaveBeenCalled() expect(renewSessionSpy).toHaveBeenCalled() - expect(getCookie(SESSION_COOKIE_NAME)).toContain( + expect(getCookie(SESSION_STORE_KEY)).toContain( `${RUM_SESSION_KEY}=${RumTrackingType.TRACKED_WITH_SESSION_REPLAY}` ) - expect(getCookie(SESSION_COOKIE_NAME)).toMatch(/id=[a-f0-9-]/) + expect(getCookie(SESSION_STORE_KEY)).toMatch(/id=[a-f0-9-]/) }) }) describe('findSession', () => { it('should return the current session', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) const rumSessionManager = startRumSessionManager(configuration, lifeCycle) expect(rumSessionManager.findTrackedSession()!.id).toBe('abcdef') }) it('should return undefined if the session is not tracked', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=0', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=0', DURATION) const rumSessionManager = startRumSessionManager(configuration, lifeCycle) expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) it('should return undefined if the session has expired', () => { const rumSessionManager = startRumSessionManager(configuration, lifeCycle) - setCookie(SESSION_COOKIE_NAME, '', DURATION) + setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(COOKIE_ACCESS_DELAY) expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) it('should return session corresponding to start time', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) const rumSessionManager = startRumSessionManager(configuration, lifeCycle) clock.tick(10 * ONE_SECOND) - setCookie(SESSION_COOKIE_NAME, '', DURATION) + setCookie(SESSION_STORE_KEY, '', DURATION) clock.tick(COOKIE_ACCESS_DELAY) expect(rumSessionManager.findTrackedSession()).toBeUndefined() expect(rumSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') }) it('should return session with plan WITH_SESSION_REPLAY', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=1', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=1', DURATION) const rumSessionManager = startRumSessionManager(configuration, lifeCycle) expect(rumSessionManager.findTrackedSession()!.plan).toBe(RumSessionPlan.WITH_SESSION_REPLAY) }) it('should return session with plan WITHOUT_SESSION_REPLAY', () => { - setCookie(SESSION_COOKIE_NAME, 'id=abcdef&rum=2', DURATION) + setCookie(SESSION_STORE_KEY, 'id=abcdef&rum=2', DURATION) const rumSessionManager = startRumSessionManager(configuration, lifeCycle) expect(rumSessionManager.findTrackedSession()!.plan).toBe(RumSessionPlan.WITHOUT_SESSION_REPLAY) }) diff --git a/test/e2e/lib/helpers/session.ts b/test/e2e/lib/helpers/session.ts index 499f551177..36a4efbe1b 100644 --- a/test/e2e/lib/helpers/session.ts +++ b/test/e2e/lib/helpers/session.ts @@ -1,4 +1,4 @@ -import { SESSION_COOKIE_NAME } from '@datadog/browser-core' +import { SESSION_STORE_KEY } from '@datadog/browser-core' import { deleteAllCookies } from './browser' export async function renewSession() { @@ -16,7 +16,7 @@ export async function expireSession() { } export async function findSessionCookie() { - const cookies = await browser.getCookies(SESSION_COOKIE_NAME) + const cookies = await browser.getCookies(SESSION_STORE_KEY) // In some case, the session cookie is returned but with an empty value. Let's consider it expired // in this case. return cookies[0]?.value || undefined diff --git a/test/e2e/scenario/sessionStore.scenario.ts b/test/e2e/scenario/sessionStore.scenario.ts index c53d96587a..9a9187b732 100644 --- a/test/e2e/scenario/sessionStore.scenario.ts +++ b/test/e2e/scenario/sessionStore.scenario.ts @@ -1,15 +1,13 @@ +import { SESSION_STORE_KEY } from '@datadog/browser-core' import { createTest } from '../lib/framework' -const SESSION_COOKIE_NAME = '_dd_s' -const SESSION_LOCAL_STORAGE_KEY = '_dd_s' - describe('Session Stores', () => { describe('Cookies', () => { createTest('Cookie Initialization') .withLogs() .withRum() .run(async () => { - const [cookie] = await browser.getCookies([SESSION_COOKIE_NAME]) + const [cookie] = await browser.getCookies([SESSION_STORE_KEY]) const cookieSessionId = cookie.value.match(/id=([\w-]+)/)![1] const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) @@ -27,10 +25,7 @@ describe('Session Stores', () => { // This will force the SDKs to initialize using local storage .withHead('') .run(async () => { - const sessionStateString = await browser.execute( - (key) => window.localStorage.getItem(key), - SESSION_LOCAL_STORAGE_KEY - ) + const sessionStateString = await browser.execute((key) => window.localStorage.getItem(key), SESSION_STORE_KEY) const sessionId = sessionStateString?.match(/id=([\w-]+)/)![1] const logsContext = await browser.execute(() => window.DD_LOGS?.getInternalContext()) From 1de407c3b1b15d6855946fb29fb5ac641867294b Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 12 Jun 2023 16:07:31 +0200 Subject: [PATCH 36/40] =?UTF-8?q?=F0=9F=91=8C=20Rename=20sessionStoreStrat?= =?UTF-8?q?egyType=20to=20STORE=5FTYPE=20+=20factorize=20testKey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/session/sessionManager.spec.ts | 176 ++++-------------- .../storeStrategies/sessionInLocalStorage.ts | 7 +- 2 files changed, 38 insertions(+), 145 deletions(-) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index bcbaddf74e..8d6429f55f 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -30,7 +30,7 @@ describe('startSessionManager', () => { const DURATION = 123456 const FIRST_PRODUCT_KEY = 'first' const SECOND_PRODUCT_KEY = 'second' - const sessionStoreStrategyType: SessionStoreStrategyType = { type: 'Cookie', cookieOptions: {} } + const STORE_TYPE: SessionStoreStrategyType = { type: 'Cookie', cookieOptions: {} } let clock: Clock function expireSessionCookie() { @@ -84,22 +84,14 @@ describe('startSessionManager', () => { describe('cookie management', () => { it('when tracked, should store tracking type and session id', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expectSessionIdToBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) }) it('when not tracked should store tracking type', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => NOT_TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -108,11 +100,7 @@ describe('startSessionManager', () => { it('when tracked should keep existing tracking type and session id', () => { setCookie(SESSION_STORE_KEY, 'id=abcdef&first=tracked', DURATION) - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expectSessionIdToBe(sessionManager, 'abcdef') expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.TRACKED) @@ -121,11 +109,7 @@ describe('startSessionManager', () => { it('when not tracked should keep existing tracking type', () => { setCookie(SESSION_STORE_KEY, 'first=not-tracked', DURATION) - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => NOT_TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) expectSessionIdToNotBeDefined(sessionManager) expectTrackingTypeToBe(sessionManager, FIRST_PRODUCT_KEY, FakeTrackingType.NOT_TRACKED) @@ -140,36 +124,32 @@ describe('startSessionManager', () => { }) it('should be called with an empty value if the cookie is not defined', () => { - startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) + startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(undefined) }) it('should be called with an invalid value if the cookie has an invalid value', () => { setCookie(SESSION_STORE_KEY, 'first=invalid', DURATION) - startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) + startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith('invalid') }) it('should be called with TRACKED', () => { setCookie(SESSION_STORE_KEY, 'first=tracked', DURATION) - startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) + startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.TRACKED) }) it('should be called with NOT_TRACKED', () => { setCookie(SESSION_STORE_KEY, 'first=not-tracked', DURATION) - startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, spy) + startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, spy) expect(spy).toHaveBeenCalledWith(FakeTrackingType.NOT_TRACKED) }) }) describe('session renewal', () => { it('should renew on activity after expiration', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -187,11 +167,7 @@ describe('startSessionManager', () => { }) it('should not renew on visibility after expiration', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const renewSessionSpy = jasmine.createSpy() sessionManager.renewObservable.subscribe(renewSessionSpy) @@ -206,25 +182,17 @@ describe('startSessionManager', () => { describe('multiple startSessionManager calls', () => { it('should re-use the same session id', () => { - const firstSessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const firstSessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const idA = firstSessionManager.findActiveSession()!.id - const secondSessionManager = startSessionManager( - sessionStoreStrategyType, - SECOND_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const secondSessionManager = startSessionManager(STORE_TYPE, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const idB = secondSessionManager.findActiveSession()!.id expect(idA).toBe(idB) }) it('should not erase other session type', () => { - startSessionManager(sessionStoreStrategyType, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // schedule an expandOrRenewSession document.dispatchEvent(new CustomEvent('click')) @@ -234,7 +202,7 @@ describe('startSessionManager', () => { // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) - startSessionManager(sessionStoreStrategyType, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + startSessionManager(STORE_TYPE, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // cookie correctly set expect(getCookie(SESSION_STORE_KEY)).toContain('first') @@ -248,37 +216,21 @@ describe('startSessionManager', () => { }) it('should have independent tracking types', () => { - const firstSessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) - const secondSessionManager = startSessionManager( - sessionStoreStrategyType, - SECOND_PRODUCT_KEY, - () => NOT_TRACKED_SESSION_STATE - ) + const firstSessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) + const secondSessionManager = startSessionManager(STORE_TYPE, SECOND_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) expect(firstSessionManager.findActiveSession()!.trackingType).toEqual(FakeTrackingType.TRACKED) expect(secondSessionManager.findActiveSession()!.trackingType).toEqual(FakeTrackingType.NOT_TRACKED) }) it('should notify each expire and renew observables', () => { - const firstSessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const firstSessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionASpy = jasmine.createSpy() firstSessionManager.expireObservable.subscribe(expireSessionASpy) const renewSessionASpy = jasmine.createSpy() firstSessionManager.renewObservable.subscribe(renewSessionASpy) - const secondSessionManager = startSessionManager( - sessionStoreStrategyType, - SECOND_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const secondSessionManager = startSessionManager(STORE_TYPE, SECOND_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionBSpy = jasmine.createSpy() secondSessionManager.expireObservable.subscribe(expireSessionBSpy) const renewSessionBSpy = jasmine.createSpy() @@ -300,11 +252,7 @@ describe('startSessionManager', () => { describe('session timeout', () => { it('should expire the session when the time out delay is reached', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -320,11 +268,7 @@ describe('startSessionManager', () => { it('should renew an existing timed out session', () => { setCookie(SESSION_STORE_KEY, `id=abcde&first=tracked&created=${Date.now() - SESSION_TIME_OUT_DELAY}`, DURATION) - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -336,11 +280,7 @@ describe('startSessionManager', () => { it('should not add created date to an existing session from an older versions', () => { setCookie(SESSION_STORE_KEY, 'id=abcde&first=tracked', DURATION) - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expect(sessionManager.findActiveSession()!.id).toBe('abcde') expect(getCookie(SESSION_STORE_KEY)).not.toContain('created=') @@ -357,11 +297,7 @@ describe('startSessionManager', () => { }) it('should expire the session after expiration delay', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -373,11 +309,7 @@ describe('startSessionManager', () => { }) it('should expand duration on activity', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -396,11 +328,7 @@ describe('startSessionManager', () => { }) it('should expand not tracked session duration on activity', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => NOT_TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -421,11 +349,7 @@ describe('startSessionManager', () => { it('should expand session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -446,11 +370,7 @@ describe('startSessionManager', () => { it('should expand not tracked session on visibility', () => { setPageVisibility('visible') - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => NOT_TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => NOT_TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -471,11 +391,7 @@ describe('startSessionManager', () => { describe('manual session expiration', () => { it('expires the session when calling expire()', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -486,11 +402,7 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() multiple times', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -502,11 +414,7 @@ describe('startSessionManager', () => { }) it('notifies expired session only once when calling expire() after the session has been expired', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) const expireSessionSpy = jasmine.createSpy() sessionManager.expireObservable.subscribe(expireSessionSpy) @@ -518,11 +426,7 @@ describe('startSessionManager', () => { }) it('renew the session on user activity', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) clock.tick(COOKIE_ACCESS_DELAY) sessionManager.expire() @@ -535,33 +439,21 @@ describe('startSessionManager', () => { describe('session history', () => { it('should return undefined when there is no current session and no startTime', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expireSessionCookie() expect(sessionManager.findActiveSession()).toBeUndefined() }) it('should return the current session context when there is no start time', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) expect(sessionManager.findActiveSession()!.id).toBeDefined() expect(sessionManager.findActiveSession()!.trackingType).toBeDefined() }) it('should return the session context corresponding to startTime', () => { - const sessionManager = startSessionManager( - sessionStoreStrategyType, - FIRST_PRODUCT_KEY, - () => TRACKED_SESSION_STATE - ) + const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // 0s to 10s: first session clock.tick(10 * ONE_SECOND - COOKIE_ACCESS_DELAY) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index 35532a2822..b07832b6ab 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -9,9 +9,10 @@ const LOCAL_STORAGE_TEST_KEY = '_dd_test_' export function selectLocalStorageStrategy(): SessionStoreStrategyType | undefined { try { const id = generateUUID() - localStorage.setItem(`${LOCAL_STORAGE_TEST_KEY}${id}`, id) - const retrievedId = localStorage.getItem(`${LOCAL_STORAGE_TEST_KEY}${id}`) - localStorage.removeItem(`${LOCAL_STORAGE_TEST_KEY}${id}`) + const testKey = `${LOCAL_STORAGE_TEST_KEY}${id}` + localStorage.setItem(testKey, id) + const retrievedId = localStorage.getItem(testKey) + localStorage.removeItem(testKey) return id === retrievedId ? { type: 'LocalStorage' } : undefined } catch (e) { return undefined From dae11c9156cdaf7eee0418140bd70a8081f175cc Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 12 Jun 2023 16:19:42 +0200 Subject: [PATCH 37/40] =?UTF-8?q?=F0=9F=91=8C=20Remove=20COOKIE=5FACCESS?= =?UTF-8?q?=5FDELAY=20in=20favor=20of=20STORAGE=5FPOLL=5FDELAY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/browser/cookie.ts | 2 -- .../src/domain/session/sessionManager.spec.ts | 13 +++++++------ .../core/src/domain/session/sessionStore.spec.ts | 16 ++++++++-------- packages/core/src/domain/session/sessionStore.ts | 11 ++++++++--- packages/core/src/index.ts | 3 ++- .../logs/src/domain/logsSessionManager.spec.ts | 8 ++++---- .../src/domain/rumSessionManager.spec.ts | 8 ++++---- 7 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/core/src/browser/cookie.ts b/packages/core/src/browser/cookie.ts index 075299fd6f..7d9bfd1389 100644 --- a/packages/core/src/browser/cookie.ts +++ b/packages/core/src/browser/cookie.ts @@ -2,8 +2,6 @@ import { display } from '../tools/display' import { ONE_MINUTE, ONE_SECOND } from '../tools/utils/timeUtils' import { findCommaSeparatedValue, generateUUID } from '../tools/utils/stringUtils' -export const COOKIE_ACCESS_DELAY = ONE_SECOND - export interface CookieOptions { secure?: boolean crossSite?: boolean diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 8d6429f55f..36e48bebb0 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -1,6 +1,6 @@ import { createNewEvent, mockClock, restorePageVisibility, setPageVisibility } from '../../../test' import type { Clock } from '../../../test' -import { COOKIE_ACCESS_DELAY, getCookie, setCookie } from '../../browser/cookie' +import { getCookie, setCookie } from '../../browser/cookie' import type { RelativeTime } from '../../tools/utils/timeUtils' import { isIE } from '../../tools/utils/browserDetection' import { DOM_EVENT } from '../../browser/addEventListener' @@ -10,6 +10,7 @@ import { startSessionManager, stopSessionManager, VISIBILITY_CHECK_DELAY } from import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' +import { STORAGE_POLL_DELAY } from './sessionStore' const enum FakeTrackingType { NOT_TRACKED = 'not-tracked', @@ -35,7 +36,7 @@ describe('startSessionManager', () => { function expireSessionCookie() { setCookie(SESSION_STORE_KEY, '', DURATION) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) } function expectSessionIdToBe(sessionManager: SessionManager, sessionId: string) { @@ -197,7 +198,7 @@ describe('startSessionManager', () => { // schedule an expandOrRenewSession document.dispatchEvent(new CustomEvent('click')) - clock.tick(COOKIE_ACCESS_DELAY / 2) + clock.tick(STORAGE_POLL_DELAY / 2) // expand first session cookie cache document.dispatchEvent(createNewEvent(DOM_EVENT.VISIBILITY_CHANGE)) @@ -208,7 +209,7 @@ describe('startSessionManager', () => { expect(getCookie(SESSION_STORE_KEY)).toContain('first') expect(getCookie(SESSION_STORE_KEY)).toContain('second') - clock.tick(COOKIE_ACCESS_DELAY / 2) + clock.tick(STORAGE_POLL_DELAY / 2) // scheduled expandOrRenewSession should not use cached value expect(getCookie(SESSION_STORE_KEY)).toContain('first') @@ -427,7 +428,7 @@ describe('startSessionManager', () => { it('renew the session on user activity', () => { const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) sessionManager.expire() @@ -456,7 +457,7 @@ describe('startSessionManager', () => { const sessionManager = startSessionManager(STORE_TYPE, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE) // 0s to 10s: first session - clock.tick(10 * ONE_SECOND - COOKIE_ACCESS_DELAY) + clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY) const firstSessionId = sessionManager.findActiveSession()!.id const firstSessionTrackingType = sessionManager.findActiveSession()!.trackingType expireSessionCookie() diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 802370addc..a897357103 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,8 +1,8 @@ import type { Clock } from '../../../test' import { mockClock } from '../../../test' -import { getCookie, setCookie, COOKIE_ACCESS_DELAY } from '../../browser/cookie' +import { getCookie, setCookie } from '../../browser/cookie' import type { SessionStore } from './sessionStore' -import { startSessionStore, selectSessionStoreStrategyType } from './sessionStore' +import { STORAGE_POLL_DELAY, startSessionStore, selectSessionStoreStrategyType } from './sessionStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' @@ -333,7 +333,7 @@ describe('session store', () => { it('when session not in cache and session not in store, should do nothing', () => { setupSessionStore() - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).not.toHaveBeenCalled() @@ -343,7 +343,7 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).not.toHaveBeenCalled() @@ -354,7 +354,7 @@ describe('session store', () => { setupSessionStore() resetSessionInStore() - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() @@ -365,7 +365,7 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID, Date.now() + SESSION_TIME_OUT_DELAY + 10) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(sessionStoreManager.getSession().id).toBe(FIRST_ID) expect(sessionStoreManager.getSession().expire).toBe(getStoreExpiration()) @@ -377,7 +377,7 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, SECOND_ID) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() @@ -388,7 +388,7 @@ describe('session store', () => { setupSessionStore() setSessionInStore(FakeTrackingType.TRACKED, FIRST_ID) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(sessionStoreManager.getSession().id).toBeUndefined() expect(expireSpy).toHaveBeenCalled() diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index f3daa6fdec..fecc7c44a9 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -21,7 +21,12 @@ export interface SessionStore { stop: () => void } -const POLL_DELAY = ONE_SECOND +/** + * Every second, the storage will be polled to check for any change that can occur + * to the session state in another browser tab, or another window. + * This value has been determined from our previous cookie-only implementation. + */ +export const STORAGE_POLL_DELAY = ONE_SECOND /** * Checks if cookies are available as the preferred storage @@ -57,7 +62,7 @@ export function startSessionStore( : initLocalStorageStrategy() const { clearSession, retrieveSession } = sessionStoreStrategy - const watchSessionTimeoutId = setInterval(watchSession, POLL_DELAY) + const watchSessionTimeoutId = setInterval(watchSession, STORAGE_POLL_DELAY) let sessionCache: SessionState = retrieveActiveSession() function expandOrRenewSession() { @@ -164,7 +169,7 @@ export function startSessionStore( } return { - expandOrRenewSession: throttle(expandOrRenewSession, POLL_DELAY).throttled, + expandOrRenewSession: throttle(expandOrRenewSession, STORAGE_POLL_DELAY).throttled, expandSession, getSession: () => sessionCache, renewObservable, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a12b247026..4338cb3c6f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -84,7 +84,7 @@ export { } from './domain/error/error' export { NonErrorPrefix } from './domain/error/error.types' export { Context, ContextArray, ContextValue } from './tools/serialisation/context' -export { areCookiesAuthorized, getCookie, setCookie, deleteCookie, COOKIE_ACCESS_DELAY } from './browser/cookie' +export { areCookiesAuthorized, getCookie, setCookie, deleteCookie } from './browser/cookie' export { initXhrObservable, XhrCompleteContext, XhrStartContext } from './browser/xhrObservable' export { initFetchObservable, FetchResolveContext, FetchStartContext, FetchContext } from './browser/fetchObservable' export { createPageExitObservable, PageExitEvent, PageExitReason, isPageExitReason } from './browser/pageExitObservable' @@ -101,6 +101,7 @@ export { } from './tools/serialisation/heavyCustomerDataWarning' export { ValueHistory, ValueHistoryEntry, CLEAR_OLD_VALUES_INTERVAL } from './tools/valueHistory' export { readBytesFromStream } from './tools/readBytesFromStream' +export { STORAGE_POLL_DELAY } from './domain/session/sessionStore' export { SESSION_STORE_KEY } from './domain/session/storeStrategies/sessionStoreStrategy' export { willSyntheticsInjectRum, diff --git a/packages/logs/src/domain/logsSessionManager.spec.ts b/packages/logs/src/domain/logsSessionManager.spec.ts index 485d7bad95..62f7aea34d 100644 --- a/packages/logs/src/domain/logsSessionManager.spec.ts +++ b/packages/logs/src/domain/logsSessionManager.spec.ts @@ -1,7 +1,7 @@ import type { RelativeTime } from '@datadog/browser-core' import { + STORAGE_POLL_DELAY, SESSION_STORE_KEY, - COOKIE_ACCESS_DELAY, getCookie, setCookie, stopSessionManager, @@ -81,7 +81,7 @@ describe('logs session manager', () => { setCookie(SESSION_STORE_KEY, '', DURATION) expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) tracked = true document.body.click() @@ -106,7 +106,7 @@ describe('logs session manager', () => { it('should return undefined if the session has expired', () => { const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) setCookie(SESSION_STORE_KEY, '', DURATION) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(logsSessionManager.findTrackedSession()).toBe(undefined) }) @@ -115,7 +115,7 @@ describe('logs session manager', () => { const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration) clock.tick(10 * ONE_SECOND) setCookie(SESSION_STORE_KEY, '', DURATION) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(logsSessionManager.findTrackedSession()).toBeUndefined() expect(logsSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') }) diff --git a/packages/rum-core/src/domain/rumSessionManager.spec.ts b/packages/rum-core/src/domain/rumSessionManager.spec.ts index 89e6aeefcb..7199060a2e 100644 --- a/packages/rum-core/src/domain/rumSessionManager.spec.ts +++ b/packages/rum-core/src/domain/rumSessionManager.spec.ts @@ -1,7 +1,7 @@ import type { RelativeTime } from '@datadog/browser-core' import { + STORAGE_POLL_DELAY, SESSION_STORE_KEY, - COOKIE_ACCESS_DELAY, getCookie, isIE, setCookie, @@ -131,7 +131,7 @@ describe('rum session manager', () => { expect(getCookie(SESSION_STORE_KEY)).toBeUndefined() expect(expireSessionSpy).not.toHaveBeenCalled() expect(renewSessionSpy).not.toHaveBeenCalled() - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) setupDraws({ tracked: true, trackedWithSessionReplay: true }) document.dispatchEvent(new CustomEvent('click')) @@ -161,7 +161,7 @@ describe('rum session manager', () => { it('should return undefined if the session has expired', () => { const rumSessionManager = startRumSessionManager(configuration, lifeCycle) setCookie(SESSION_STORE_KEY, '', DURATION) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(rumSessionManager.findTrackedSession()).toBe(undefined) }) @@ -170,7 +170,7 @@ describe('rum session manager', () => { const rumSessionManager = startRumSessionManager(configuration, lifeCycle) clock.tick(10 * ONE_SECOND) setCookie(SESSION_STORE_KEY, '', DURATION) - clock.tick(COOKIE_ACCESS_DELAY) + clock.tick(STORAGE_POLL_DELAY) expect(rumSessionManager.findTrackedSession()).toBeUndefined() expect(rumSessionManager.findTrackedSession(0 as RelativeTime)!.id).toBe('abcdef') }) From 8ac8692a1fb2c3882898757f47b3721a30b150dd Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 12 Jun 2023 16:26:16 +0200 Subject: [PATCH 38/40] =?UTF-8?q?=F0=9F=91=8C=20Add=20a=20test=20on=20conf?= =?UTF-8?q?iguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/configuration/configuration.spec.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/domain/configuration/configuration.spec.ts b/packages/core/src/domain/configuration/configuration.spec.ts index 344951a7fc..c8c71f9b3e 100644 --- a/packages/core/src/domain/configuration/configuration.spec.ts +++ b/packages/core/src/domain/configuration/configuration.spec.ts @@ -125,6 +125,14 @@ describe('validateAndBuildConfiguration', () => { }) it('should contain cookie in the configuration by default', () => { + const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: false }) + expect(configuration?.sessionStoreStrategyType).toEqual({ + type: 'Cookie', + cookieOptions: { secure: false, crossSite: false }, + }) + }) + + it('should contain cookie in the configuration when fallback is enabled and cookies are available', () => { const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true }) expect(configuration?.sessionStoreStrategyType).toEqual({ type: 'Cookie', @@ -132,7 +140,7 @@ describe('validateAndBuildConfiguration', () => { }) }) - it('should contain local storage in the configuration when enabled and cookies are not available', () => { + it('should contain local storage in the configuration when fallback is enabled and cookies are not available', () => { spyOnProperty(document, 'cookie', 'get').and.returnValue('') const configuration = validateAndBuildConfiguration({ clientToken, allowFallbackToLocalStorage: true }) expect(configuration?.sessionStoreStrategyType).toEqual({ type: 'LocalStorage' }) From f3f7b929f462dd36fe3237435de03f84987e8b89 Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Mon, 12 Jun 2023 16:38:49 +0200 Subject: [PATCH 39/40] =?UTF-8?q?=F0=9F=91=8C=20Factorize=20into=20expandS?= =?UTF-8?q?essionState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/src/domain/session/oldCookiesMigration.ts | 6 ++---- .../core/src/domain/session/sessionState.spec.ts | 13 ++++++++++++- packages/core/src/domain/session/sessionState.ts | 6 ++++++ .../domain/session/sessionStoreOperations.spec.ts | 5 ++--- .../src/domain/session/sessionStoreOperations.ts | 6 ++---- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/core/src/domain/session/oldCookiesMigration.ts b/packages/core/src/domain/session/oldCookiesMigration.ts index 6b9314a7cf..d9fdbde510 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.ts @@ -1,10 +1,8 @@ import { getCookie } from '../../browser/cookie' -import { dateNow } from '../../tools/utils/timeUtils' import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' -import { isSessionInExpiredState } from './sessionState' -import { SESSION_EXPIRATION_DELAY } from './sessionConstants' +import { expandSessionState, isSessionInExpiredState } from './sessionState' export const OLD_SESSION_COOKIE_NAME = '_dd' export const OLD_RUM_COOKIE_NAME = '_dd_r' @@ -37,7 +35,7 @@ export function tryOldCookiesMigration(cookieStoreStrategy: SessionStoreStrategy } if (!isSessionInExpiredState(session)) { - session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) + expandSessionState(session) cookieStoreStrategy.persistSession(session) } } diff --git a/packages/core/src/domain/session/sessionState.spec.ts b/packages/core/src/domain/session/sessionState.spec.ts index 253fb69d78..0f04b5fb2f 100644 --- a/packages/core/src/domain/session/sessionState.spec.ts +++ b/packages/core/src/domain/session/sessionState.spec.ts @@ -1,5 +1,7 @@ +import { dateNow } from '../../tools/utils/timeUtils' +import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionState } from './sessionState' -import { isSessionInExpiredState, toSessionString, toSessionState } from './sessionState' +import { expandSessionState, isSessionInExpiredState, toSessionString, toSessionState } from './sessionState' describe('session state utilities', () => { const EXPIRED_SESSION: SessionState = {} @@ -41,4 +43,13 @@ describe('session state utilities', () => { expect(toSessionState(sessionString)).toEqual(EXPIRED_SESSION) }) }) + + describe('expandSessionState', () => { + it('should modify the expire property of the session', () => { + const session = { ...LIVE_SESSION } + const now = dateNow() + expandSessionState(session) + expect(session.expire).toBeGreaterThanOrEqual(now + SESSION_EXPIRATION_DELAY) + }) + }) }) diff --git a/packages/core/src/domain/session/sessionState.ts b/packages/core/src/domain/session/sessionState.ts index 2df2deb3a4..5f34e81543 100644 --- a/packages/core/src/domain/session/sessionState.ts +++ b/packages/core/src/domain/session/sessionState.ts @@ -1,5 +1,7 @@ import { isEmptyObject } from '../../tools/utils/objectUtils' import { objectEntries } from '../../tools/utils/polyfills' +import { dateNow } from '../../tools/utils/timeUtils' +import { SESSION_EXPIRATION_DELAY } from './sessionConstants' const SESSION_ENTRY_REGEXP = /^([a-z]+)=([a-z0-9-]+)$/ const SESSION_ENTRY_SEPARATOR = '&' @@ -17,6 +19,10 @@ export function isSessionInExpiredState(session: SessionState) { return isEmptyObject(session) } +export function expandSessionState(session: SessionState) { + session.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) +} + export function toSessionString(session: SessionState) { return objectEntries(session) .map(([key, value]) => `${key}=${value as string}`) diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 7eb31d9455..0ae57375f4 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -1,11 +1,10 @@ import type { StubStorage } from '../../../test' import { mockClock, stubCookieProvider, stubLocalStorageProvider } from '../../../test' import type { CookieOptions } from '../../browser/cookie' -import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { initCookieStrategy } from './storeStrategies/sessionInCookie' import { initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' import type { SessionState } from './sessionState' -import { toSessionString } from './sessionState' +import { expandSessionState, toSessionString } from './sessionState' import { processSessionStoreOperations, isLockEnabled, @@ -195,7 +194,7 @@ const cookieOptions: CookieOptions = {} retryState: { ...initialSession, other: 'other' }, }), }) - initialSession.expire = String(Date.now() + SESSION_EXPIRATION_DELAY) + expandSessionState(initialSession) sessionStoreStrategy.persistSession(initialSession) processSpy.and.callFake((session) => ({ ...session, processed: 'processed' } as SessionState)) diff --git a/packages/core/src/domain/session/sessionStoreOperations.ts b/packages/core/src/domain/session/sessionStoreOperations.ts index d17e45d187..aaa3d94395 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.ts @@ -1,11 +1,9 @@ import { setTimeout } from '../../tools/timer' -import { dateNow } from '../../tools/utils/timeUtils' import { generateUUID } from '../../tools/utils/stringUtils' import { isChromium } from '../../tools/utils/browserDetection' -import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' import type { SessionState } from './sessionState' -import { isSessionInExpiredState } from './sessionState' +import { expandSessionState, isSessionInExpiredState } from './sessionState' type Operations = { process: (sessionState: SessionState) => SessionState | undefined @@ -68,7 +66,7 @@ export function processSessionStoreOperations( if (isSessionInExpiredState(processedSession)) { clearSession() } else { - processedSession.expire = String(dateNow() + SESSION_EXPIRATION_DELAY) + expandSessionState(processedSession) persistSession(processedSession) } } From d5f66338c7ca7f542fd3bb10ca3a62ab391985fc Mon Sep 17 00:00:00 2001 From: Yannick Adam Date: Wed, 14 Jun 2023 15:35:50 +0200 Subject: [PATCH 40/40] =?UTF-8?q?=F0=9F=91=8C=20Fix=20typo=20in=20sessionS?= =?UTF-8?q?tate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/domain/session/sessionStore.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index fecc7c44a9..449cca08d3 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -159,12 +159,12 @@ export function startSessionStore( return {} } - function isActiveSession(sessionDate: SessionState) { + function isActiveSession(sessionState: SessionState) { // created and expire can be undefined for versions which was not storing them // these checks could be removed when older versions will not be available/live anymore return ( - (sessionDate.created === undefined || dateNow() - Number(sessionDate.created) < SESSION_TIME_OUT_DELAY) && - (sessionDate.expire === undefined || dateNow() < Number(sessionDate.expire)) + (sessionState.created === undefined || dateNow() - Number(sessionState.created) < SESSION_TIME_OUT_DELAY) && + (sessionState.expire === undefined || dateNow() < Number(sessionState.expire)) ) }