Skip to content

Commit

Permalink
Test anonymous id on staging behind ff
Browse files Browse the repository at this point in the history
  • Loading branch information
cy-moi committed Dec 10, 2024
1 parent 50b1073 commit bd99736
Show file tree
Hide file tree
Showing 22 changed files with 186 additions and 95 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/browser/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface CookieOptions {
domain?: string
}

export function setCookie(name: string, value: string, expireDelay: number, options?: CookieOptions) {
export function setCookie(name: string, value: string, expireDelay: number = 0, options?: CookieOptions) {
const date = new Date()
date.setTime(date.getTime() + expireDelay)
const expires = `expires=${date.toUTCString()}`
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/domain/session/sessionConstants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ONE_HOUR, ONE_MINUTE } from '../../tools/utils/timeUtils'
import { ONE_HOUR, ONE_MINUTE, ONE_YEAR } from '../../tools/utils/timeUtils'

export const SESSION_TIME_OUT_DELAY = 4 * ONE_HOUR
export const SESSION_EXPIRATION_DELAY = 15 * ONE_MINUTE
export const SESSION_COOKIE_EXPIRATION_DELAY = ONE_YEAR
2 changes: 2 additions & 0 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface SessionContext<TrackingType extends string> extends Context {
id: string
trackingType: TrackingType
isReplayForced: boolean
anonymousId: string | undefined
}

export const VISIBILITY_CHECK_DELAY = ONE_MINUTE
Expand Down Expand Up @@ -86,6 +87,7 @@ export function startSessionManager<TrackingType extends string>(
id: sessionStore.getSession().id!,
trackingType: sessionStore.getSession()[productKey] as TrackingType,
isReplayForced: !!sessionStore.getSession().forcedReplay,
anonymousId: sessionStore.getSession().anonymousId,
}
}

Expand Down
42 changes: 25 additions & 17 deletions packages/core/src/domain/session/sessionState.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
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 { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_DELAY } from './sessionConstants'

const SESSION_ENTRY_REGEXP = /^([a-zA-Z]+)=([a-z0-9-]+)$/
const SESSION_ENTRY_SEPARATOR = '&'

import { isValidSessionString, SESSION_ENTRY_REGEXP, SESSION_ENTRY_SEPARATOR } from './sessionStateValidation'
export const EXPIRED = '1'

