Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💥 [RUM-7704] Remove anonymous user feature flag for v6 #3216

Merged
merged 4 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/core/src/domain/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/domain/session/oldCookiesMigration.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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()
})

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/domain/session/sessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('startSessionManager', () => {

function expectSessionIdToNotBeDefined(sessionManager: SessionManager<FakeTrackingType>) {
expect(sessionManager.findSession()!.id).toBeUndefined()
expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=')
expect(getCookie(SESSION_STORE_KEY)).not.toMatch(/\bid=/)
}

function expectTrackingTypeToBe(
Expand Down Expand Up @@ -592,6 +592,7 @@ describe('startSessionManager', () => {

describe('tracking consent', () => {
it('expires the session when tracking consent is withdrawn', () => {
spyOn(Math, 'random').and.callFake(() => 0) // mock random for anonymous uuid generation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏I don't think the anonymous id value is tested in this test. Maybe remove this line?

const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED)
const sessionManager = startSessionManagerWithDefaults({ trackingConsentState })

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ export function startSessionManager<TrackingType extends string>(
const expireObservable = new Observable<void>()

// 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<SessionContext<TrackingType>>({
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/domain/session/sessionState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ExperimentalFeature, isExperimentalFeatureEnabled } from '../../tools/experimentalFeatures'
import { isEmptyObject } from '../../tools/utils/objectUtils'
import { objectEntries } from '../../tools/utils/polyfills'
import { dateNow } from '../../tools/utils/timeUtils'
import { generateAnonymousId } from '../user'
import type { Configuration } from '../configuration'
import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants'
import { isValidSessionString, SESSION_ENTRY_REGEXP, SESSION_ENTRY_SEPARATOR } from './sessionStateValidation'
export const EXPIRED = '1'
Expand All @@ -16,11 +16,14 @@ export interface SessionState {
[key: string]: string | undefined
}

export function getExpiredSessionState(previousSessionState: SessionState | undefined): SessionState {
export function getExpiredSessionState(
previousSessionState: SessionState | undefined,
configuration: Configuration
): SessionState {
const expiredSessionState: SessionState = {
isExpired: EXPIRED,
}
if (isExperimentalFeatureEnabled(ExperimentalFeature.ANONYMOUS_USER_TRACKING)) {
if (configuration.trackAnonymousUser) {
if (previousSessionState?.anonymousId) {
expiredSessionState.anonymousId = previousSessionState?.anonymousId
} else {
Expand Down
25 changes: 19 additions & 6 deletions packages/core/src/domain/session/sessionStore.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -16,8 +17,8 @@ const DURATION = 123456
const PRODUCT_KEY = 'product'
const FIRST_ID = 'first'
const SECOND_ID = 'second'

const EXPIRED_SESSION: SessionState = { isExpired: '1' }
const EXPIRED_SESSION: SessionState = { isExpired: '1', anonymousId: '0' }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion:

Suggested change
const EXPIRED_SESSION: SessionState = { isExpired: '1', anonymousId: '0' }
const EXPIRED_SESSION: SessionState = { isExpired: '1', anonymousId: jasmine.any(String) as string }

const DEFAULT_INIT_CONFIGURATION = { trackAnonymousUser: true } as Configuration

function setSessionInStore(trackingType: FakeTrackingType = FakeTrackingType.TRACKED, id?: string, expire?: number) {
setCookie(
Expand All @@ -36,14 +37,14 @@ function expectTrackedSessionToBeInStore(id?: string) {
}

function expectNotTrackedSessionToBeInStore() {
expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=')
expect(getCookie(SESSION_STORE_KEY)).not.toContain('&id=')
expect(getCookie(SESSION_STORE_KEY)).not.toContain('isExpired=1')
expect(getCookie(SESSION_STORE_KEY)).toContain(`${PRODUCT_KEY}=${FakeTrackingType.NOT_TRACKED}`)
}

function expectSessionToBeExpiredInStore() {
expect(getCookie(SESSION_STORE_KEY)).toContain('isExpired=1')
expect(getCookie(SESSION_STORE_KEY)).not.toContain('id=')
expect(getCookie(SESSION_STORE_KEY)).not.toContain('&id=')
expect(getCookie(SESSION_STORE_KEY)).not.toContain(`${PRODUCT_KEY}=`)
}

Expand Down Expand Up @@ -117,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)
}
Expand All @@ -136,6 +142,7 @@ describe('session store', () => {

describe('initialize session', () => {
it('when session not in store, should initialize a new session', () => {
spyOn(Math, 'random').and.callFake(() => 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏remove this line, use jasmine.any(String)

setupSessionStore()

expect(sessionStoreManager.getSession()).toEqual(EXPIRED_SESSION)
Expand Down Expand Up @@ -454,6 +461,7 @@ describe('session store', () => {

describe('reinitialize session', () => {
it('when session not in store, should reinitialize the store', () => {
spyOn(Math, 'random').and.callFake(() => 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏remove this line, use jasmine.any(String)

setupSessionStore()

sessionStoreManager.restartSession()
Expand Down Expand Up @@ -491,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
Expand Down
17 changes: 9 additions & 8 deletions packages/core/src/domain/session/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -58,6 +58,7 @@ export function selectSessionStoreStrategyType(
*/
export function startSessionStore<TrackingType extends string>(
sessionStoreStrategyType: SessionStoreStrategyType,
configuration: Configuration,
productKey: string,
computeSessionState: (rawTrackingType?: string) => { trackingType: TrackingType; isTracked: boolean }
): SessionStore {
Expand All @@ -67,8 +68,8 @@ export function startSessionStore<TrackingType extends string>(

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)
Expand Down Expand Up @@ -117,7 +118,7 @@ export function startSessionStore<TrackingType extends string>(
processSessionStoreOperations(
{
process: (sessionState) =>
isSessionInExpiredState(sessionState) ? getExpiredSessionState(sessionState) : undefined,
isSessionInExpiredState(sessionState) ? getExpiredSessionState(sessionState, configuration) : undefined,
after: synchronizeSession,
},
sessionStoreStrategy
Expand All @@ -126,7 +127,7 @@ export function startSessionStore<TrackingType extends string>(

function synchronizeSession(sessionState: SessionState) {
if (isSessionInExpiredState(sessionState)) {
sessionState = getExpiredSessionState(sessionState)
sessionState = getExpiredSessionState(sessionState, configuration)
}
if (hasSessionInCache()) {
if (isSessionInCacheOutdated(sessionState)) {
Expand All @@ -144,7 +145,7 @@ export function startSessionStore<TrackingType extends string>(
{
process: (sessionState) => {
if (isSessionInNotStartedState(sessionState)) {
return getExpiredSessionState(sessionState)
return getExpiredSessionState(sessionState, configuration)
}
},
after: (sessionState) => {
Expand Down Expand Up @@ -178,7 +179,7 @@ export function startSessionStore<TrackingType extends string>(
}

function expireSessionInCache() {
sessionCache = getExpiredSessionState(sessionCache)
sessionCache = getExpiredSessionState(sessionCache, configuration)
expireObservable.notify()
}

Expand Down Expand Up @@ -208,7 +209,7 @@ export function startSessionStore<TrackingType extends string>(
expire: () => {
cancelExpandOrRenewSession()
expireSession(sessionCache)
synchronizeSession(getExpiredSessionState(sessionCache))
synchronizeSession(getExpiredSessionState(sessionCache, configuration))
},
stop: () => {
clearInterval(watchSessionTimeoutId)
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/domain/session/sessionStoreOperations.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -9,19 +10,19 @@ import { processSessionStoreOperations, LOCK_MAX_TRIES, LOCK_RETRY_DELAY } from
import { SESSION_STORE_KEY } from './storeStrategies/sessionStoreStrategy'

const cookieOptions: CookieOptions = {}
const EXPIRED_SESSION: SessionState = { isExpired: '1' }

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,
},
Expand All @@ -37,6 +38,7 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
const now = Date.now()

beforeEach(() => {
spyOn(Math, 'random').and.callFake(() => 0)
sessionStoreStrategy.expireSession(initialSession)
initialSession = { id: '123', created: String(now) }
otherSession = { id: '456', created: String(now + 100) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ExperimentalFeature, resetExperimentalFeatures } from '../../../tools/experimentalFeatures'
import { mockExperimentalFeatures } from '../../../../test'
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(() => {
Expand All @@ -25,12 +28,13 @@ 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)
const session = cookieStorageStrategy.retrieveSession()
expect(session).toEqual({ isExpired: '1' })
expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1')
expect(session).toEqual({ isExpired: '1', anonymousId: '0' })
expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1&aid=0')
})

it('should return an empty object if session string is invalid', () => {
Expand Down Expand Up @@ -99,33 +103,33 @@ 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(() => {
mockExperimentalFeatures([ExperimentalFeature.ANONYMOUS_USER_TRACKING])
cookieStorageStrategy = initCookieStrategy({})
cookieStorageStrategy = initCookieStrategy({ trackAnonymousUser: false } as Configuration, {})
})

afterEach(() => {
resetExperimentalFeatures()
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')
})
})
Loading