diff --git a/packages/core/src/domain/session/sessionManager.spec.ts b/packages/core/src/domain/session/sessionManager.spec.ts index 29c181c42c..bec5a2d72b 100644 --- a/packages/core/src/domain/session/sessionManager.spec.ts +++ b/packages/core/src/domain/session/sessionManager.spec.ts @@ -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 { + for (let i = 0; i < 10; i += 1) { + await Promise.resolve() + } +} + describe('startSessionManager', () => { const STORE_TYPE: SessionStoreStrategyType = { type: SessionPersistence.COOKIE, cookieOptions: {} } let fakeStrategy: ReturnType @@ -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 => { fn({}) return new Promise((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 => { + fn({}) + return new Promise((resolve) => { + initResolvers.push(resolve) }) }) @@ -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() }) }) diff --git a/packages/core/src/domain/session/sessionManager.ts b/packages/core/src/domain/session/sessionManager.ts index 1f164230d5..90735547ae 100644 --- a/packages/core/src/domain/session/sessionManager.ts +++ b/packages/core/src/domain/session/sessionManager.ts @@ -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((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)