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
263 changes: 163 additions & 100 deletions packages/core/src/domain/session/sessionManager.spec.ts

Large diffs are not rendered by default.

82 changes: 42 additions & 40 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export function startSessionManager<TrackingType extends string>(
configuration: Configuration,
productKey: string,
computeTrackingType: (rawTrackingType?: string) => TrackingType,
trackingConsentState: TrackingConsentState
): SessionManager<TrackingType> {
trackingConsentState: TrackingConsentState,
onReady: (sessionManager: SessionManager<TrackingType>) => void
) {
const renewObservable = new Observable<void>()
const expireObservable = new Observable<void>()

Expand All @@ -68,41 +69,51 @@ export function startSessionManager<TrackingType extends string>(
})
stopCallbacks.push(() => sessionContextHistory.stop())

sessionStore.renewObservable.subscribe(() => {
sessionContextHistory.add(buildSessionContext(), relativeNow())
renewObservable.notify()
})
sessionStore.expireObservable.subscribe(() => {
expireObservable.notify()
sessionContextHistory.closeActive(relativeNow())
})

// We expand/renew session unconditionally as tracking consent is always granted when the session
// manager is started.
sessionStore.expandOrRenewSession()
sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative)
if (isExperimentalFeatureEnabled(ExperimentalFeature.SHORT_SESSION_INVESTIGATION)) {
const session = sessionStore.getSession()
if (session) {
detectSessionIdChange(configuration, session)
sessionStore.expandOrRenewSession(() => {
sessionStore.renewObservable.subscribe(() => {
sessionContextHistory.add(buildSessionContext(), relativeNow())
renewObservable.notify()
})
sessionStore.expireObservable.subscribe(() => {
expireObservable.notify()
sessionContextHistory.closeActive(relativeNow())
})

sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative)
if (isExperimentalFeatureEnabled(ExperimentalFeature.SHORT_SESSION_INVESTIGATION)) {
const session = sessionStore.getSession()
if (session) {
detectSessionIdChange(configuration, session)
}
}
}

trackingConsentState.observable.subscribe(() => {
if (trackingConsentState.isGranted()) {
sessionStore.expandOrRenewSession()
} else {
sessionStore.expire(false)
}
})
trackingConsentState.observable.subscribe(() => {
if (trackingConsentState.isGranted()) {
sessionStore.expandOrRenewSession()
} else {
sessionStore.expire(false)
}
})

trackActivity(configuration, () => {
if (trackingConsentState.isGranted()) {
sessionStore.expandOrRenewSession()
}
trackActivity(configuration, () => {
if (trackingConsentState.isGranted()) {
sessionStore.expandOrRenewSession()
}
})
trackVisibility(configuration, () => sessionStore.expandSession())
trackResume(configuration, () => sessionStore.restartSession())

onReady({
findSession: (startTime, options) => sessionContextHistory.find(startTime, options),
renewObservable,
expireObservable,
sessionStateUpdateObservable: sessionStore.sessionStateUpdateObservable,
expire: sessionStore.expire,
updateSessionState: sessionStore.updateSessionState,
})
})
trackVisibility(configuration, () => sessionStore.expandSession())
trackResume(configuration, () => sessionStore.restartSession())

function buildSessionContext() {
const session = sessionStore.getSession()
Expand All @@ -125,15 +136,6 @@ export function startSessionManager<TrackingType extends string>(
anonymousId: session.anonymousId,
}
}

return {
findSession: (startTime, options) => sessionContextHistory.find(startTime, options),
renewObservable,
expireObservable,
sessionStateUpdateObservable: sessionStore.sessionStateUpdateObservable,
expire: sessionStore.expire,
updateSessionState: sessionStore.updateSessionState,
}
}

export function stopSessionManager() {
Expand Down
46 changes: 46 additions & 0 deletions packages/core/src/domain/session/sessionStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SessionPersistence,
} from './sessionConstants'
import type { SessionState } from './sessionState'
import { LOCK_RETRY_DELAY, createLock } from './sessionStoreOperations'

const enum FakeTrackingType {
TRACKED = 'tracked',
Expand Down Expand Up @@ -413,6 +414,51 @@ describe('session store', () => {
expect(sessionStoreManager.getSession().id).toBeUndefined()
expect(renewSpy).not.toHaveBeenCalled()
})

it('should execute callback after session expansion', () => {
setupSessionStore(createSessionState(FakeTrackingType.TRACKED, FIRST_ID))

const callbackSpy = jasmine.createSpy('callback')
sessionStoreManager.expandOrRenewSession(callbackSpy)

expect(callbackSpy).toHaveBeenCalledTimes(1)
})

it('should execute callback after lock is released', () => {
const sessionStoreStrategyType = selectSessionStoreStrategyType(DEFAULT_INIT_CONFIGURATION)
if (sessionStoreStrategyType?.type !== SessionPersistence.COOKIE) {
fail('Unable to initialize cookie storage')
return
}

// Create a locked session state
const lockedSession: SessionState = {
...createSessionState(FakeTrackingType.TRACKED, FIRST_ID),
lock: createLock(),
}

sessionStoreStrategy = createFakeSessionStoreStrategy({ isLockEnabled: true, initialSession: lockedSession })

sessionStoreManager = startSessionStore(
sessionStoreStrategyType,
DEFAULT_CONFIGURATION,
PRODUCT_KEY,
() => FakeTrackingType.TRACKED,
sessionStoreStrategy
)

const callbackSpy = jasmine.createSpy('callback')
sessionStoreManager.expandOrRenewSession(callbackSpy)

expect(callbackSpy).not.toHaveBeenCalled()

// Remove the lock from the session
sessionStoreStrategy.planRetrieveSession(0, createSessionState(FakeTrackingType.TRACKED, FIRST_ID))

clock.tick(LOCK_RETRY_DELAY)

expect(callbackSpy).toHaveBeenCalledTimes(1)
})
})

