Skip to content
Open
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
125 changes: 124 additions & 1 deletion packages/core/src/domain/session/sessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,11 +665,13 @@ describe('startSessionManager', () => {
productKey = FIRST_PRODUCT_KEY,
computeTrackingType = () => FakeTrackingType.TRACKED,
trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED),
applicationId,
}: {
configuration?: Partial<Configuration>
productKey?: string
computeTrackingType?: () => FakeTrackingType
trackingConsentState?: TrackingConsentState
applicationId?: string
} = {}) {
return startSessionManager(
{
Expand All @@ -678,7 +680,128 @@ describe('startSessionManager', () => {
} as Configuration,
productKey,
computeTrackingType,
trackingConsentState
trackingConsentState,
applicationId
)
}

describe('session ID generation with applicationId', () => {
it('should generate deterministic session ID when applicationId is provided', () => {
const applicationId = 'test-app-123'
const sessionManager = startSessionManagerWithDefaults({
applicationId,
configuration: { trackAnonymousUser: true } as Partial<Configuration>,
})

const session1 = sessionManager.findSession()
expect(session1).toBeDefined()
expect(session1!.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/)

const sessionId1 = session1!.id
const anonymousId = session1!.anonymousId

expect(anonymousId).toBeDefined()

// Expire session
sessionManager.expire()

// Manually restore anonymousId (within same time window)
setCookie(SESSION_STORE_KEY, `aid=${anonymousId}&first=tracked`, DURATION)

// Create new session manager with same applicationId (without advancing time)
stopSessionManager()
const sessionManager2 = startSessionManagerWithDefaults({
applicationId,
configuration: { trackAnonymousUser: true } as Partial<Configuration>,
})

const session2 = sessionManager2.findSession()
const sessionId2 = session2!.id

// Session IDs should be deterministic (same anonymousId + applicationId + time window)
expect(sessionId2).toBe(sessionId1)
expect(session2!.anonymousId).toBe(anonymousId)
})

it('should generate random session ID when applicationId is not provided', () => {
const sessionManager = startSessionManagerWithDefaults()

const session1 = sessionManager.findSession()
const sessionId1 = session1!.id
expect(sessionId1).toBeDefined()

// Expire current session
sessionManager.expire()

// Create new session by restoring state and triggering renewal
setCookie(SESSION_STORE_KEY, 'first=tracked', DURATION)
stopSessionManager()
const sessionManager2 = startSessionManagerWithDefaults()

const session2 = sessionManager2.findSession()
const sessionId2 = session2!.id

// Session IDs should be different (random generation)
expect(sessionId2).toBeDefined()
expect(sessionId2).not.toBe(sessionId1)
})

it('should generate different session IDs for different applicationIds', () => {
const sessionManager1 = startSessionManagerWithDefaults({
applicationId: 'app-1',
configuration: { trackAnonymousUser: true } as Partial<Configuration>,
})
const session1 = sessionManager1.findSession()
const sessionId1 = session1!.id
const anonymousId = session1!.anonymousId

expect(anonymousId).toBeDefined()
expect(anonymousId).not.toBe('undefined')

sessionManager1.expire()
stopSessionManager()

// Create new session with same anonymousId but different applicationId
setCookie(SESSION_STORE_KEY, `aid=${anonymousId}&first=tracked`, DURATION)

const sessionManager2 = startSessionManagerWithDefaults({
applicationId: 'app-2',
configuration: { trackAnonymousUser: true } as Partial<Configuration>,
})
const session2 = sessionManager2.findSession()
const sessionId2 = session2!.id

// Different applicationIds should produce different session IDs (even with same anonymousId)
expect(sessionId2).toBeDefined()
expect(sessionId2).not.toBe(sessionId1)
expect(session2!.anonymousId).toBe(anonymousId)
})

it('should preserve anonymousId across session renewals', () => {
const applicationId = 'test-app-123'
const sessionManager = startSessionManagerWithDefaults({
applicationId,
configuration: { trackAnonymousUser: true } as Partial<Configuration>,
})

const session1 = sessionManager.findSession()
const anonymousId1 = session1!.anonymousId
expect(anonymousId1).toBeDefined()

// Expire and create new session
sessionManager.expire()
setCookie(SESSION_STORE_KEY, `aid=${anonymousId1}&first=tracked`, DURATION)
stopSessionManager()

const sessionManager2 = startSessionManagerWithDefaults({
applicationId,
configuration: { trackAnonymousUser: true } as Partial<Configuration>,
})
const session2 = sessionManager2.findSession()
const anonymousId2 = session2!.anonymousId

// anonymousId should be preserved
expect(anonymousId2).toBe(anonymousId1)
})
})
})
7 changes: 4 additions & 3 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,18 @@ export function startSessionManager<TrackingType extends string>(
configuration: Configuration,
productKey: string,
computeTrackingType: (rawTrackingType?: string) => TrackingType,
trackingConsentState: TrackingConsentState
trackingConsentState: TrackingConsentState,
applicationId?: string
): SessionManager<TrackingType> {
const renewObservable = new Observable<void>()
const expireObservable = new Observable<void>()

// TODO - Improve configuration type and remove assertion
const sessionStore = startSessionStore(
configuration.sessionStoreStrategyType!,
configuration,
productKey,
computeTrackingType
computeTrackingType,
applicationId
)
stopCallbacks.push(() => sessionStore.stop())

Expand Down
114 changes: 110 additions & 4 deletions packages/core/src/domain/session/sessionStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe('session store', () => {

function setupSessionStore(
initialState: SessionState = {},
applicationId?: string,
computeTrackingType: (rawTrackingType?: string) => FakeTrackingType = () => FakeTrackingType.TRACKED
) {
const sessionStoreStrategyType = selectSessionStoreStrategyType(DEFAULT_INIT_CONFIGURATION)
Expand All @@ -209,6 +210,7 @@ describe('session store', () => {
DEFAULT_CONFIGURATION,
PRODUCT_KEY,
computeTrackingType,
applicationId,
sessionStoreStrategy
)
sessionStoreStrategy.persistSession.calls.reset()
Expand Down Expand Up @@ -256,6 +258,101 @@ describe('session store', () => {
})
})

describe('session ID generation with applicationId', () => {
it('should generate deterministic session ID when applicationId is provided', () => {
const applicationId = 'test-app-123'
setupSessionStore(undefined, applicationId)

// First expand creates both anonymousId and session ID
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)

const anonymousId = sessionStoreManager.getSession().anonymousId
const sessionId1 = sessionStoreManager.getSession().id

expect(anonymousId).toBeDefined()
expect(sessionId1).toBeDefined()
expect(sessionId1).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/)

// Reset and recreate session with same anonymousId and applicationId
sessionStoreManager.expire()
clock.tick(STORAGE_POLL_DELAY)

// Manually set the same anonymousId
setSessionInStore({ anonymousId })
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)

const sessionId2 = sessionStoreManager.getSession().id

// Session IDs should be deterministic (same for same inputs)
expect(sessionId2).toBe(sessionId1)
})

it('should generate random session ID when applicationId is not provided', () => {
setupSessionStore(undefined, undefined)

sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)

const sessionId1 = sessionStoreManager.getSession().id
expect(sessionId1).toBeDefined()

// Expire and recreate
sessionStoreManager.expire()
clock.tick(STORAGE_POLL_DELAY)
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)

const sessionId2 = sessionStoreManager.getSession().id

// Session IDs should be random (different)
expect(sessionId2).not.toBe(sessionId1)
})

