diff --git a/packages/core/src/domain/configuration/configuration.ts b/packages/core/src/domain/configuration/configuration.ts index 79cdc030f4..b9653bf367 100644 --- a/packages/core/src/domain/configuration/configuration.ts +++ b/packages/core/src/domain/configuration/configuration.ts @@ -114,7 +114,11 @@ export interface InitConfiguration { * @default false */ trackSessionAcrossSubdomains?: boolean | undefined - + /** + * Track anonymous user for the same site and extend cookie expiration date + * @default true + */ + trackAnonymousUser?: boolean | undefined // internal options /** * [Internal option] Enable experimental features @@ -173,6 +177,7 @@ export interface Configuration extends TransportConfiguration { allowUntrustedEvents: boolean trackingConsent: TrackingConsent storeContextsAcrossPages: boolean + trackAnonymousUser?: boolean // Event limits eventRateLimiterThreshold: number // Limit the maximum number of actions, errors and logs per minutes @@ -248,6 +253,7 @@ export function validateAndBuildConfiguration(initConfiguration: InitConfigurati silentMultipleInit: !!initConfiguration.silentMultipleInit, allowUntrustedEvents: !!initConfiguration.allowUntrustedEvents, trackingConsent: initConfiguration.trackingConsent ?? TrackingConsent.GRANTED, + trackAnonymousUser: initConfiguration.trackAnonymousUser ?? true, storeContextsAcrossPages: !!initConfiguration.storeContextsAcrossPages, /** * beacon payload max queue size implementation is 64kb @@ -285,6 +291,7 @@ export function serializeConfiguration(initConfiguration: InitConfiguration) { use_proxy: !!initConfiguration.proxy, silent_multiple_init: initConfiguration.silentMultipleInit, track_session_across_subdomains: initConfiguration.trackSessionAcrossSubdomains, + track_anonymous_user: initConfiguration.trackAnonymousUser, allow_fallback_to_local_storage: !!initConfiguration.allowFallbackToLocalStorage, store_contexts_across_pages: !!initConfiguration.storeContextsAcrossPages, allow_untrusted_events: !!initConfiguration.allowUntrustedEvents, diff --git a/packages/core/src/domain/session/oldCookiesMigration.spec.ts b/packages/core/src/domain/session/oldCookiesMigration.spec.ts index e4382195b3..273b7897ea 100644 --- a/packages/core/src/domain/session/oldCookiesMigration.spec.ts +++ b/packages/core/src/domain/session/oldCookiesMigration.spec.ts @@ -1,4 +1,5 @@ import { getCookie, resetInitCookies, setCookie } from '../../browser/cookie' +import type { Configuration } from '../configuration' import { OLD_LOGS_COOKIE_NAME, OLD_RUM_COOKIE_NAME, @@ -9,12 +10,13 @@ import { SESSION_EXPIRATION_DELAY } from './sessionConstants' import { initCookieStrategy } from './storeStrategies/sessionInCookie' import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy' import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' +const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration describe('old cookies migration', () => { let sessionStoreStrategy: SessionStoreStrategy beforeEach(() => { - sessionStoreStrategy = initCookieStrategy({}) + sessionStoreStrategy = initCookieStrategy(DEFAULT_INIT_CONFIGURATION, {}) resetInitCookies() }) diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 6a29e95058..d9bc5f1a7d 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -599,7 +599,7 @@ describe('startSessionManager', () => { trackingConsentState.update(TrackingConsent.NOT_GRANTED) expectSessionToBeExpired(sessionManager) - expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1&aid=0') + expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1') }) it('does not renew the session when tracking consent is withdrawn', () => { diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 142cdf655b..03caf2f49c 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -44,7 +44,12 @@ export function startSessionManager( const expireObservable = new Observable() // TODO - Improve configuration type and remove assertion - const sessionStore = startSessionStore(configuration.sessionStoreStrategyType!, productKey, computeSessionState) + const sessionStore = startSessionStore( + configuration.sessionStoreStrategyType!, + configuration, + productKey, + computeSessionState + ) stopCallbacks.push(() => sessionStore.stop()) const sessionContextHistory = createValueHistory>({ diff --git a/packages/core/src/domain/session/sessionState.ts b/packages/core/src/domain/session/sessionState.ts index ff63c6dc27..817ed83c79 100644 --- a/packages/core/src/domain/session/sessionState.ts +++ b/packages/core/src/domain/session/sessionState.ts @@ -15,14 +15,19 @@ export interface SessionState { [key: string]: string | undefined } -export function getExpiredSessionState(previousSessionState: SessionState | undefined): SessionState { +export function getExpiredSessionState( + previousSessionState: SessionState | undefined, + trackAnonymousUser: boolean +): SessionState { const expiredSessionState: SessionState = { isExpired: EXPIRED, } - if (previousSessionState?.anonymousId) { - expiredSessionState.anonymousId = previousSessionState?.anonymousId - } else { - expiredSessionState.anonymousId = generateAnonymousId() + if (trackAnonymousUser) { + if (previousSessionState?.anonymousId) { + expiredSessionState.anonymousId = previousSessionState?.anonymousId + } else { + expiredSessionState.anonymousId = generateAnonymousId() + } } return expiredSessionState } diff --git a/packages/core/src/domain/session/sessionStore.spec.ts b/packages/core/src/domain/session/sessionStore.spec.ts index 6008802cc0..656c900398 100644 --- a/packages/core/src/domain/session/sessionStore.spec.ts +++ b/packages/core/src/domain/session/sessionStore.spec.ts @@ -1,6 +1,7 @@ import type { Clock } from '../../../test' import { expireCookie, mockClock } from '../../../test' import { getCookie, setCookie } from '../../browser/cookie' +import type { Configuration } from '../configuration' import type { SessionStore } from './sessionStore' import { STORAGE_POLL_DELAY, startSessionStore, selectSessionStoreStrategyType } from './sessionStore' import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants' @@ -17,6 +18,7 @@ const PRODUCT_KEY = 'product' const FIRST_ID = 'first' const SECOND_ID = 'second' const EXPIRED_SESSION: SessionState = { isExpired: '1', anonymousId: '0' } +const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) { setCookie( @@ -116,7 +118,12 @@ describe('session store', () => { fail('Unable to initialize cookie storage') return } - sessionStoreManager = startSessionStore(sessionStoreStrategyType, PRODUCT_KEY, computeSessionState) + sessionStoreManager = startSessionStore( + sessionStoreStrategyType, + DEFAULT_INIT_CONFIGURATION, + PRODUCT_KEY, + computeSessionState + ) sessionStoreManager.expireObservable.subscribe(expireSpy) sessionStoreManager.renewObservable.subscribe(renewSpy) } @@ -492,7 +499,12 @@ describe('session store', () => { allowFallbackToLocalStorage: false, }) - const sessionStoreManager = startSessionStore(sessionStoreStrategyType!, PRODUCT_KEY, computeSessionState) + const sessionStoreManager = startSessionStore( + sessionStoreStrategyType!, + DEFAULT_INIT_CONFIGURATION, + PRODUCT_KEY, + computeSessionState + ) sessionStoreManager.sessionStateUpdateObservable.subscribe(updateSpy) return sessionStoreManager diff --git a/packages/core/src/domain/session/sessionStore.ts b/packages/core/src/domain/session/sessionStore.ts index 7790d6a971..24f1a71ed5 100644 --- a/packages/core/src/domain/session/sessionStore.ts +++ b/packages/core/src/domain/session/sessionStore.ts @@ -3,7 +3,7 @@ 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 type { InitConfiguration, Configuration } from '../configuration' import { selectCookieStrategy, initCookieStrategy } from './storeStrategies/sessionInCookie' import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy' import { @@ -58,6 +58,7 @@ export function selectSessionStoreStrategyType( */ export function startSessionStore( sessionStoreStrategyType: SessionStoreStrategyType, + configuration: Configuration, productKey: string, computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean } ): SessionStore { @@ -67,8 +68,8 @@ export function startSessionStore( const sessionStoreStrategy = sessionStoreStrategyType.type === 'Cookie' - ? initCookieStrategy(sessionStoreStrategyType.cookieOptions) - : initLocalStorageStrategy() + ? initCookieStrategy(configuration, sessionStoreStrategyType.cookieOptions) + : initLocalStorageStrategy(configuration) const { expireSession } = sessionStoreStrategy const watchSessionTimeoutId = setInterval(watchSession, STORAGE_POLL_DELAY) @@ -117,7 +118,9 @@ export function startSessionStore( processSessionStoreOperations( { process: (sessionState) => - isSessionInExpiredState(sessionState) ? getExpiredSessionState(sessionState) : undefined, + isSessionInExpiredState(sessionState) + ? getExpiredSessionState(sessionState, !!configuration.trackAnonymousUser) + : undefined, after: synchronizeSession, }, sessionStoreStrategy @@ -126,7 +129,7 @@ export function startSessionStore( function synchronizeSession(sessionState: SessionState) { if (isSessionInExpiredState(sessionState)) { - sessionState = getExpiredSessionState(sessionState) + sessionState = getExpiredSessionState(sessionState, !!configuration.trackAnonymousUser) } if (hasSessionInCache()) { if (isSessionInCacheOutdated(sessionState)) { @@ -144,7 +147,7 @@ export function startSessionStore( { process: (sessionState) => { if (isSessionInNotStartedState(sessionState)) { - return getExpiredSessionState(sessionState) + return getExpiredSessionState(sessionState, !!configuration.trackAnonymousUser) } }, after: (sessionState) => { @@ -178,7 +181,7 @@ export function startSessionStore( } function expireSessionInCache() { - sessionCache = getExpiredSessionState(sessionCache) + sessionCache = getExpiredSessionState(sessionCache, !!configuration.trackAnonymousUser) expireObservable.notify() } @@ -208,7 +211,7 @@ export function startSessionStore( expire: () => { cancelExpandOrRenewSession() expireSession(sessionCache) - synchronizeSession(getExpiredSessionState(sessionCache)) + synchronizeSession(getExpiredSessionState(sessionCache, !!configuration.trackAnonymousUser)) }, stop: () => { clearInterval(watchSessionTimeoutId) diff --git a/packages/core/src/domain/session/sessionStoreOperations.spec.ts b/packages/core/src/domain/session/sessionStoreOperations.spec.ts index 2129db9bd0..7f166b7ff9 100644 --- a/packages/core/src/domain/session/sessionStoreOperations.spec.ts +++ b/packages/core/src/domain/session/sessionStoreOperations.spec.ts @@ -1,6 +1,7 @@ import type { MockStorage } from '../../../test' import { mockClock, mockCookie, mockLocalStorage } from '../../../test' import type { CookieOptions } from '../../browser/cookie' +import type { Configuration } from '../configuration' import { initCookieStrategy } from './storeStrategies/sessionInCookie' import { initLocalStorageStrategy } from './storeStrategies/sessionInLocalStorage' import type { SessionState } from './sessionState' @@ -10,18 +11,18 @@ import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy' const cookieOptions: CookieOptions = {} const EXPIRED_SESSION: SessionState = { isExpired: '1', anonymousId: '0' } - +const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration ;( [ { title: 'Cookie Storage', - createSessionStoreStrategy: () => initCookieStrategy(cookieOptions), + createSessionStoreStrategy: () => initCookieStrategy(DEFAULT_INIT_CONFIGURATION, cookieOptions), mockStorage: mockCookie, storageKey: SESSION_STORE_KEY, }, { title: 'Local Storage', - createSessionStoreStrategy: () => initLocalStorageStrategy(), + createSessionStoreStrategy: () => initLocalStorageStrategy(DEFAULT_INIT_CONFIGURATION), mockStorage: mockLocalStorage, storageKey: SESSION_STORE_KEY, }, diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts index 39944ad6ba..b38332e3b1 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.spec.ts @@ -1,16 +1,20 @@ import { resetExperimentalFeatures } from '../../../tools/experimentalFeatures' +import { mockClock } from '../../../../test' import { setCookie, deleteCookie, getCookie, getCurrentSite } from '../../../browser/cookie' import { type SessionState } from '../sessionState' +import type { Configuration } from '../../configuration' +import { SESSION_TIME_OUT_DELAY } from '../sessionConstants' import { buildCookieOptions, selectCookieStrategy, initCookieStrategy } from './sessionInCookie' import type { SessionStoreStrategy } from './sessionStoreStrategy' import { SESSION_STORE_KEY } from './sessionStoreStrategy' +export const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration describe('session in cookie strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } let cookieStorageStrategy: SessionStoreStrategy beforeEach(() => { - cookieStorageStrategy = initCookieStrategy({}) + cookieStorageStrategy = initCookieStrategy(DEFAULT_INIT_CONFIGURATION, {}) }) afterEach(() => { @@ -24,7 +28,7 @@ describe('session in cookie strategy', () => { expect(getCookie(SESSION_STORE_KEY)).toBe('id=123&created=0') }) - it('should set `isExpired=1` to the cookie holding the session', () => { + it('should set `isExpired=1` and `aid` to the cookie holding the session', () => { spyOn(Math, 'random').and.callFake(() => 0) cookieStorageStrategy.persistSession(sessionState) cookieStorageStrategy.expireSession(sessionState) @@ -99,13 +103,13 @@ describe('session in cookie strategy', () => { }) }) }) -describe('session in cookie strategy with anonymous user tracking', () => { +describe('session in cookie strategy when opt-out anonymous user tracking', () => { const anonymousId = 'device-123' const sessionState: SessionState = { id: '123', created: '0' } let cookieStorageStrategy: SessionStoreStrategy beforeEach(() => { - cookieStorageStrategy = initCookieStrategy({}) + cookieStorageStrategy = initCookieStrategy({ trackAnonymousUser: false } as Configuration, {}) }) afterEach(() => { @@ -113,18 +117,19 @@ describe('session in cookie strategy with anonymous user tracking', () => { deleteCookie(SESSION_STORE_KEY) }) - it('should persist a session with anonymous id in a cookie', () => { - cookieStorageStrategy.persistSession({ ...sessionState, anonymousId }) - const session = cookieStorageStrategy.retrieveSession() - expect(session).toEqual({ ...sessionState, anonymousId }) - expect(getCookie(SESSION_STORE_KEY)).toBe(`id=123&created=0&aid=${anonymousId}`) + it('should not extend cookie expiration time when opt-out', () => { + const cookieSetSpy = spyOnProperty(document, 'cookie', 'set') + const clock = mockClock() + cookieStorageStrategy.expireSession({ ...sessionState, anonymousId }) + expect(cookieSetSpy.calls.argsFor(0)[0]).toContain(new Date(clock.timeStamp(SESSION_TIME_OUT_DELAY)).toUTCString()) + clock.cleanup() }) - it('should expire a session with anonymous id in a cookie', () => { + it('should not persist or expire a session with anonymous id when opt-out', () => { cookieStorageStrategy.persistSession({ ...sessionState, anonymousId }) cookieStorageStrategy.expireSession({ ...sessionState, anonymousId }) const session = cookieStorageStrategy.retrieveSession() - expect(session).toEqual({ isExpired: '1', anonymousId }) - expect(getCookie(SESSION_STORE_KEY)).toBe(`isExpired=1&aid=${anonymousId}`) + expect(session).toEqual({ isExpired: '1' }) + expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1') }) }) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts index 1904539bd2..0adac3cfa8 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInCookie.ts @@ -1,9 +1,9 @@ import { isChromium } from '../../../tools/utils/browserDetection' import type { CookieOptions } from '../../../browser/cookie' import { getCurrentSite, areCookiesAuthorized, getCookie, setCookie } from '../../../browser/cookie' -import type { InitConfiguration } from '../../configuration' +import type { InitConfiguration, Configuration } from '../../configuration' import { tryOldCookiesMigration } from '../oldCookiesMigration' -import { SESSION_COOKIE_EXPIRATION_DELAY, SESSION_EXPIRATION_DELAY } from '../sessionConstants' +import { SESSION_COOKIE_EXPIRATION_DELAY, SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from '../sessionConstants' import type { SessionState } from '../sessionState' import { toSessionString, toSessionState, getExpiredSessionState } from '../sessionState' import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' @@ -14,7 +14,7 @@ export function selectCookieStrategy(initConfiguration: InitConfiguration): Sess return areCookiesAuthorized(cookieOptions) ? { type: 'Cookie', cookieOptions } : undefined } -export function initCookieStrategy(cookieOptions: CookieOptions): SessionStoreStrategy { +export function initCookieStrategy(configuration: Configuration, cookieOptions: CookieOptions): SessionStoreStrategy { const cookieStore = { /** * Lock strategy allows mitigating issues due to concurrent access to cookie. @@ -23,7 +23,8 @@ export function initCookieStrategy(cookieOptions: CookieOptions): SessionStoreSt isLockEnabled: isChromium(), persistSession: persistSessionCookie(cookieOptions), retrieveSession: retrieveSessionCookie, - expireSession: (sessionState: SessionState) => expireSessionCookie(cookieOptions, sessionState), + expireSession: (sessionState: SessionState) => + expireSessionCookie(cookieOptions, sessionState, !!configuration.trackAnonymousUser), } tryOldCookiesMigration(cookieStore) @@ -37,9 +38,15 @@ function persistSessionCookie(options: CookieOptions) { } } -function expireSessionCookie(options: CookieOptions, sessionState: SessionState) { - const expiredSessionState = getExpiredSessionState(sessionState) - setCookie(SESSION_STORE_KEY, toSessionString(expiredSessionState), SESSION_COOKIE_EXPIRATION_DELAY, options) +function expireSessionCookie(options: CookieOptions, sessionState: SessionState, trackAnonymousUser: boolean) { + const expiredSessionState = getExpiredSessionState(sessionState, trackAnonymousUser) + // we do not extend cookie expiration date + setCookie( + SESSION_STORE_KEY, + toSessionString(expiredSessionState), + trackAnonymousUser ? SESSION_COOKIE_EXPIRATION_DELAY : SESSION_TIME_OUT_DELAY, + options + ) } function retrieveSessionCookie(): SessionState { diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts index 84dd13c3f1..53fb9425a5 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.spec.ts @@ -1,6 +1,8 @@ +import type { Configuration } from '../../configuration' import { type SessionState } from '../sessionState' import { selectLocalStorageStrategy, initLocalStorageStrategy } from './sessionInLocalStorage' import { SESSION_STORE_KEY } from './sessionStoreStrategy' +const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration describe('session in local storage strategy', () => { const sessionState: SessionState = { id: '123', created: '0' } @@ -24,7 +26,7 @@ describe('session in local storage strategy', () => { }) it('should persist a session in local storage', () => { - const localStorageStrategy = initLocalStorageStrategy() + const localStorageStrategy = initLocalStorageStrategy(DEFAULT_INIT_CONFIGURATION) localStorageStrategy.persistSession(sessionState) const session = localStorageStrategy.retrieveSession() expect(session).toEqual({ ...sessionState }) @@ -32,7 +34,7 @@ describe('session in local storage strategy', () => { }) it('should set `isExpired=1` to the local storage item holding the session', () => { - const localStorageStrategy = initLocalStorageStrategy() + const localStorageStrategy = initLocalStorageStrategy(DEFAULT_INIT_CONFIGURATION) localStorageStrategy.persistSession(sessionState) localStorageStrategy.expireSession(sessionState) const session = localStorageStrategy?.retrieveSession() @@ -42,7 +44,7 @@ describe('session in local storage strategy', () => { it('should not interfere with other keys present in local storage', () => { window.localStorage.setItem('test', 'hello') - const localStorageStrategy = initLocalStorageStrategy() + const localStorageStrategy = initLocalStorageStrategy(DEFAULT_INIT_CONFIGURATION) localStorageStrategy.persistSession(sessionState) localStorageStrategy.retrieveSession() localStorageStrategy.expireSession(sessionState) diff --git a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts index a32403c966..73e5d4b6c7 100644 --- a/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts +++ b/packages/core/src/domain/session/storeStrategies/sessionInLocalStorage.ts @@ -1,4 +1,5 @@ import { generateUUID } from '../../../tools/utils/stringUtils' +import type { Configuration } from '../../configuration' import type { SessionState } from '../sessionState' import { toSessionString, toSessionState, getExpiredSessionState } from '../sessionState' import type { SessionStoreStrategy, SessionStoreStrategyType } from './sessionStoreStrategy' @@ -19,12 +20,12 @@ export function selectLocalStorageStrategy(): SessionStoreStrategyType | undefin } } -export function initLocalStorageStrategy(): SessionStoreStrategy { +export function initLocalStorageStrategy(configuration: Configuration): SessionStoreStrategy { return { isLockEnabled: false, persistSession: persistInLocalStorage, retrieveSession: retrieveSessionFromLocalStorage, - expireSession: expireSessionFromLocalStorage, + expireSession: (sessionState: SessionState) => expireSessionFromLocalStorage(sessionState, configuration), } } @@ -37,6 +38,6 @@ function retrieveSessionFromLocalStorage(): SessionState { return toSessionState(sessionString) } -function expireSessionFromLocalStorage(previousSessionState: SessionState) { - persistInLocalStorage(getExpiredSessionState(previousSessionState)) +function expireSessionFromLocalStorage(previousSessionState: SessionState, configuration: Configuration) { + persistInLocalStorage(getExpiredSessionState(previousSessionState, !!configuration.trackAnonymousUser)) } diff --git a/packages/core/src/domain/telemetry/telemetryEvent.types.ts b/packages/core/src/domain/telemetry/telemetryEvent.types.ts index 8d5c0ba0f7..d2298c1701 100644 --- a/packages/core/src/domain/telemetry/telemetryEvent.types.ts +++ b/packages/core/src/domain/telemetry/telemetryEvent.types.ts @@ -383,6 +383,18 @@ export type TelemetryConfigurationEvent = CommonTelemetryProperties & { name: string [k: string]: unknown }[] + /** + * Whether the SDK is initialised on the application's main or a secondary process + */ + is_main_process?: boolean + /** + * The list of events that include feature flags collection + */ + collect_feature_flags_on?: ('view' | 'error' | 'vital')[] + /** + * Whether the anonymous users are tracked + */ + track_anonymous_user?: boolean [k: string]: unknown } [k: string]: unknown @@ -505,6 +517,10 @@ export interface CommonTelemetryProperties { id: string [k: string]: unknown } + /** + * The actual percentage of telemetry usage per event + */ + effective_sample_rate?: number /** * Enabled experimental features */ diff --git a/packages/core/test/coreConfiguration.ts b/packages/core/test/coreConfiguration.ts index ea46dd3aa7..0b2ff46a3e 100644 --- a/packages/core/test/coreConfiguration.ts +++ b/packages/core/test/coreConfiguration.ts @@ -26,6 +26,7 @@ export const EXHAUSTIVE_INIT_CONFIGURATION: Required = { version: 'version', usePartitionedCrossSiteSessionCookie: true, useSecureSessionCookie: true, + trackAnonymousUser: true, trackSessionAcrossSubdomains: true, enableExperimentalFeatures: ['foo'], replica: { @@ -52,6 +53,7 @@ export const SERIALIZED_EXHAUSTIVE_INIT_CONFIGURATION = { store_contexts_across_pages: true, allow_untrusted_events: true, tracking_consent: 'not-granted' as const, + track_anonymous_user: true, } /** diff --git a/packages/logs/src/boot/startLogs.spec.ts b/packages/logs/src/boot/startLogs.spec.ts index eb936cd0b9..792c384ea9 100644 --- a/packages/logs/src/boot/startLogs.spec.ts +++ b/packages/logs/src/boot/startLogs.spec.ts @@ -117,6 +117,9 @@ describe('logs', () => { url: 'common_url', }, origin: ErrorSource.LOGGER, + usr: { + anonymous_id: jasmine.any(String), + }, }) }) diff --git a/packages/logs/src/domain/assembly.ts b/packages/logs/src/domain/assembly.ts index c6ad7fbb1f..fd1ccef8b3 100644 --- a/packages/logs/src/domain/assembly.ts +++ b/packages/logs/src/domain/assembly.ts @@ -33,6 +33,9 @@ export function startLogsAssembly( } const commonContext = savedCommonContext || getCommonContext() + if (session && session.anonymousId && !commonContext.user.anonymous_id) { + commonContext.user.anonymous_id = session.anonymousId + } const log = combine( { service: configuration.service, diff --git a/packages/logs/src/domain/logsSessionManager.ts b/packages/logs/src/domain/logsSessionManager.ts index def6986bda..1489fdec3a 100644 --- a/packages/logs/src/domain/logsSessionManager.ts +++ b/packages/logs/src/domain/logsSessionManager.ts @@ -11,6 +11,7 @@ export interface LogsSessionManager { export type LogsSession = { id?: string // session can be tracked without id + anonymousId?: string // device id lasts across session } export const enum LoggerTrackingType { @@ -34,6 +35,7 @@ export function startLogsSessionManager( return session && session.trackingType === LoggerTrackingType.TRACKED ? { id: session.id, + anonymousId: session.anonymousId, } : undefined }, diff --git a/packages/rum-core/src/domain/assembly.spec.ts b/packages/rum-core/src/domain/assembly.spec.ts index a4f7e10f03..d3983a76f8 100644 --- a/packages/rum-core/src/domain/assembly.spec.ts +++ b/packages/rum-core/src/domain/assembly.spec.ts @@ -501,6 +501,18 @@ describe('rum assembly', () => { expect(serverRumEvents[0].usr).toEqual({ anonymous_id: 'device-123' }) }) + it('should not contain anonymous id when opt-out', () => { + const { lifeCycle, serverRumEvents, commonContext } = setupAssemblyTestWithDefaults({ + partialConfiguration: { trackAnonymousUser: false }, + }) + commonContext.user = {} + notifyRawRumEvent(lifeCycle, { + rawRumEvent: createRawRumEvent(RumEventType.VIEW), + }) + + expect(serverRumEvents[0].usr).toBeUndefined() + }) + it('should ignore the current user when a saved common context user is provided', () => { const { lifeCycle, serverRumEvents, commonContext } = setupAssemblyTestWithDefaults() commonContext.user = { replacedAttribute: 'b', addedAttribute: 'x' } diff --git a/packages/rum-core/src/domain/assembly.ts b/packages/rum-core/src/domain/assembly.ts index 2e9905d9f3..a1d5134468 100644 --- a/packages/rum-core/src/domain/assembly.ts +++ b/packages/rum-core/src/domain/assembly.ts @@ -189,7 +189,7 @@ export function startRumAssembly( session.sessionReplay === SessionReplayState.SAMPLED } - if (!commonContext.user.anonymous_id) { + if (session.anonymousId && !commonContext.user.anonymous_id && !!configuration.trackAnonymousUser) { commonContext.user.anonymous_id = session.anonymousId } if (!isEmptyObject(commonContext.user)) { diff --git a/packages/rum-core/src/domain/configuration/configuration.spec.ts b/packages/rum-core/src/domain/configuration/configuration.spec.ts index d024d1885f..52c635e28c 100644 --- a/packages/rum-core/src/domain/configuration/configuration.spec.ts +++ b/packages/rum-core/src/domain/configuration/configuration.spec.ts @@ -512,7 +512,6 @@ describe('serializeRumConfiguration', () => { : Key extends 'applicationId' | 'subdomain' | 'remoteConfigurationId' ? never : CamelToSnakeCase - // By specifying the type here, we can ensure that serializeConfiguration is returning an // object containing all expected properties. const serializedConfiguration: ExtractTelemetryConfiguration< diff --git a/packages/rum-core/src/rumEvent.types.ts b/packages/rum-core/src/rumEvent.types.ts index 4fa6cbdaf8..77840ff7d8 100644 --- a/packages/rum-core/src/rumEvent.types.ts +++ b/packages/rum-core/src/rumEvent.types.ts @@ -1333,6 +1333,10 @@ export interface CommonProperties { * Email of the user */ readonly email?: string + /** + * Identifier of the user across sessions + */ + readonly anonymous_id?: string [k: string]: unknown } /** diff --git a/packages/rum-core/test/mockRumConfiguration.ts b/packages/rum-core/test/mockRumConfiguration.ts index dadfdb4f70..ef1e6e00c8 100644 --- a/packages/rum-core/test/mockRumConfiguration.ts +++ b/packages/rum-core/test/mockRumConfiguration.ts @@ -10,9 +10,9 @@ export function mockRumConfiguration(partialConfig: Partial = applicationId: FAKE_APP_ID, trackResources: true, trackLongTasks: true, + trackAnonymousUser: true, })!, ...SPEC_ENDPOINTS, } - return { ...baseConfig, ...partialConfig } }