describe('expand session', () => {
Expand Down
55 changes: 30 additions & 25 deletions packages/core/src/domain/session/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { processSessionStoreOperations } from './sessionStoreOperations'
import { SESSION_NOT_TRACKED, SessionPersistence } from './sessionConstants'

export interface SessionStore {
expandOrRenewSession: () => void
expandOrRenewSession: (callback?: () => void) => void
expandSession: () => void
getSession: () => SessionState
restartSession: () => void
Expand Down Expand Up @@ -94,30 +94,34 @@ export function startSessionStore<TrackingType extends string>(
const watchSessionTimeoutId = setInterval(watchSession, STORAGE_POLL_DELAY)
let sessionCache: SessionState

startSession()

const { throttled: throttledExpandOrRenewSession, cancel: cancelExpandOrRenewSession } = throttle(() => {
processSessionStoreOperations(
{
process: (sessionState) => {
if (isSessionInNotStartedState(sessionState)) {
return
}

const synchronizedSession = synchronizeSession(sessionState)
expandOrRenewSessionState(synchronizedSession)
return synchronizedSession
},
after: (sessionState) => {
if (isSessionStarted(sessionState) && !hasSessionInCache()) {
renewSessionInCache(sessionState)
}
sessionCache = sessionState
const { throttled: throttledExpandOrRenewSession, cancel: cancelExpandOrRenewSession } = throttle(
(callback?: () => void) => {
processSessionStoreOperations(
{
process: (sessionState) => {
if (isSessionInNotStartedState(sessionState)) {
return
}

const synchronizedSession = synchronizeSession(sessionState)
expandOrRenewSessionState(synchronizedSession)
return synchronizedSession
},
after: (sessionState) => {
if (isSessionStarted(sessionState) && !hasSessionInCache()) {
renewSessionInCache(sessionState)
}
sessionCache = sessionState
callback?.()
},
},
},
sessionStoreStrategy
)
}, STORAGE_POLL_DELAY)
sessionStoreStrategy
)
},
STORAGE_POLL_DELAY
)

startSession()

function expandSession() {
processSessionStoreOperations(
Expand Down Expand Up @@ -165,7 +169,7 @@ export function startSessionStore<TrackingType extends string>(
return sessionState
}

function startSession() {
function startSession(callback?: () => void) {
processSessionStoreOperations(
{
process: (sessionState) => {
Expand All @@ -176,6 +180,7 @@ export function startSessionStore<TrackingType extends string>(
},
after: (sessionState) => {
sessionCache = sessionState
callback?.()
},
},
sessionStoreStrategy
Expand Down
19 changes: 14 additions & 5 deletions packages/core/src/domain/session/sessionStoreOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { setTimeout } from '../../tools/timer'
import { generateUUID } from '../../tools/utils/stringUtils'
import type { TimeStamp } from '../../tools/utils/timeUtils'
import { elapsed, ONE_SECOND, timeStampNow } from '../../tools/utils/timeUtils'
import { addTelemetryError } from '../telemetry'
import type { SessionStoreStrategy } from './storeStrategies/sessionStoreStrategy'
import type { SessionState } from './sessionState'
import { expandSessionState, isSessionInExpiredState } from './sessionState'
Expand All @@ -22,6 +23,14 @@ const LOCK_SEPARATOR = '--'
const bufferedOperations: Operations[] = []
let ongoingOperations: Operations | undefined

function safePersist(persistFn: () => void) {
try {
persistFn()
} catch (e) {
addTelemetryError(e)
}
}

export function processSessionStoreOperations(
operations: Operations,
sessionStoreStrategy: SessionStoreStrategy,
Expand Down Expand Up @@ -58,7 +67,7 @@ export function processSessionStoreOperations(
}
// acquire lock
currentLock = createLock()
persistWithLock(currentStore.session)
safePersist(() => persistWithLock(currentStore.session))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why do we need this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the session manager inits before the tryInit, so it outside of the monitor call.

// if lock is not acquired, retry later
currentStore = retrieveStore()
if (currentStore.lock !== currentLock) {
Expand All @@ -77,13 +86,13 @@ export function processSessionStoreOperations(
}
if (processedSession) {
if (isSessionInExpiredState(processedSession)) {
expireSession(processedSession)
safePersist(() => expireSession(processedSession!))
} else {
expandSessionState(processedSession)
if (isLockEnabled) {
persistWithLock(processedSession)
safePersist(() => persistWithLock(processedSession!))
} else {
persistSession(processedSession)
safePersist(() => persistSession(processedSession!))
}
}
}
Expand All @@ -97,7 +106,7 @@ export function processSessionStoreOperations(
retryLater(operations, sessionStoreStrategy, numberOfRetries)
return
}
persistSession(currentStore.session)
safePersist(() => persistSession(currentStore.session))
processedSession = currentStore.session
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/domain/telemetry/telemetryEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,17 +641,17 @@ export interface CommonTelemetryProperties {
*/
model?: string
/**
* Number of device processors
* Number of logical CPU cores available for scheduling on the device at runtime, as reported by the operating system.
*/
readonly processor_count?: number
readonly logical_cpu_count?: number
/**
* Total RAM in megabytes
*/
readonly total_ram?: number
/**
* Whether the device is considered a low RAM device (Android)
*/
readonly is_low_ram_device?: boolean
readonly is_low_ram?: boolean
[k: string]: unknown
}
/**
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/domain/trackingConsent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,22 @@ describe('createTrackingConsentState', () => {
trackingConsentState.tryToInit(TrackingConsent.NOT_GRANTED)
expect(trackingConsentState.isGranted()).toBeTrue()
})

describe('onGrantedOnce', () => {
it('calls onGrantedOnce when consent was already granted', () => {
const trackingConsentState = createTrackingConsentState(TrackingConsent.GRANTED)
const spy = jasmine.createSpy()
trackingConsentState.onGrantedOnce(spy)
expect(spy).toHaveBeenCalledTimes(1)
})

it('calls onGrantedOnce when consent is granted', () => {
const trackingConsentState = createTrackingConsentState()
const spy = jasmine.createSpy()
trackingConsentState.onGrantedOnce(spy)
expect(spy).toHaveBeenCalledTimes(0)
trackingConsentState.update(TrackingConsent.GRANTED)
expect(spy).toHaveBeenCalledTimes(1)
})
})
})
21 changes: 18 additions & 3 deletions packages/core/src/domain/trackingConsent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,39 @@ export interface TrackingConsentState {
update: (trackingConsent: TrackingConsent) => void
isGranted: () => boolean
observable: Observable<void>
onGrantedOnce: (callback: () => void) => void
}

export function createTrackingConsentState(currentConsent?: TrackingConsent): TrackingConsentState {
const observable = new Observable<void>()

function isGranted() {
return currentConsent === TrackingConsent.GRANTED
}

return {
tryToInit(trackingConsent: TrackingConsent) {
if (!currentConsent) {
currentConsent = trackingConsent
}
},
onGrantedOnce(fn) {
if (isGranted()) {
fn()
} else {
const subscription = observable.subscribe(() => {
if (isGranted()) {
fn()
subscription.unsubscribe()
}
})
}
},
update(trackingConsent: TrackingConsent) {
currentConsent = trackingConsent
observable.notify()
},
isGranted() {
return currentConsent === TrackingConsent.GRANTED
},
isGranted,
observable,
}
}
Loading