it('should generate different session IDs for different applicationIds', () => {
const anonymousId = 'device-123-456'

// First session with app1
setupSessionStore({ anonymousId, [PRODUCT_KEY]: FakeTrackingType.TRACKED }, 'app-1')
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)
const sessionId1 = sessionStoreManager.getSession().id

// Reset and create session with app2
resetSessionInStore()
setupSessionStore({ anonymousId, [PRODUCT_KEY]: FakeTrackingType.TRACKED }, 'app-2')
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)
const sessionId2 = sessionStoreManager.getSession().id

// Different applicationIds should produce different session IDs
expect(sessionId1).not.toBe(sessionId2)
})

it('should use anonymousId as deviceId for deterministic generation', () => {
const applicationId = 'test-app-123'
setupSessionStore(undefined, applicationId)

// Create initial session with anonymousId
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)

const anonymousId = sessionStoreManager.getSession().anonymousId
expect(anonymousId).toBeDefined()

// Create session ID
sessionStoreManager.expandOrRenewSession()
clock.tick(STORAGE_POLL_DELAY)

const sessionId = sessionStoreManager.getSession().id
expect(sessionId).toBeDefined()

// Session ID should be deterministic based on anonymousId and applicationId
expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/)
})
})

describe('expand or renew session', () => {
it(
'when session not in cache, session not in store and new session tracked, ' +
Expand All @@ -276,7 +373,7 @@ describe('session store', () => {
'when session not in cache, session not in store and new session not tracked, ' +
'should store not tracked session and trigger renew session',
() => {
setupSessionStore(EMPTY_SESSION_STATE, () => FakeTrackingType.NOT_TRACKED)
setupSessionStore(EMPTY_SESSION_STATE, undefined, () => FakeTrackingType.NOT_TRACKED)

sessionStoreManager.expandOrRenewSession()

Expand Down Expand Up @@ -321,7 +418,11 @@ describe('session store', () => {
'when session in cache, session not in store and new session not tracked, ' +
'should expire session, store not tracked session and trigger renew session',
() => {
setupSessionStore(createSessionState(FakeTrackingType.TRACKED, FIRST_ID), () => FakeTrackingType.NOT_TRACKED)
setupSessionStore(
createSessionState(FakeTrackingType.TRACKED, FIRST_ID),
undefined,
() => FakeTrackingType.NOT_TRACKED
)
resetSessionInStore()

sessionStoreManager.expandOrRenewSession()
Expand All @@ -338,7 +439,11 @@ describe('session store', () => {
'when session not tracked in cache, session not in store and new session not tracked, ' +
'should expire session, store not tracked session and trigger renew session',
() => {
setupSessionStore(createSessionState(FakeTrackingType.NOT_TRACKED), () => FakeTrackingType.NOT_TRACKED)
setupSessionStore(
createSessionState(FakeTrackingType.NOT_TRACKED),
undefined,
() => FakeTrackingType.NOT_TRACKED
)
resetSessionInStore()

sessionStoreManager.expandOrRenewSession()
Expand Down Expand Up @@ -384,7 +489,7 @@ describe('session store', () => {
'when session in cache is different session than in store and store session is not tracked, ' +
'should expire session, store not tracked session and trigger renew',
() => {
setupSessionStore(createSessionState(FakeTrackingType.TRACKED, FIRST_ID), (rawTrackingType) =>
setupSessionStore(createSessionState(FakeTrackingType.TRACKED, FIRST_ID), undefined, (rawTrackingType) =>
rawTrackingType === FakeTrackingType.TRACKED ? FakeTrackingType.TRACKED : FakeTrackingType.NOT_TRACKED
)
setSessionInStore(createSessionState(FakeTrackingType.NOT_TRACKED, ''))
Expand Down Expand Up @@ -604,6 +709,7 @@ describe('session store', () => {
DEFAULT_CONFIGURATION,
PRODUCT_KEY,
computeTrackingType,
undefined,
sessionStoreStrategy
)
sessionStoreManager.sessionStateUpdateObservable.subscribe(updateSpy)
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/domain/session/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function startSessionStore<TrackingType extends string>(
configuration: Configuration,
productKey: string,
computeTrackingType: (rawTrackingType?: string) => TrackingType,
applicationId?: string,
sessionStoreStrategy: SessionStoreStrategy = getSessionStoreStrategy(sessionStoreStrategyType, configuration)
): SessionStore {
const renewObservable = new Observable<void>()
Expand All @@ -93,7 +94,6 @@ export function startSessionStore<TrackingType extends string>(

const watchSessionTimeoutId = setInterval(watchSession, STORAGE_POLL_DELAY)
let sessionCache: SessionState

startSession()

const { throttled: throttledExpandOrRenewSession, cancel: cancelExpandOrRenewSession } = throttle(() => {
Expand All @@ -105,7 +105,7 @@ export function startSessionStore<TrackingType extends string>(
}

const synchronizedSession = synchronizeSession(sessionState)
expandOrRenewSessionState(synchronizedSession)
expandOrRenewSessionState(synchronizedSession, applicationId)
return synchronizedSession
},
after: (sessionState) => {
Expand Down Expand Up @@ -182,7 +182,7 @@ export function startSessionStore<TrackingType extends string>(
)
}

function expandOrRenewSessionState(sessionState: SessionState) {
function expandOrRenewSessionState(sessionState: SessionState, applicationId?: string) {
if (isSessionInNotStartedState(sessionState)) {
return false
}
Expand All @@ -191,7 +191,7 @@ export function startSessionStore<TrackingType extends string>(
sessionState[productKey] = trackingType
delete sessionState.isExpired
if (trackingType !== SESSION_NOT_TRACKED && !sessionState.id) {
sessionState.id = generateUUID()
sessionState.id = generateUUID(sessionState.anonymousId, applicationId)
sessionState.created = String(dateNow())
}
}
Expand Down
Loading