Skip to content

Commit

Permalink
Allow to find expired session
Browse files Browse the repository at this point in the history
  • Loading branch information
amortemousque committed Feb 1, 2024
1 parent e708f8d commit c9e0459
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 62 deletions.
91 changes: 50 additions & 41 deletions packages/core/src/domain/session/sessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,47 +446,56 @@ describe('startSessionManager', () => {
})

describe('session history', () => {
it('should return undefined when there is no current session and no startTime', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)
expireSessionCookie()

expect(sessionManager.findActiveSession()).toBeUndefined()
})

it('should return the current session context when there is no start time', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)

expect(sessionManager.findActiveSession()!.id).toBeDefined()
expect(sessionManager.findActiveSession()!.trackingType).toBeDefined()
})

it('should return the session context corresponding to startTime', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)

// 0s to 10s: first session
clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY)
const firstSessionId = sessionManager.findActiveSession()!.id
const firstSessionTrackingType = sessionManager.findActiveSession()!.trackingType
expireSessionCookie()

// 10s to 20s: no session
clock.tick(10 * ONE_SECOND)

// 20s to end: second session
document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK))
clock.tick(10 * ONE_SECOND)
const secondSessionId = sessionManager.findActiveSession()!.id
const secondSessionTrackingType = sessionManager.findActiveSession()!.trackingType

expect(sessionManager.findActiveSession((5 * ONE_SECOND) as RelativeTime)!.id).toBe(firstSessionId)
expect(sessionManager.findActiveSession((5 * ONE_SECOND) as RelativeTime)!.trackingType).toBe(
firstSessionTrackingType
)
expect(sessionManager.findActiveSession((15 * ONE_SECOND) as RelativeTime)).toBeUndefined()
expect(sessionManager.findActiveSession((25 * ONE_SECOND) as RelativeTime)!.id).toBe(secondSessionId)
expect(sessionManager.findActiveSession((25 * ONE_SECOND) as RelativeTime)!.trackingType).toBe(
secondSessionTrackingType
)
;['findActiveSession' as const, 'findActiveOrExpiredSession' as const].forEach((method) => {
describe(method, () => {
it('should return the current session context when there is no start time', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)

expect(sessionManager[method]()!.id).toBeDefined()
expect(sessionManager[method]()!.trackingType).toBeDefined()
})

it('should return the session context corresponding to startTime', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)

// 0s to 10s: first session
clock.tick(10 * ONE_SECOND - STORAGE_POLL_DELAY)
const firstSessionId = sessionManager[method]()!.id
const firstSessionTrackingType = sessionManager[method]()!.trackingType
expireSessionCookie()

// 10s to end: second session
document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK))
clock.tick(10 * ONE_SECOND)
const secondSessionId = sessionManager[method]()!.id
const secondSessionTrackingType = sessionManager[method]()!.trackingType

expect(sessionManager[method]((5 * ONE_SECOND) as RelativeTime)!.id).toBe(firstSessionId)
expect(sessionManager[method]((5 * ONE_SECOND) as RelativeTime)!.trackingType).toBe(firstSessionTrackingType)
expect(sessionManager[method]((15 * ONE_SECOND) as RelativeTime)!.id).toBe(secondSessionId)
expect(sessionManager[method]((15 * ONE_SECOND) as RelativeTime)!.trackingType).toBe(
secondSessionTrackingType
)
})
})
})

describe('findActiveSession', () => {
it('should return undefined when the session is expired', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)
expireSessionCookie()

expect(sessionManager.findActiveSession()).toBeUndefined()
})
})

describe('findActiveOrExpiredSession', () => {
it('should return the session context when the session is expired', () => {
const sessionManager = startSessionManager(configuration, FIRST_PRODUCT_KEY, () => TRACKED_SESSION_STATE)
expireSessionCookie()

expect(sessionManager.findActiveOrExpiredSession()).toBeDefined()
})
})
})
})
18 changes: 13 additions & 5 deletions packages/core/src/domain/session/sessionManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Observable } from '../../tools/observable'
import type { Context } from '../../tools/serialisation/context'
import { ValueHistory } from '../../tools/valueHistory'
import { AFTER_ENTRY_START, ValueHistory } from '../../tools/valueHistory'
import type { RelativeTime } from '../../tools/utils/timeUtils'
import { relativeNow, clocksOrigin, ONE_MINUTE } from '../../tools/utils/timeUtils'
import { DOM_EVENT, addEventListener, addEventListeners } from '../../browser/addEventListener'
Expand All @@ -11,6 +11,8 @@ import { startSessionStore } from './sessionStore'

