diff --git a/CHANGELOG.md b/CHANGELOG.md index bafe75e9601..d68779762ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ For semantic convention package changes, see the [semconv CHANGELOG](packages/se ### :rocket: (Enhancement) +* feat(web): add session handling implementation [5173](https://github.com/open-telemetry/opentelemetry-js/pull/5173) + ### :bug: (Bug Fix) ### :books: (Refine Doc) diff --git a/examples/opentelemetry-web/examples/session/index.html b/examples/opentelemetry-web/examples/session/index.html new file mode 100644 index 00000000000..0d5231d2636 --- /dev/null +++ b/examples/opentelemetry-web/examples/session/index.html @@ -0,0 +1,19 @@ + + + + + + Session Example + + + + + + + Example of using attaching session attributes to spans and logs. + + + + + + diff --git a/examples/opentelemetry-web/examples/session/index.js b/examples/opentelemetry-web/examples/session/index.js new file mode 100644 index 00000000000..5b5984cd474 --- /dev/null +++ b/examples/opentelemetry-web/examples/session/index.js @@ -0,0 +1,69 @@ +const { ConsoleSpanExporter, SimpleSpanProcessor } = require( '@opentelemetry/sdk-trace-base'); +const { WebTracerProvider } = require( '@opentelemetry/sdk-trace-web'); +const { + LoggerProvider, + SimpleLogRecordProcessor, + ConsoleLogRecordExporter, +} = require('@opentelemetry/sdk-logs'); +const { + createSessionSpanProcessor, + createSessionLogRecordProcessor, + Session, + SessionManager, + DefaultIdGenerator, + LocalStorageSessionStore +} = require('@opentelemetry/web-common'); + +// session manager +const sessionManager = new SessionManager({ + sessionIdGenerator: new DefaultIdGenerator(), + sessionStore: new LocalStorageSessionStore(), + maxDuration: 20, + inactivityTimeout: 10 +}); + +sessionManager.addObserver({ + onSessionStarted: (newSession, previousSession) => { + console.log('Session started', newSession, previousSession); + }, + onSessionEnded: (session) => { + console.log('Session ended', session); + } +}); + +// configure tracer +const provider = new WebTracerProvider(); +provider.addSpanProcessor(createSessionSpanProcessor(sessionManager)); +provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +provider.register(); +const tracer = provider.getTracer('example'); + +// configure logger +const loggerProvider = new LoggerProvider(); +loggerProvider.addLogRecordProcessor(createSessionLogRecordProcessor(sessionManager)); +loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()) +); +const logger = loggerProvider.getLogger('example'); + +window.addEventListener('load', () => { + document.getElementById('button1').addEventListener('click', generateSpan); + document.getElementById('button2').addEventListener('click', generateLog); +}); + +function generateSpan() { + const span = tracer.startSpan('foo'); + span.setAttribute('key', 'value'); + span.end(); +} + +function generateLog() { + logger.emit({ + attributes: { + name: 'my-event', + }, + body: { + key1: 'val1' + } + }); +} diff --git a/examples/opentelemetry-web/webpack.dev.config.js b/examples/opentelemetry-web/webpack.dev.config.js index 6d3be1090b1..09922ca7816 100644 --- a/examples/opentelemetry-web/webpack.dev.config.js +++ b/examples/opentelemetry-web/webpack.dev.config.js @@ -14,6 +14,7 @@ const common = { fetchXhrB3: 'examples/fetchXhrB3/index.js', 'fetch-proto': 'examples/fetch-proto/index.js', zipkin: 'examples/zipkin/index.js', + session: 'examples/session/index.js' }, output: { path: path.resolve(__dirname, 'dist'), diff --git a/examples/opentelemetry-web/webpack.prod.config.js b/examples/opentelemetry-web/webpack.prod.config.js index 96f7d69af29..4e508f165f1 100644 --- a/examples/opentelemetry-web/webpack.prod.config.js +++ b/examples/opentelemetry-web/webpack.prod.config.js @@ -14,6 +14,7 @@ const common = { fetchXhrB3: 'examples/fetchXhrB3/index.js', "fetch-proto": "examples/fetch-proto/index.js", zipkin: 'examples/zipkin/index.js', + session: 'examples/session/index.js' }, output: { path: path.resolve(__dirname, 'dist'), diff --git a/experimental/packages/web-common/README.md b/experimental/packages/web-common/README.md index b5c0bbacd6a..4f2be6e1cb2 100644 --- a/experimental/packages/web-common/README.md +++ b/experimental/packages/web-common/README.md @@ -13,6 +13,84 @@ This package contains classes and utils that are common for web use cases. npm install --save @opentelemetry/web-common ``` +## Sessions + +Sessions correlate multiple traces, events and logs that happen within a given time period. Sessions are represented as span/log attributes prefixed with the `session.` namespace. For additional information, see [documentation in semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/session.md). + +We provide a default implementation of managing sessions that: + +- abstracts persisting sessions across page loads, with default implementation based on LocalStorage +- abstracts generating session IDs +- provides a mechanism for resetting the active session after maximum defined duration +- provides a mechanism for resetting the active session after a defined inactivity duration + +Example: + +```js +const { + createSessionSpanProcessor, + createSessionLogRecordProcessor, + SessionManager, + DefaultIdGenerator, + LocalStorageSessionStore +} = require('@opentelemetry/web-common'); + +// session manager +const sessionManager = new SessionManager({ + sessionIdGenerator: new DefaultIdGenerator(), + sessionStore: new LocalStorageSessionStore(), + maxDuration: 7200, // 4 hours + inactivityTimeout: 1800 // 30 minutes +}); + +// configure tracer +const provider = new WebTracerProvider(); +provider.addSpanProcessor(createSessionSpanProcessor(sessionManager)); + +// configure logger +const loggerProvider = new LoggerProvider(); +loggerProvider.addLogRecordProcessor(createSessionLogRecordProcessor(sessionManager)); +``` + +The above implementation can be customized by providing different implementations of SessionStore and SessionIdGenerator. + +### Observing sessions + +The SessionManager class provides a mechanism for observing sessions. This is useful when other components should be notified when a session is started or ended. + +```js +sessionManager.addObserver({ + onSessionStarted: (newSession, previousSession) => { + console.log('Session started', newSession, previousSession); + }, + onSessionEnded: (session) => { + console.log('Session ended', session); + } +}); +``` + +### Custom implementation of managing sessions + +If you require a completely custom solution for managing sessions, you can still use the processors that attach attributes to spans/logs. Here is an example: + +```js +function getSessionId() { + return 'abcd1234'; +} + +// configure tracer +const provider = new WebTracerProvider(); +provider.addSpanProcessor(createSessionSpanProcessor({ + getSessionId: getSessionId +})); + +// configure logger +const loggerProvider = new LoggerProvider(); +loggerProvider.addLogRecordProcessor(createSessionLogRecordProcessor({ + getSessionId: getSessionId +})); +``` + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/experimental/packages/web-common/src/DefaultIdGenerator.ts b/experimental/packages/web-common/src/DefaultIdGenerator.ts new file mode 100644 index 00000000000..e85c956d42c --- /dev/null +++ b/experimental/packages/web-common/src/DefaultIdGenerator.ts @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SessionIdGenerator } from './types/SessionIdGenerator'; + +export class DefaultIdGenerator implements SessionIdGenerator { + generateSessionId = getIdGenerator(16); +} + +const SHARED_CHAR_CODES_ARRAY = Array(32); +function getIdGenerator(bytes: number): () => string { + return function generateId() { + for (let i = 0; i < bytes * 2; i++) { + SHARED_CHAR_CODES_ARRAY[i] = Math.floor(Math.random() * 16) + 48; + // valid hex characters in the range 48-57 and 97-102 + if (SHARED_CHAR_CODES_ARRAY[i] >= 58) { + SHARED_CHAR_CODES_ARRAY[i] += 39; + } + } + return String.fromCharCode.apply( + null, + SHARED_CHAR_CODES_ARRAY.slice(0, bytes * 2) + ); + }; +} diff --git a/experimental/packages/web-common/src/LocalStorageSessionStore.ts b/experimental/packages/web-common/src/LocalStorageSessionStore.ts new file mode 100644 index 00000000000..6837df08ba1 --- /dev/null +++ b/experimental/packages/web-common/src/LocalStorageSessionStore.ts @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Session } from './types/Session'; +import { SessionStore } from './types/SessionStore'; + +const SESSION_STORAGE_KEY = 'opentelemetry-session'; + +export class LocalStorageSessionStore implements SessionStore { + save(session: Session): void { + localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)); + } + + get(): Session | null { + const sessionData = localStorage.getItem(SESSION_STORAGE_KEY); + if (sessionData) { + return JSON.parse(sessionData) as Session; + } + return null; + } +} diff --git a/experimental/packages/web-common/src/SessionManager.ts b/experimental/packages/web-common/src/SessionManager.ts new file mode 100644 index 00000000000..6a8128e0ffc --- /dev/null +++ b/experimental/packages/web-common/src/SessionManager.ts @@ -0,0 +1,177 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SessionIdGenerator } from './types/SessionIdGenerator'; +import { Session } from './types/Session'; +import { SessionProvider } from './types/SessionProvider'; +import { SessionObserver } from './types/SessionObserver'; +import { SessionStore } from './types/SessionStore'; +import { SessionPublisher } from './types/SessionPublisher'; + +export interface SessionManagerConfig { + /** Class responsible for generating a session ID */ + sessionIdGenerator: SessionIdGenerator; + /** Class responsible for storing session data over multiple page loads. */ + sessionStore: SessionStore; + /** Maximum duration of a session in seconds */ + maxDuration?: number; + /** Maximum time without a user activity after which a session should be expired, in seconds */ + inactivityTimeout?: number; +} + +/** + * SessionManager is responsible for managing the active session including starting, + * resetting, and persisting. + */ +export class SessionManager implements SessionProvider, SessionPublisher { + private _session: Session | null | undefined; + private _idGenerator: SessionIdGenerator; + private _store: SessionStore; + private _observers: SessionObserver[]; + + private _maxDuration?: number; + private _maxDurationTimeoutId?: ReturnType; + + private _inactivityTimeout?: number; + private _inactivityTimeoutId?: ReturnType; + private _lastActivityTimestamp: number = 0; + private _inactivityResetDelay: number = 5000; // minimum time in ms between activities before timer is reset + + constructor(config: SessionManagerConfig) { + this._idGenerator = config.sessionIdGenerator; + this._store = config.sessionStore; + this._maxDuration = config.maxDuration; + this._inactivityTimeout = config.inactivityTimeout; + + this._observers = []; + + this._session = this._store.get(); + if (this._session) { + this.resetTimers(); + } + } + + addObserver(observer: SessionObserver): void { + this._observers.push(observer); + } + + getSessionId(): string | null { + return this.getSession().id; + } + + getSession(): Session { + if (!this._session) { + this._session = this.startSession(); + this.resetTimers(); + } + + if (this._inactivityTimeout) { + if ( + Date.now() - this._lastActivityTimestamp > + this._inactivityResetDelay + ) { + this.resetInactivityTimer(); + this._lastActivityTimestamp = Date.now(); + } + } + + return this._session; + } + + shutdown(): void { + if (this._inactivityTimeoutId) { + clearTimeout(this._inactivityTimeoutId); + } + + if (this._maxDurationTimeoutId) { + clearTimeout(this._maxDurationTimeoutId); + } + } + + private startSession(): Session { + const sessionId = this._idGenerator.generateSessionId(); + + const session: Session = { + id: sessionId, + startTimestamp: Date.now(), + }; + + this._store.save(session); + + for (const observer of this._observers) { + observer.onSessionStarted(session, this._session ?? undefined); + } + + this._session = session; + + return session; + } + + private endSession(): void { + if (this._session) { + for (const observer of this._observers) { + observer.onSessionEnded(this._session); + } + } + + if (this._inactivityTimeoutId) { + clearTimeout(this._inactivityTimeoutId); + this._inactivityTimeoutId = undefined; + } + } + + private resetSession(): void { + this.endSession(); + this.startSession(); + this.resetTimers(); + } + + private resetTimers() { + this.resetInactivityTimer(); + this.resetMaxDurationTimer(); + } + + private resetInactivityTimer() { + if (!this._inactivityTimeout) { + return; + } + + if (this._inactivityTimeoutId) { + clearTimeout(this._inactivityTimeoutId); + } + + this._inactivityTimeoutId = setTimeout(() => { + this.resetSession(); + }, this._inactivityTimeout * 1000); + } + + private resetMaxDurationTimer() { + if (!this._maxDuration || !this._session) { + return; + } + + if (this._maxDurationTimeoutId) { + clearTimeout(this._maxDurationTimeoutId); + } + + const timeoutIn = + this._maxDuration * 1000 - (Date.now() - this._session?.startTimestamp); + + this._maxDurationTimeoutId = setTimeout(() => { + this.resetSession(); + }, timeoutIn); + } +} diff --git a/experimental/packages/web-common/src/index.ts b/experimental/packages/web-common/src/index.ts index a9b7d66facc..719ebcbf1fb 100644 --- a/experimental/packages/web-common/src/index.ts +++ b/experimental/packages/web-common/src/index.ts @@ -19,3 +19,11 @@ export { createSessionSpanProcessor, createSessionLogRecordProcessor, } from './utils'; +export { Session } from './types/Session'; +export { SessionIdGenerator } from './types/SessionIdGenerator'; +export { SessionPublisher } from './types/SessionPublisher'; +export { SessionObserver } from './types/SessionObserver'; +export { SessionStore } from './types/SessionStore'; +export { SessionManager } from './SessionManager'; +export { LocalStorageSessionStore } from './LocalStorageSessionStore'; +export { DefaultIdGenerator } from './DefaultIdGenerator'; diff --git a/experimental/packages/web-common/src/types/Session.ts b/experimental/packages/web-common/src/types/Session.ts new file mode 100644 index 00000000000..df30f0e5e2a --- /dev/null +++ b/experimental/packages/web-common/src/types/Session.ts @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Session { + id: string; + startTimestamp: number; // epoch milliseconds +} diff --git a/experimental/packages/web-common/src/types/SessionIdGenerator.ts b/experimental/packages/web-common/src/types/SessionIdGenerator.ts new file mode 100644 index 00000000000..ec168882783 --- /dev/null +++ b/experimental/packages/web-common/src/types/SessionIdGenerator.ts @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface SessionIdGenerator { + generateSessionId(): string; +} diff --git a/experimental/packages/web-common/src/types/SessionObserver.ts b/experimental/packages/web-common/src/types/SessionObserver.ts new file mode 100644 index 00000000000..68f2d3fde5e --- /dev/null +++ b/experimental/packages/web-common/src/types/SessionObserver.ts @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Session } from './Session'; + +export interface SessionObserver { + onSessionStarted(newSession: Session, previousSession?: Session): void; + onSessionEnded(session: Session): void; +} diff --git a/experimental/packages/web-common/src/types/SessionPublisher.ts b/experimental/packages/web-common/src/types/SessionPublisher.ts new file mode 100644 index 00000000000..28a4aa319c5 --- /dev/null +++ b/experimental/packages/web-common/src/types/SessionPublisher.ts @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SessionObserver } from './SessionObserver'; + +export interface SessionPublisher { + addObserver(observer: SessionObserver): void; +} diff --git a/experimental/packages/web-common/src/types/SessionStore.ts b/experimental/packages/web-common/src/types/SessionStore.ts new file mode 100644 index 00000000000..806af2c59b7 --- /dev/null +++ b/experimental/packages/web-common/src/types/SessionStore.ts @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Session } from './Session'; + +export interface SessionStore { + save(session: Session): void; + get(): Session | null; +} diff --git a/experimental/packages/web-common/test/SessionManager.test.ts b/experimental/packages/web-common/test/SessionManager.test.ts new file mode 100644 index 00000000000..52097e8c3ba --- /dev/null +++ b/experimental/packages/web-common/test/SessionManager.test.ts @@ -0,0 +1,184 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import { SessionManager, SessionManagerConfig } from '../src/SessionManager'; +import { SessionIdGenerator } from '../src/types/SessionIdGenerator'; +import { SessionStore } from '../src/types/SessionStore'; +import { SessionObserver } from '../src/types/SessionObserver'; +import { Session } from '../src/types/Session'; + +class MockSessionIdGenerator implements SessionIdGenerator { + private _id = 0; + generateSessionId(): string { + return `session-${++this._id}`; + } +} + +class MockSessionStore implements SessionStore { + private _session: Session | null = null; + save(session: Session): void { + this._session = session; + } + get(): Session | null { + return this._session; + } +} + +class MockSessionObserver implements SessionObserver { + startedSessions: Session[] = []; + endedSessions: Session[] = []; + + onSessionStarted(newSession: Session, oldSession?: Session): void { + this.startedSessions.push(newSession); + } + + onSessionEnded(session: Session): void { + this.endedSessions.push(session); + } +} + +describe('SessionManager', () => { + let config: SessionManagerConfig; + let store: MockSessionStore; + let idGenerator: MockSessionIdGenerator; + let observer: MockSessionObserver; + let sessionManager: SessionManager; + + beforeEach(() => { + idGenerator = new MockSessionIdGenerator(); + store = new MockSessionStore(); + observer = new MockSessionObserver(); + config = { + sessionIdGenerator: idGenerator, + sessionStore: store, + }; + }); + + afterEach(() => { + sessionManager.shutdown(); + }); + + it('should start a new session if none exists', () => { + sessionManager = new SessionManager(config); + const session = sessionManager.getSession(); + assert.strictEqual(session?.id, 'session-1'); + assert.strictEqual(store.get()?.id, 'session-1'); + }); + + it('should return the same session ID if session exists', () => { + sessionManager = new SessionManager(config); + sessionManager.getSessionId(); // Starts session-1 + const sessionId = sessionManager.getSessionId(); // Reuse session-1 + assert.strictEqual(sessionId, 'session-1'); + }); + + it('should reset session after max duration', async () => { + config = { + sessionIdGenerator: idGenerator, + sessionStore: store, + maxDuration: 0.5, + }; + sessionManager = new SessionManager(config); + + sessionManager.getSessionId(); // Starts session-1 + await wait(600); + const sessionId = sessionManager.getSessionId(); + assert.strictEqual(sessionId, 'session-2'); + }); + + it('should resume max duration in a new instance', async () => { + config = { + sessionIdGenerator: idGenerator, + sessionStore: store, + maxDuration: 0.5, + }; + sessionManager = new SessionManager(config); + sessionManager.getSessionId(); // Starts session-1 + await wait(400); + + // create a new manager with the same store + sessionManager.shutdown(); + sessionManager = new SessionManager(config); + + await wait(300); + const sessionId = sessionManager.getSessionId(); + assert.strictEqual(sessionId, 'session-2'); + }); + + it('should reset session after inactivity timeout', async () => { + config = { + sessionIdGenerator: idGenerator, + sessionStore: store, + maxDuration: 1, + inactivityTimeout: 0.5, + }; + sessionManager = new SessionManager(config); + + sessionManager.getSessionId(); // Starts session-1 + await wait(600); + const sessionId = sessionManager.getSessionId(); + assert.strictEqual(sessionId, 'session-2'); + }); + + it('should extend session life when there is activity', async () => { + config = { + sessionIdGenerator: idGenerator, + sessionStore: store, + maxDuration: 1, + inactivityTimeout: 0.5, + }; + sessionManager = new SessionManager(config); + + sessionManager.getSessionId(); // Starts session-1 + + await wait(300); + let sessionId = sessionManager.getSessionId(); // extends session-1 + assert.strictEqual(sessionId, 'session-1'); + + await wait(600); + sessionId = sessionManager.getSessionId(); + assert.strictEqual(sessionId, 'session-2'); + }); + + it('should notify observers when session starts and ends', () => { + sessionManager = new SessionManager(config); + sessionManager.addObserver(observer); + + sessionManager.getSessionId(); // Starts session-1 + sessionManager.getSessionId(); // Reuse session-1 + assert.strictEqual(observer.startedSessions.length, 1); + assert.strictEqual(observer.endedSessions.length, 0); + + sessionManager['resetSession'](); // Force reset + assert.strictEqual(observer.startedSessions.length, 2); + assert.strictEqual(observer.endedSessions.length, 1); + }); + + it('should persist session in store', () => { + sessionManager = new SessionManager(config); + const sessionId = sessionManager.getSessionId(); + assert.strictEqual(store.get()?.id, sessionId); + }); +}); + +function wait(timeInMs: number) { + return new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, timeInMs); + }); +}