+
+ 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);
+ });
+}