Skip to content

Commit

Permalink
Add opt-out configuration parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
cy-moi committed Dec 23, 2024
1 parent c5d3f2a commit 73db602
Show file tree
Hide file tree
Showing 22 changed files with 142 additions and 51 deletions.
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
2 changes: 1 addition & 1 deletion packages/core/src/domain/session/sessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
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
15 changes: 10 additions & 5 deletions packages/core/src/domain/session/sessionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
16 changes: 14 additions & 2 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 @@ -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(
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
19 changes: 11 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,9 @@ export function startSessionStore<TrackingType extends string>(
processSessionStoreOperations(
{
process: (sessionState) =>
isSessionInExpiredState(sessionState) ? getExpiredSessionState(sessionState) : undefined,
isSessionInExpiredState(sessionState)
? getExpiredSessionState(sessionState, !!configuration.trackAnonymousUser)
: undefined,
after: synchronizeSession,
},
sessionStoreStrategy
Expand All @@ -126,7 +129,7 @@ export function startSessionStore<TrackingType extends string>(

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

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

Expand Down Expand Up @@ -208,7 +211,7 @@ export function startSessionStore<TrackingType extends string>(
expire: () => {
cancelExpandOrRenewSession()
expireSession(sessionCache)
synchronizeSession(getExpiredSessionState(sessionCache))
synchronizeSession(getExpiredSessionState(sessionCache, !!configuration.trackAnonymousUser))
},
stop: () => {
clearInterval(watchSessionTimeoutId)
Expand Down
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 @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -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)
Expand Down Expand Up @@ -99,32 +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(() => {
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')
})
})
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 73db602

Please sign in to comment.