export interface SessionState {
Expand All @@ -17,10 +16,18 @@ export interface SessionState {
[key: string]: string | undefined
}

export function getExpiredSessionState(): SessionState {
return {
export function getExpiredSessionState(previousSessionState: SessionState | undefined): SessionState {
const expiredSessionState: SessionState = {
isExpired: EXPIRED,
}
if (isExperimentalFeatureEnabled(ExperimentalFeature.ANONYMOUS_USER_TRACKING)) {
if (previousSessionState?.anonymousId) {
expiredSessionState.anonymousId = previousSessionState?.anonymousId
} else {
expiredSessionState.anonymousId = generateAnonymousId()
}
}
return expiredSessionState
}

export function isSessionInNotStartedState(session: SessionState) {
Expand Down Expand Up @@ -50,9 +57,12 @@ export function expandSessionState(session: SessionState) {
}

export function toSessionString(session: SessionState) {
return objectEntries(session)
.map(([key, value]) => `${key}=${value}`)
.join(SESSION_ENTRY_SEPARATOR)
return (
objectEntries(session)
// we use `aid` as a key for anonymousId
.map(([key, value]) => (key === 'anonymousId' ? `aid=${value}` : `${key}=${value}`))
.join(SESSION_ENTRY_SEPARATOR)
)
}

export function toSessionState(sessionString: string | undefined | null) {
Expand All @@ -62,16 +72,14 @@ export function toSessionState(sessionString: string | undefined | null) {
const matches = SESSION_ENTRY_REGEXP.exec(entry)
if (matches !== null) {
const [, key, value] = matches
session[key] = value
if (key === 'aid') {
// we use `aid` as a key for anonymousId
session.anonymousId = value
} else {
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))
)
}
9 changes: 9 additions & 0 deletions packages/core/src/domain/session/sessionStateValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const SESSION_ENTRY_REGEXP = /^([a-zA-Z]+)=([a-z0-9-]+)$/
export const SESSION_ENTRY_SEPARATOR = '&'

export function isValidSessionString(sessionString: string | undefined | null): sessionString is string {
return (
!!sessionString &&
(sessionString.indexOf(SESSION_ENTRY_SEPARATOR) !== -1 || SESSION_ENTRY_REGEXP.test(sessionString))
)
}
16 changes: 8 additions & 8 deletions packages/core/src/domain/session/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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 { assign } from '../../tools/utils/polyfills'
import { selectCookieStrategy, initCookieStrategy } from './storeStrategies/sessionInCookie'
import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStrategy'
import {
Expand Down Expand Up @@ -117,7 +116,8 @@ export function startSessionStore<TrackingType extends string>(
function watchSession() {
processSessionStoreOperations(
{
process: (sessionState) => (isSessionInExpiredState(sessionState) ? getExpiredSessionState() : undefined),
process: (sessionState) =>
isSessionInExpiredState(sessionState) ? getExpiredSessionState(sessionState) : undefined,
after: synchronizeSession,
},
sessionStoreStrategy
Expand All @@ -126,7 +126,7 @@ export function startSessionStore<TrackingType extends string>(

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

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

Expand All @@ -190,7 +190,7 @@ export function startSessionStore<TrackingType extends string>(
function updateSessionState(partialSessionState: Partial<SessionState>) {
processSessionStoreOperations(
{
process: (sessionState) => assign({}, sessionState, partialSessionState),
process: (sessionState) => ({ ...sessionState, ...partialSessionState }),
after: synchronizeSession,
},
sessionStoreStrategy
Expand All @@ -207,8 +207,8 @@ export function startSessionStore<TrackingType extends string>(
restartSession: startSession,
expire: () => {
cancelExpandOrRenewSession()
expireSession()
synchronizeSession(getExpiredSessionState())
expireSession(sessionCache)
synchronizeSession(getExpiredSessionState(sessionCache))
},
stop: () => {
clearInterval(watchSessionTimeoutId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const EXPIRED_SESSION: SessionState = { isExpired: '1' }
const now = Date.now()

beforeEach(() => {
sessionStoreStrategy.expireSession()
sessionStoreStrategy.expireSession(initialSession)
initialSession = { id: '123', created: String(now) }
otherSession = { id: '456', created: String(now + 100) }
processSpy = jasmine.createSpy('process')
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/domain/session/sessionStoreOperations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { setTimeout } from '../../tools/timer'
import { generateUUID } from '../../tools/utils/stringUtils'
import { assign } from '../../tools/utils/polyfills'
import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy'
import type { SessionState } from './sessionState'
import { expandSessionState, isSessionInExpiredState } from './sessionState'
Expand All @@ -21,7 +20,7 @@ export function processSessionStoreOperations(
numberOfRetries = 0
) {
const { isLockEnabled, persistSession, expireSession } = sessionStoreStrategy
const persistWithLock = (session: SessionState) => persistSession(assign({}, session, { lock: currentLock }))
const persistWithLock = (session: SessionState) => persistSession({ ...session, lock: currentLock })
const retrieveStore = () => {
const session = sessionStoreStrategy.retrieveSession()
const lock = session.lock
Expand Down Expand Up @@ -76,7 +75,7 @@ export function processSessionStoreOperations(
}
if (processedSession) {
if (isSessionInExpiredState(processedSession)) {
expireSession()
expireSession(processedSession)
} else {
expandSessionState(processedSession)
isLockEnabled ? persistWithLock(processedSession) : persistSession(processedSession)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExperimentalFeature, resetExperimentalFeatures } from '../../../tools/experimentalFeatures'
import { mockExperimentalFeatures } from '../../../../test'
import { setCookie, deleteCookie, getCookie, getCurrentSite } from '../../../browser/cookie'
import { type SessionState } from '../sessionState'
import { buildCookieOptions, selectCookieStrategy, initCookieStrategy } from './sessionInCookie'
Expand Down Expand Up @@ -25,7 +27,7 @@ describe('session in cookie strategy', () => {

it('should set `isExpired=1` to the cookie holding the session', () => {
cookieStorageStrategy.persistSession(sessionState)
cookieStorageStrategy.expireSession()
cookieStorageStrategy.expireSession(sessionState)
const session = cookieStorageStrategy.retrieveSession()
expect(session).toEqual({ isExpired: '1' })
expect(getCookie(SESSION_STORE_KEY)).toBe('isExpired=1')
Expand Down Expand Up @@ -108,3 +110,33 @@ describe('session in cookie strategy', () => {
})
})
})
describe('session in cookie strategy with 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({})
})

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 expire a session with anonymous id in a cookie', () => {
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}`)
})
})
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { isChromium } from '../../../tools/utils/browserDetection'
import { ExperimentalFeature, isExperimentalFeatureEnabled } from '../../../tools/experimentalFeatures'
import type { CookieOptions } from '../../../browser/cookie'
import { getCurrentSite, areCookiesAuthorized, getCookie, setCookie } from '../../../browser/cookie'
import type { InitConfiguration } from '../../configuration'
import { tryOldCookiesMigration } from '../oldCookiesMigration'
import { SESSION_EXPIRATION_DELAY, SESSION_TIME_OUT_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 @@ -23,7 +24,7 @@ export function initCookieStrategy(cookieOptions: CookieOptions): SessionStoreSt
isLockEnabled: isChromium(),
persistSession: persistSessionCookie(cookieOptions),
retrieveSession: retrieveSessionCookie,
expireSession: () => expireSessionCookie(cookieOptions),
expireSession: (sessionState: SessionState) => expireSessionCookie(cookieOptions, sessionState),
}

tryOldCookiesMigration(cookieStore)
Expand All @@ -37,13 +38,22 @@ function persistSessionCookie(options: CookieOptions) {
}
}

function expireSessionCookie(options: CookieOptions) {
setCookie(SESSION_STORE_KEY, toSessionString(getExpiredSessionState()), SESSION_TIME_OUT_DELAY, options)
function expireSessionCookie(options: CookieOptions, sessionState: SessionState) {
const expiredSessionState = getExpiredSessionState(sessionState)
setCookie(
SESSION_STORE_KEY,
toSessionString(expiredSessionState),
isExperimentalFeatureEnabled(ExperimentalFeature.ANONYMOUS_USER_TRACKING)
? SESSION_COOKIE_EXPIRATION_DELAY
: SESSION_TIME_OUT_DELAY,
options
)
}

function retrieveSessionCookie(): SessionState {
const sessionString = getCookie(SESSION_STORE_KEY)
return toSessionState(sessionString)
const sessionState = toSessionState(sessionString)
return sessionState
}

export function buildCookieOptions(initConfiguration: InitConfiguration) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { mockExperimentalFeatures } from '../../../../test'
import { ExperimentalFeature } from '../../../tools/experimentalFeatures'
import { type SessionState } from '../sessionState'
import { selectLocalStorageStrategy, initLocalStorageStrategy } from './sessionInLocalStorage'
import { SESSION_STORE_KEY } from './sessionStoreStrategy'

describe('session in local storage strategy', () => {
const sessionState: SessionState = { id: '123', created: '0' }
beforeEach(() => {
mockExperimentalFeatures([ExperimentalFeature.ANONYMOUS_USER_TRACKING])
spyOn(Math, 'random').and.returnValue(1)
})

afterEach(() => {
window.localStorage.clear()
Expand Down Expand Up @@ -31,25 +37,18 @@ describe('session in local storage strategy', () => {
it('should set `isExpired=1` to the local storage item holding the session', () => {
const localStorageStrategy = initLocalStorageStrategy()
localStorageStrategy.persistSession(sessionState)
localStorageStrategy.expireSession()
localStorageStrategy.expireSession(sessionState)
const session = localStorageStrategy?.retrieveSession()
expect(session).toEqual({ isExpired: '1' })
expect(window.localStorage.getItem(SESSION_STORE_KEY)).toBe('isExpired=1')
expect(session).toEqual({ isExpired: '1', anonymousId: '2gosa7pa2gw' })
expect(window.localStorage.getItem(SESSION_STORE_KEY)).toBe('isExpired=1&aid=2gosa7pa2gw')
})

it('should not interfere with other keys present in local storage', () => {
window.localStorage.setItem('test', 'hello')
const localStorageStrategy = initLocalStorageStrategy()
localStorageStrategy.persistSession(sessionState)
localStorageStrategy.retrieveSession()
localStorageStrategy.expireSession()
localStorageStrategy.expireSession(sessionState)
expect(window.localStorage.getItem('test')).toEqual('hello')
})

it('should return an empty object if session string is invalid', () => {
const localStorageStrategy = initLocalStorageStrategy()
localStorage.setItem(SESSION_STORE_KEY, '{test:42}')
const session = localStorageStrategy?.retrieveSession()
expect(session).toEqual({})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ function retrieveSessionFromLocalStorage(): SessionState {
return toSessionState(sessionString)
}

function expireSessionFromLocalStorage() {
persistInLocalStorage(getExpiredSessionState())
function expireSessionFromLocalStorage(previousSessionState: SessionState) {
persistInLocalStorage(getExpiredSessionState(previousSessionState))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export interface SessionStoreStrategy {
isLockEnabled: boolean
persistSession: (session: SessionState) => void
retrieveSession: () => SessionState
expireSession: () => void
expireSession: (previousSessionState: SessionState) => void
}
8 changes: 7 additions & 1 deletion packages/core/src/domain/user/user.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { display } from '../../tools/display'
import { checkUser, sanitizeUser } from './user'
import { checkUser, generateAnonymousId, sanitizeUser } from './user'
import type { User } from './user.types'

describe('sanitize user function', () => {
Expand Down Expand Up @@ -37,3 +37,9 @@ describe('check user function', () => {
expect(display.error).toHaveBeenCalledTimes(3)
})
})

describe('check anonymous id storage functions', () => {
it('should generate a random anonymous id', () => {
expect(generateAnonymousId()).toMatch(/^[a-z0-9]+$/)
})
})
Loading

0 comments on commit bd99736

Please sign in to comment.