diff --git a/examples/session/otel.js b/examples/session/otel.js index 3a4f62929..1542e2f71 100644 --- a/examples/session/otel.js +++ b/examples/session/otel.js @@ -3,7 +3,7 @@ import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xm import { ConsoleLogRecordExporter, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs'; import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { configureWebSDK, getResource, getSessionManager } from '@opentelemetry/web-configuration'; -import { WebLocalStorage } from '@opentelemetry/web-common'; +import { DefaultSessionLifecycle, WebLocalStorage } from '@opentelemetry/web-common'; const sessionObserver = { onSessionStarted: (session) => { @@ -21,6 +21,8 @@ const customIdGenerator = { } } +const sessionLifecycle = new DefaultSessionLifecycle(); + configureWebSDK( { logRecordProcessors: [new SimpleLogRecordProcessor(new ConsoleLogRecordExporter())], @@ -29,11 +31,10 @@ configureWebSDK( serviceName: 'My app', }), sessionIdProvider: getSessionManager({ - maxDuration: 20000, - idleTimeout: 10000, sessionStorage: new WebLocalStorage(), sessionIdGenerator: customIdGenerator, - sessionObserver: sessionObserver + sessionObserver: sessionObserver, + sessionLifecycle: new DefaultSessionLifecycle(20000, 10000) }) }, [ diff --git a/packages/web-common/src/DefaultSessionLifecycle.ts b/packages/web-common/src/DefaultSessionLifecycle.ts new file mode 100644 index 000000000..ee524b914 --- /dev/null +++ b/packages/web-common/src/DefaultSessionLifecycle.ts @@ -0,0 +1,116 @@ + +/* + * 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 { SessionLifecycle } from "./types/SessionLifecycle"; + +export class DefaultSessionLifecycle implements SessionLifecycle { + private _maxDuration?: number; + private _maxDurationTimeoutId?: ReturnType; + private _inactivityTimeout?: number; + private _inactivityTimeoutId?: ReturnType; + private _lastActivityTimestamp: number = 0; + private _inactivityResetDelay: number = 5000; // minimum time between activities before timer is reset + private _timeoutListener: (() => void) | undefined; + private _session: Session | undefined; + + constructor(maxDuration?: number, inactivityTimeout?: number) { + this._maxDuration = maxDuration; + this._inactivityTimeout = inactivityTimeout; + } + + onSessionEnded(listener: () => void): void { + this._timeoutListener = listener; + } + + public start(session: Session) { + this._session = session; + this.resetTimers(); + } + + public clear() { + if (this._inactivityTimeoutId) { + clearTimeout(this._inactivityTimeoutId); + this._inactivityTimeoutId = undefined; + } + + if (this._maxDurationTimeoutId) { + clearTimeout(this._maxDurationTimeoutId); + this._maxDurationTimeoutId = undefined; + } + + this._session = undefined; + } + + public bumpSessionActive() { + if (!this._session) { + return; + } + + if (this._maxDuration && (Date.now() - this._session.startTimestamp) > this._maxDuration) { + this.notifySessionEnded(); + } + + if (this._inactivityTimeout) { + if (Date.now() - this._lastActivityTimestamp > this._inactivityResetDelay) { + this.resetIdleTimer(); + this._lastActivityTimestamp = Date.now(); + } + } + } + + private notifySessionEnded() { + if (this._timeoutListener) { + this._timeoutListener(); + } + } + + private resetTimers() { + if (this._inactivityTimeout) { + this.resetIdleTimer(); + } + if (this._maxDuration) { + this.resetMaxDurationTimer(); + } + } + + private resetIdleTimer() { + if (this._inactivityTimeoutId) { + clearTimeout(this._inactivityTimeoutId); + } + + this._inactivityTimeoutId = setTimeout(() => { + this.notifySessionEnded(); + }, this._inactivityTimeout); + } + + private resetMaxDurationTimer() { + if (!this._maxDuration || !this._session) { + return + } + + if (this._maxDurationTimeoutId) { + clearTimeout(this._maxDurationTimeoutId); + } + + const timeoutIn = this._maxDuration - (Date.now() - this._session?.startTimestamp); + + this._maxDurationTimeoutId = setTimeout(() => { + this.notifySessionEnded(); + }, timeoutIn); + } +} diff --git a/packages/web-common/src/SessionManager.ts b/packages/web-common/src/SessionManager.ts index b16727e93..8c9cc5243 100644 --- a/packages/web-common/src/SessionManager.ts +++ b/packages/web-common/src/SessionManager.ts @@ -20,12 +20,12 @@ import { SessionProvider } from "./types/SessionProvider"; import { SessionObserver } from "./types/SessionObserver"; import { SessionStorage } from "./types/SessionStorage"; import { SessionPublisher } from "./types/SessionPublisher"; +import { SessionLifecycle } from "./types/SessionLifecycle"; export interface SessionManagerConfig { sessionIdGenerator: SessionIdGenerator; sessionStorage: SessionStorage; - maxDuration?: number; - idleTimeout?: number; + sessionLifecycle: SessionLifecycle } export class SessionManager implements SessionProvider, SessionPublisher { @@ -33,28 +33,23 @@ export class SessionManager implements SessionProvider, SessionPublisher { private _idGenerator: SessionIdGenerator; private _storage: SessionStorage; 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 between activities before timer is reset + private _lifecycle: SessionLifecycle; constructor(config: SessionManagerConfig) { this._idGenerator = config.sessionIdGenerator; this._storage = config.sessionStorage; - this._maxDuration = config.maxDuration; - this._inactivityTimeout = config.idleTimeout; - this._observers = []; + this._lifecycle = config.sessionLifecycle; + this._lifecycle.onSessionEnded(() => { + this.resetSession(); + }); + this._session = this._storage.get(); if (!this._session) { this._session = this.startSession(); } - this.resetTimers(); + this._lifecycle.start(this._session); } addObserver(observer: SessionObserver): void { @@ -66,16 +61,7 @@ export class SessionManager implements SessionProvider, SessionPublisher { return null; } - if (this._maxDuration && (Date.now() - this._session.startTimestamp) > this._maxDuration) { - this.resetSession(); - } - - if (this._inactivityTimeout) { - if (Date.now() - this._lastActivityTimestamp > this._inactivityResetDelay) { - this.resetIdleTimer(); - this._lastActivityTimestamp = Date.now(); - } - } + this._lifecycle.bumpSessionActive(); return this._session.id; } @@ -113,50 +99,12 @@ export class SessionManager implements SessionProvider, SessionPublisher { } this._session = undefined; - if (this._inactivityTimeoutId) { - clearTimeout(this._inactivityTimeoutId); - this._inactivityTimeoutId = undefined; - } + this._lifecycle.clear(); } private resetSession(): void { this.endSession(); this.startSession(); - this.resetTimers(); - } - - private resetTimers() { - if (this._inactivityTimeout) { - this.resetIdleTimer(); - } - if (this._maxDuration) { - this.resetMaxDurationTimer(); - } - } - - private resetIdleTimer() { - if (this._inactivityTimeoutId) { - clearTimeout(this._inactivityTimeoutId); - } - - this._inactivityTimeoutId = setTimeout(() => { - this.resetSession(); - }, this._inactivityTimeout); - } - - private resetMaxDurationTimer() { - if (!this._maxDuration || !this._session) { - return - } - - if (this._maxDurationTimeoutId) { - clearTimeout(this._maxDurationTimeoutId); - } - - const timeoutIn = this._maxDuration - (Date.now() - this._session?.startTimestamp); - - this._maxDurationTimeoutId = setTimeout(() => { - this.resetSession(); - }, timeoutIn); + this._lifecycle.start(this._session!); } } diff --git a/packages/web-common/src/index.ts b/packages/web-common/src/index.ts index 953bcccd4..9d4f702aa 100644 --- a/packages/web-common/src/index.ts +++ b/packages/web-common/src/index.ts @@ -26,3 +26,5 @@ export { SessionManager } from './SessionManager'; export { WebSessionStorage } from './WebSessionStorage'; export { WebLocalStorage } from './WebLocalStorage'; export { DefaultIdGenerator } from './DefaultIdGenerator'; +export { SessionLifecycle } from './types/SessionLifecycle'; +export { DefaultSessionLifecycle } from './DefaultSessionLifecycle'; diff --git a/packages/web-common/src/types/SessionLifecycle.ts b/packages/web-common/src/types/SessionLifecycle.ts new file mode 100644 index 000000000..b54c07c2b --- /dev/null +++ b/packages/web-common/src/types/SessionLifecycle.ts @@ -0,0 +1,32 @@ + +/* + * 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 SessionLifecycle { + /** Starts tracking a new session */ + start(session: Session): void; + + /** Stop tracking the current session, reset to a clear state */ + clear(): void; + + /** Notifies this class that the session is currently active. */ + bumpSessionActive(): void; + + /** Register a listener function to call when session is ended. */ + onSessionEnded(listener: () => void): void; +} diff --git a/packages/web-configuration/src/utils.ts b/packages/web-configuration/src/utils.ts index 4a40428f2..df549aa1f 100644 --- a/packages/web-configuration/src/utils.ts +++ b/packages/web-configuration/src/utils.ts @@ -30,7 +30,7 @@ import { BatchLogRecordProcessor, LoggerProvider, LoggerProviderConfig, LogRecor import { BatchSpanProcessor, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base"; import { WebTracerConfig, WebTracerProvider } from "@opentelemetry/sdk-trace-web"; import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import { DefaultIdGenerator, SessionIdGenerator, SessionIdProvider, SessionLogRecordProcessor, SessionManager, SessionObserver, SessionSpanProcessor, SessionStorage, WebSessionStorage } from "@opentelemetry/web-common"; +import { DefaultIdGenerator, DefaultSessionLifecycle, SessionIdGenerator, SessionIdProvider, SessionLifecycle, SessionLogRecordProcessor, SessionManager, SessionObserver, SessionSpanProcessor, SessionStorage, WebSessionStorage } from "@opentelemetry/web-common"; import { browserDetector } from '@opentelemetry/opentelemetry-browser-detector'; export interface ResourceConfiguration { @@ -42,9 +42,8 @@ export interface ResourceConfiguration { export interface SessionConfiguration { sessionIdGenerator?: SessionIdGenerator; sessionStorage?: SessionStorage; - maxDuration?: number; - idleTimeout?: number; - sessionObserver: SessionObserver + sessionObserver: SessionObserver; + sessionLifecycle: SessionLifecycle } export interface TraceSDKConfiguration { @@ -112,12 +111,12 @@ export function getResource(config?: ResourceConfiguration): Resource { export function getSessionManager(config?: SessionConfiguration): SessionManager { const idGenerator = config?.sessionIdGenerator || new DefaultIdGenerator(); const storage = config?.sessionStorage || new WebSessionStorage(); + const lifecycle = config?.sessionLifecycle || new DefaultSessionLifecycle(); const sessionManager = new SessionManager({ sessionIdGenerator: idGenerator, sessionStorage: storage, - maxDuration: config?.maxDuration, - idleTimeout: config?.idleTimeout + sessionLifecycle: lifecycle }); if (config?.sessionObserver) {