Skip to content
Draft
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
70 changes: 61 additions & 9 deletions packages/core/src/domain/session/sessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ import type { SessionStoreStrategyType } from './storeStrategies/sessionStoreStr
import type { SessionState } from './sessionState'
import { EXPIRED } from './sessionState'

// Flush several microtask cycles so chained awaits inside startSessionManager
// (resolveInitialState's await, the .catch chain, and the post-await continuation)
// have a chance to run before assertions.
async function flushMicrotasks(): Promise<void> {
for (let i = 0; i < 10; i += 1) {
await Promise.resolve()
}
}

describe('startSessionManager', () => {
const STORE_TYPE: SessionStoreStrategyType = { type: SessionPersistence.COOKIE, cookieOptions: {} }
let fakeStrategy: ReturnType<typeof createFakeSessionStoreStrategy>
Expand Down Expand Up @@ -518,18 +527,55 @@ describe('startSessionManager', () => {
expect(fakeStrategy.getInternalState().anonymousId).toBeUndefined()
})

it('should expire the session when consent is revoked before initialization completes', async () => {
it('should not install the session manager while consent stays revoked after being revoked during init', async () => {
const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED)

// Create a strategy where setSessionState returns a pending promise (to simulate async init)
let resolveInit!: () => void
// Strategy where setSessionState returns a pending promise (to simulate async init)
const initResolvers: Array<() => void> = []
const delayedStrategy = createFakeSessionStoreStrategy()
delayedStrategy.setSessionState = jasmine
.createSpy('setSessionState')
.and.callFake((fn: (state: SessionState) => SessionState): Promise<void> => {
fn({})
return new Promise<void>((resolve) => {
resolveInit = resolve
initResolvers.push(resolve)
})
})

fakeStrategy = delayedStrategy

const sessionManagerResolution = jasmine.createSpy('sessionManagerResolution')
void startSessionManager(
{
sessionStoreStrategyType: STORE_TYPE,
sessionSampleRate: 100,
trackAnonymousUser: false,
} as Configuration,
trackingConsentState
).then(sessionManagerResolution)

// Consent revoked while initialization promise is pending
trackingConsentState.update(TrackingConsent.NOT_GRANTED)
initResolvers[0]()

// Flush several microtask cycles so the consent check inside startSessionManager runs
await flushMicrotasks()

// Promise must stay pending β€” re-granting consent later should be able to recover
expect(sessionManagerResolution).not.toHaveBeenCalled()
})

it('should install the session manager when consent is granted again after being revoked during init', async () => {
const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED)

const initResolvers: Array<() => void> = []
const delayedStrategy = createFakeSessionStoreStrategy()
delayedStrategy.setSessionState = jasmine
.createSpy('setSessionState')
.and.callFake((fn: (state: SessionState) => SessionState): Promise<void> => {
fn({})
return new Promise<void>((resolve) => {
initResolvers.push(resolve)
})
})

Expand All @@ -544,15 +590,21 @@ describe('startSessionManager', () => {
trackingConsentState
)

// Consent revoked while initialization promise is pending
// Consent revoked during the first setSessionState
trackingConsentState.update(TrackingConsent.NOT_GRANTED)
initResolvers[0]()
await flushMicrotasks()

// Resolve the initialization promise
resolveInit()
// Customer re-grants consent β†’ triggers a fresh resolveInitialState
trackingConsentState.update(TrackingConsent.GRANTED)
await flushMicrotasks()

// Resolve the second setSessionState so initialization can complete
expect(initResolvers.length).toBe(2)
initResolvers[1]()

// Should resolve with undefined because consent was revoked
const sessionManager = await sessionManagerPromise
expect(sessionManager).toBeUndefined()
expect(sessionManager).toBeDefined()
})
})

Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,26 @@ export async function startSessionManager(
stopped = true
})

const initialState = await resolveInitialState().catch(monitorError)
let initialState = await resolveInitialState().catch(monitorError)
if (!initialState || stopped) {
return
}

// Consent is always granted when the session manager is started, but it may
// be revoked during the async initialization (e.g., while waiting for cookie lock).
if (!trackingConsentState.isGranted()) {
expire()
return
// Wait for it to be granted again before installing the session manager β€”
// otherwise re-granting consent later in the page would never bring the SDK back.
while (!trackingConsentState.isGranted()) {
await new Promise<void>((resolve) => trackingConsentState.onGrantedOnce(resolve))
if (stopped) {
return
}
// Re-resolve the initial state after the consent re-grant; intervening
// state changes (other tabs, expirations) may have made the previous one stale.
initialState = await resolveInitialState().catch(monitorError)
if (!initialState || stopped) {
return
}
}

sessionContextHistory.add(buildSessionContext(initialState), clocksOrigin().relative)
Expand Down
Loading