UNPKG

@multiplayer-app/session-recorder-node

Version:

Multiplayer Fullstack Session Recorder for Node.js

274 lines (232 loc) 7.82 kB
import { SessionType, SessionRecorderSdk, SessionRecorderIdGenerator, ATTR_MULTIPLAYER_SESSION_RECORDER_VERSION, MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH, } from '@multiplayer-app/session-recorder-common' import { ApiService } from './services/api.service' import { ISession } from './types' import { getFormattedDate } from './helper' import { SESSION_RECORDER_VERSION } from './config' export class SessionRecorder { private _isInitialized = false private _shortSessionId: string | boolean = false private _traceIdGenerator: SessionRecorderIdGenerator | undefined private _sessionType: SessionType = SessionType.PLAIN private _sessionState: 'STARTED' | 'STOPPED' | 'PAUSED' = 'STOPPED' private _apiService = new ApiService() private _sessionShortIdGenerator = SessionRecorderSdk.getIdGenerator(MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH) private _resourceAttributes: object = {} /** * Initialize session recorder with default or custom configurations */ constructor() { } /** * @description Initialize the session recorder * @param apiKey - multiplayer otlp key * @param traceIdGenerator - multiplayer compatible trace id generator */ public init(config: { apiKey: string, traceIdGenerator: SessionRecorderIdGenerator, resourceAttributes?: object, generateSessionShortIdLocally?: boolean | (() => string) }): void { this._resourceAttributes = config.resourceAttributes || { [ATTR_MULTIPLAYER_SESSION_RECORDER_VERSION]: SESSION_RECORDER_VERSION } this._isInitialized = true if (typeof config.generateSessionShortIdLocally === 'function') { this._sessionShortIdGenerator = config.generateSessionShortIdLocally } if (!config?.apiKey?.length) { throw new Error('Api key not provided') } if (!config?.traceIdGenerator?.setSessionId) { throw new Error('Incompatible trace id generator') } this._traceIdGenerator = config.traceIdGenerator this._apiService.init({ apiKey: config.apiKey }) } /** * @description Start a new session * @param {SessionType} SessionType - the type of session to start * @param {ISession} [sessionPayload] - session metadata * @returns {Promise<void>} */ public async start( sessionType: SessionType, sessionPayload?: Omit<ISession, '_id'> ): Promise<void> { if (!this._isInitialized) { throw new Error( 'Configuration not initialized. Call init() before performing any actions.', ) } if ( sessionPayload?.shortId && sessionPayload?.shortId?.length !== MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH ) { throw new Error('Invalid short session id') } sessionPayload = sessionPayload || {} if (this._sessionState !== 'STOPPED') { throw new Error('Session should be ended before starting new one.') } this._sessionType = sessionType let session: ISession sessionPayload.name = sessionPayload.name ? sessionPayload.name : `Session on ${getFormattedDate(Date.now())}` sessionPayload.resourceAttributes = { ...this._resourceAttributes, ...sessionPayload.resourceAttributes } if (this._sessionType === SessionType.CONTINUOUS) { session = await this._apiService.startContinuousSession(sessionPayload) } else { session = await this._apiService.startSession(sessionPayload) } this._shortSessionId = session.shortId as string (this._traceIdGenerator as SessionRecorderIdGenerator).setSessionId( this._shortSessionId, this._sessionType ) this._sessionState = 'STARTED' } /** * @description Save the continuous session * @param {String} [reason] * @returns {Promise<void>} */ static async save(reason?: string) { SessionRecorderSdk.saveContinuousSession(reason) } /** * @description Save the continuous session * @param {ISession} [sessionData] * @returns {Promise<void>} */ public async save( sessionData?: ISession ): Promise<void> { try { if (!this._isInitialized) { throw new Error( 'Configuration not initialized. Call init() before performing any actions.', ) } if ( this._sessionState === 'STOPPED' || typeof this._shortSessionId !== 'string' ) { throw new Error('Session should be active or paused') } if (this._sessionType !== SessionType.CONTINUOUS) { throw new Error('Invalid session type') } await this._apiService.saveContinuousSession( this._shortSessionId, { ...(sessionData || {}), name: sessionData?.name ? sessionData.name : `Session on ${getFormattedDate(Date.now())}` }, ) } catch (e) { throw e } } /** * @description Stop the current session with an optional comment * @param {ISession} [sessionData] - user-provided comment to include in session metadata * @returns {Promise<void>} */ public async stop( sessionData?: ISession ): Promise<void> { try { if (!this._isInitialized) { throw new Error( 'Configuration not initialized. Call init() before performing any actions.', ) } if ( this._sessionState === 'STOPPED' || typeof this._shortSessionId !== 'string' ) { throw new Error('Session should be active or paused') } if (this._sessionType !== SessionType.PLAIN) { throw new Error('Invalid session type') } await this._apiService.stopSession( this._shortSessionId, sessionData || {}, ) } catch (e) { throw e } finally { (this._traceIdGenerator as SessionRecorderIdGenerator).setSessionId('') this._shortSessionId = false this._sessionState = 'STOPPED' } } /** * @description Cancel the current session * @returns {Promise<void>} */ public async cancel(): Promise<void> { try { if (!this._isInitialized) { throw new Error( 'Configuration not initialized. Call init() before performing any actions.', ) } if ( this._sessionState === 'STOPPED' || typeof this._shortSessionId !== 'string' ) { throw new Error('Session should be active or paused') } if (this._sessionType === SessionType.CONTINUOUS) { await this._apiService.stopContinuousSession(this._shortSessionId) } else if (this._sessionType === SessionType.PLAIN) { await this._apiService.cancelSession(this._shortSessionId) } } catch (e) { throw e } finally { (this._traceIdGenerator as SessionRecorderIdGenerator).setSessionId('') this._shortSessionId = false this._sessionState = 'STOPPED' } } /** * @description Check if continuous session should be started/stopped automatically * @param {ISession} [sessionPayload] * @returns {Promise<void>} */ public async checkRemoteContinuousSession( sessionPayload?: Omit<ISession, '_id' | 'shortId'> ): Promise<void> { if (!this._isInitialized) { throw new Error( 'Configuration not initialized. Call init() before performing any actions.', ) } sessionPayload = sessionPayload || {} sessionPayload.resourceAttributes = { ...(sessionPayload.resourceAttributes || {}), ...this._resourceAttributes, } const { state } = await this._apiService.checkRemoteSession(sessionPayload) if (state == 'START' && this._sessionState !== 'STARTED') { await this.start(SessionType.CONTINUOUS, sessionPayload) } else if (state == 'STOP' && this._sessionState !== 'STOPPED') { await this.stop() } } }