export interface SessionManager<TrackingType extends string> {
findActiveSession: (startTime?: RelativeTime) => SessionContext<TrackingType> | undefined
findActiveOrExpiredSession: (startTime?: RelativeTime) => SessionContext<TrackingType> | undefined

renewObservable: Observable<void>
expireObservable: Observable<void>
expire: () => void
Expand All @@ -19,6 +21,7 @@ export interface SessionManager<TrackingType extends string> {
export interface SessionContext<TrackingType extends string> extends Context {
id: string
trackingType: TrackingType
endTime?: RelativeTime
}

export const VISIBILITY_CHECK_DELAY = ONE_MINUTE
Expand All @@ -37,20 +40,24 @@ export function startSessionManager<TrackingType extends string>(
const sessionContextHistory = new ValueHistory<SessionContext<TrackingType>>(SESSION_CONTEXT_TIMEOUT_DELAY)
stopCallbacks.push(() => sessionContextHistory.stop())

let currentSessionContext = buildSessionContext()

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

sessionStore.expandOrRenewSession()
sessionContextHistory.add(buildSessionContext(), clocksOrigin().relative)
sessionContextHistory.add(currentSessionContext, clocksOrigin().relative)

trackActivity(configuration, () => sessionStore.expandOrRenewSession())
trackVisibility(configuration, () => sessionStore.expandSession())

function buildSessionContext() {
function buildSessionContext(): SessionContext<TrackingType> {
return {
id: sessionStore.getSession().id!,
trackingType: sessionStore.getSession()[productKey] as TrackingType,
Expand All @@ -59,6 +66,7 @@ export function startSessionManager<TrackingType extends string>(

return {
findActiveSession: (startTime) => sessionContextHistory.find(startTime),
findActiveOrExpiredSession: (startTime) => sessionContextHistory.find(startTime, AFTER_ENTRY_START),
renewObservable: sessionStore.renewObservable,
expireObservable: sessionStore.expireObservable,
expire: sessionStore.expire,
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/tools/valueHistory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Clock } from '../../test'
import { mockClock } from '../../test'
import type { Duration, RelativeTime } from './utils/timeUtils'
import { addDuration, ONE_MINUTE } from './utils/timeUtils'
import { CLEAR_OLD_VALUES_INTERVAL, ValueHistory } from './valueHistory'
import { AFTER_ENTRY_START, CLEAR_OLD_VALUES_INTERVAL, ValueHistory } from './valueHistory'

const EXPIRE_DELAY = 10 * ONE_MINUTE
const MAX_ENTRIES = 5
Expand Down Expand Up @@ -54,6 +54,15 @@ describe('valueHistory', () => {

expect(valueHistory.find(15 as RelativeTime)).toBeUndefined()
})

describe('with AFTER_ENTRY_START predicate', () => {
it('should return the value of the closest entry regardless of the being closed', () => {
valueHistory.add('foo', 0 as RelativeTime).close(10 as RelativeTime)
valueHistory.add('bar', 20 as RelativeTime)

expect(valueHistory.find(15 as RelativeTime, AFTER_ENTRY_START)).toEqual('foo')
})
})
})

describe('findAll', () => {
Expand Down
13 changes: 9 additions & 4 deletions packages/core/src/tools/valueHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { addDuration, relativeNow, ONE_MINUTE } from './utils/timeUtils'

const END_OF_TIMES = Infinity as RelativeTime

export const AFTER_ENTRY_START = () => true
const DURING_ENTRY_LIFESPAN = <Value>(entry: ValueHistoryEntry<Value>, startTime: RelativeTime) =>
startTime <= entry.endTime

export interface ValueHistoryEntry<T> {
startTime: RelativeTime
endTime: RelativeTime
Expand Down Expand Up @@ -60,13 +64,14 @@ export class ValueHistory<Value> {
}

/**
* Return the latest value that was active during `startTime`, or the currently active value
* if no `startTime` is provided. This method assumes that entries are not overlapping.
* Return the latest value matching the startTime and the predicate,
* if no `startTime` is provided, return the most recent value
* if no `predicate` is provided, return the active value
*/
find(startTime: RelativeTime = END_OF_TIMES): Value | undefined {
find(startTime: RelativeTime = END_OF_TIMES, predicate = DURING_ENTRY_LIFESPAN): Value | undefined {
for (const entry of this.entries) {
if (entry.startTime <= startTime) {
if (startTime <= entry.endTime) {
if (predicate(entry, startTime)) {
return entry.value
}
break
Expand Down
30 changes: 25 additions & 5 deletions packages/logs/src/domain/logsSessionManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ describe('logs session manager', () => {
expect(getCookie(SESSION_STORE_KEY)).toContain(`${LOGS_SESSION_KEY}=${LoggerTrackingType.TRACKED}`)
})

describe('findSession', () => {
it('should return the current session', () => {
describe('findTrackedSession', () => {
it('should return the current active session', () => {
setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION)
const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration)
expect(logsSessionManager.findTrackedSession()!.id).toBe('abcdef')
Expand All @@ -101,17 +101,37 @@ describe('logs session manager', () => {
it('should return undefined if the session is not tracked', () => {
setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=0', DURATION)
const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration)
expect(logsSessionManager.findTrackedSession()).toBe(undefined)
expect(logsSessionManager.findTrackedSession()).toBeUndefined()
})

it('should not return the current session if it has expired by default', () => {
setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION)
const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration)
clock.tick(10 * ONE_SECOND)
setCookie(SESSION_STORE_KEY, '', DURATION)
clock.tick(STORAGE_POLL_DELAY)
expect(logsSessionManager.findTrackedSession()).toBeUndefined()
})

it('should return undefined if the session has expired', () => {
it('should return the current session if it has expired when returnExpired = true', () => {
const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration)
setCookie(SESSION_STORE_KEY, '', DURATION)
clock.tick(STORAGE_POLL_DELAY)
expect(logsSessionManager.findTrackedSession()).toBe(undefined)
expect(logsSessionManager.findTrackedSession(relativeNow(), { returnExpired: true })).toBeDefined()
})

it('should return session corresponding to start time', () => {
setCookie(SESSION_STORE_KEY, 'id=foo&logs=1', DURATION)
const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration)
clock.tick(10 * ONE_SECOND)
setCookie(SESSION_STORE_KEY, 'id=bar&logs=1', DURATION)
// simulate a click to renew the session
document.dispatchEvent(createNewEvent(DOM_EVENT.CLICK))
clock.tick(STORAGE_POLL_DELAY)
expect(logsSessionManager.findTrackedSession(0 as RelativeTime)!.id).toEqual('foo')
expect(logsSessionManager.findTrackedSession()!.id).toEqual('bar')
})
})
setCookie(SESSION_STORE_KEY, 'id=abcdef&logs=1', DURATION)
const logsSessionManager = startLogsSessionManager(configuration as LogsConfiguration)
clock.tick(10 * ONE_SECOND)
Expand Down
16 changes: 10 additions & 6 deletions packages/logs/src/domain/logsSessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { LogsConfiguration } from './configuration'
export const LOGS_SESSION_KEY = 'logs'

export interface LogsSessionManager {
findTrackedSession: (startTime?: RelativeTime) => LogsSession | undefined
findTrackedSession: (startTime?: RelativeTime, options?: { returnExpired: boolean }) => LogsSession | undefined
expireObservable: Observable<void>
}

Expand All @@ -22,14 +22,18 @@ export function startLogsSessionManager(configuration: LogsConfiguration): LogsS
const sessionManager = startSessionManager(configuration, LOGS_SESSION_KEY, (rawTrackingType) =>
computeSessionState(configuration, rawTrackingType)
)

return {
findTrackedSession: (startTime) => {
const session = sessionManager.findActiveSession(startTime)
return session && session.trackingType === LoggerTrackingType.TRACKED
? {
findTrackedSession: (startTime?: RelativeTime, { returnExpired } = { returnExpired: false }) => {
const session = returnExpired
? sessionManager.findActiveOrExpiredSession(startTime)
: sessionManager.findActiveSession(startTime)

if (session && session.trackingType === LoggerTrackingType.TRACKED) {
return {
id: session.id,
}
: undefined
}
},
expireObservable: sessionManager.expireObservable,
}
Expand Down

0 comments on commit c9e0459

Please sign in to comment.