@multiplayer-app/session-recorder-node 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2024 Multiplayer Software, Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Session Recorder
2
+
3
+ The Multiplayer **Session Recorder** is a powerful tool that offers deep session replays with insights spanning frontend screens, platform traces, metrics, and logs. It helps your team pinpoint and resolve bugs faster by providing a complete picture of your backend system architecture. No more wasted hours combing through APM data; the Multiplayer Session Recorder does it all in one place.
4
+
5
+ ## Key Features
6
+
7
+ - **Reduced Inefficiencies**: Effortlessly capture the exact steps to reproduce an issue along with backend data in one click. No more hunting through scattered documentation, APM data, logs, or traces.
8
+ - **Faster Cross-Team Alignment**: Engineers can share session links containing all relevant information, eliminating the need for long tickets or clarifying issues through back-and-forth communication.
9
+ - **Uninterrupted Deep Work**: All system information—from architecture diagrams to API designs—is consolidated in one place. Minimize context switching and stay focused on what matters.
10
+
11
+ ## Getting Started
12
+
13
+ ### Installation
14
+
15
+ You can install the Session Recorder using npm or yarn:
16
+
17
+ ```bash
18
+ npm install @multiplayer-app/session-recorder-node @multiplayer-app/session-recorder-opentelemetry
19
+ # or
20
+ yarn add @multiplayer-app/session-recorder-node @multiplayer-app/session-recorder-opentelemetry
21
+ ```
22
+
23
+ ### Basic Setup
24
+
25
+ To initialize the Session Recorder in your application, follow the steps below.
26
+
27
+ #### Import the Session Recorder
28
+
29
+ ```javascript
30
+ import SessionRecorder from '@multiplayer-app/session-recorder-node'
31
+ // Multiplayer trace id generator which is used during opentelemetry initialization
32
+ import { idGenerator } from './opentelemetry'
33
+ ```
34
+
35
+ #### Initialization
36
+
37
+ Use the following code to initialize the session recorder with your application details:
38
+
39
+ ```javascript
40
+ SessionRecorder.init(
41
+ '{YOUR_API_KEY}',
42
+ idGenerator,
43
+ {
44
+ resourceAttributes: {
45
+ serviceName: '{YOUR_APPLICATION_NAME}'
46
+ version: '{YOUR_APPLICATION_VERSION}',
47
+ environment: '{YOUR_APPLICATION_ENVIRONMENT}',
48
+ }
49
+ }
50
+ )
51
+ ```
52
+
53
+ Replace the placeholders with your application’s version, name, environment, and API key.
54
+
55
+ ## Dependencies
56
+
57
+ This library relies on the following packages:
58
+
59
+ - **[OpenTelemetry](https://opentelemetry.io/)**: Used to capture backend traces, metrics, and logs that integrate seamlessly with the session replays for comprehensive debugging.
60
+
61
+ ## Example Usage
62
+
63
+ ```javascript
64
+ import SessionRecorder from '@multiplayer-app/session-recorder-node'
65
+ import { SessionType } from '@multiplayer-app/session-recorder-opentelemetry'
66
+ // Session recorder trace id generator which is used during opentelemetry initialization
67
+ import { idGenerator } from './opentelemetry'
68
+
69
+ SessionRecorder.init(
70
+ '{YOUR_API_KEY}',
71
+ idGenerator,
72
+ {
73
+ resourceAttributes: {
74
+ serviceName: '{YOUR_APPLICATION_NAME}'
75
+ version: '{YOUR_APPLICATION_VERSION}',
76
+ environment: '{YOUR_APPLICATION_ENVIRONMENT}',
77
+ }
78
+ }
79
+ )
80
+
81
+ // ...
82
+
83
+ await SessionRecorder.start(
84
+ SessionType.PLAIN,
85
+ {
86
+ name: 'This is test session',
87
+ sessionAttributes: {
88
+ accountId: '687e2c0d3ec8ef6053e9dc97',
89
+ accountName: 'Acme Corporation'
90
+ }
91
+ }
92
+ )
93
+
94
+ // do something here
95
+
96
+ await SessionRecorder.stop()
97
+
98
+ ```
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,QAAwE,CAAA;AAE7G,eAAO,MAAM,wBAAwB,QAAsD,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { SessionRecorder } from './sessionRecorder';
2
+ export declare const sessionRecorder: SessionRecorder;
3
+ export * from '@multiplayer-app/session-recorder-common';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEnD,eAAO,MAAM,eAAe,iBAAwB,CAAA;AAEpD,cAAc,0CAA0C,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.sessionRecorder = void 0;
18
+ const sessionRecorder_1 = require("./sessionRecorder");
19
+ exports.sessionRecorder = new sessionRecorder_1.SessionRecorder();
20
+ __exportStar(require("@multiplayer-app/session-recorder-common"), exports);
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAmD;AAEtC,QAAA,eAAe,GAAG,IAAI,iCAAe,EAAE,CAAA;AAEpD,2EAAwD"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.service.d.ts","sourceRoot":"","sources":["../../src/services/api.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAEnC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACxC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACvC,IAAI,CAAC,EAAE;QACL,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACd,EAAE,CAAA;CACJ;AAED,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,CAAC,EAAE;QAClB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,OAAO,CAAC,EAAE,MAAM,CAAA;KACjB,CAAC;CACH;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAkB;;IAQhC;;;OAGG;IACI,IAAI,CAAC,MAAM,EAAE,gBAAgB;IAIpC;;;OAGG;IACI,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC;IAItD;;;;OAIG;IACG,YAAY,CAChB,WAAW,EAAE,mBAAmB,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,QAAQ,CAAC;IASpB;;;;OAIG;IACG,WAAW,CACf,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,kBAAkB,GAC9B,OAAO,CAAC,GAAG,CAAC;IAQf;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAOpD;;;;OAIG;IACG,sBAAsB,CAC1B,WAAW,EAAE,mBAAmB,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,GAAG,CAAC;IASf;;;;;OAKG;IACG,qBAAqB,CACzB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,mBAAmB,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,GAAG,CAAC;IASf;;;OAGG;IACG,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAO5D;;OAEG;IACG,kBAAkB,CACtB,WAAW,EAAE,mBAAmB,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;KAAE,CAAC;IASvC;;;;;;OAMG;YACW,WAAW;CAsC1B"}
@@ -0,0 +1,64 @@
1
+ import { SessionType, SessionRecorderIdGenerator } from '@multiplayer-app/session-recorder-common';
2
+ import { ISession } from './types';
3
+ export declare class SessionRecorder {
4
+ private _isInitialized;
5
+ private _shortSessionId;
6
+ private _traceIdGenerator;
7
+ private _sessionType;
8
+ private _sessionState;
9
+ private _apiService;
10
+ private _sessionShortIdGenerator;
11
+ private _resourceAttributes;
12
+ /**
13
+ * Initialize session recorder with default or custom configurations
14
+ */
15
+ constructor();
16
+ /**
17
+ * @description Initialize the session recorder
18
+ * @param apiKey - multiplayer otlp key
19
+ * @param traceIdGenerator - multiplayer compatible trace id generator
20
+ */
21
+ init(config: {
22
+ apiKey: string;
23
+ traceIdGenerator: SessionRecorderIdGenerator;
24
+ resourceAttributes?: object;
25
+ generateSessionShortIdLocally?: boolean | (() => string);
26
+ }): void;
27
+ /**
28
+ * @description Start a new session
29
+ * @param {SessionType} SessionType - the type of session to start
30
+ * @param {ISession} [sessionPayload] - session metadata
31
+ * @returns {Promise<void>}
32
+ */
33
+ start(sessionType: SessionType, sessionPayload?: Omit<ISession, '_id'>): Promise<void>;
34
+ /**
35
+ * @description Save the continuous session
36
+ * @param {String} [reason]
37
+ * @returns {Promise<void>}
38
+ */
39
+ static save(reason?: string): Promise<void>;
40
+ /**
41
+ * @description Save the continuous session
42
+ * @param {ISession} [sessionData]
43
+ * @returns {Promise<void>}
44
+ */
45
+ save(sessionData?: ISession): Promise<void>;
46
+ /**
47
+ * @description Stop the current session with an optional comment
48
+ * @param {ISession} [sessionData] - user-provided comment to include in session metadata
49
+ * @returns {Promise<void>}
50
+ */
51
+ stop(sessionData?: ISession): Promise<void>;
52
+ /**
53
+ * @description Cancel the current session
54
+ * @returns {Promise<void>}
55
+ */
56
+ cancel(): Promise<void>;
57
+ /**
58
+ * @description Check if continuous session should be started/stopped automatically
59
+ * @param {ISession} [sessionPayload]
60
+ * @returns {Promise<void>}
61
+ */
62
+ checkRemoteContinuousSession(sessionPayload?: Omit<ISession, '_id' | 'shortId'>): Promise<void>;
63
+ }
64
+ //# sourceMappingURL=sessionRecorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionRecorder.d.ts","sourceRoot":"","sources":["../src/sessionRecorder.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAEX,0BAA0B,EAG3B,MAAM,0CAA0C,CAAA;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAIlC,qBAAa,eAAe;IAC1B,OAAO,CAAC,cAAc,CAAQ;IAE9B,OAAO,CAAC,eAAe,CAA0B;IAEjD,OAAO,CAAC,iBAAiB,CAAwC;IACjE,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,aAAa,CAA8C;IACnE,OAAO,CAAC,WAAW,CAAmB;IACtC,OAAO,CAAC,wBAAwB,CAAqF;IAErH,OAAO,CAAC,mBAAmB,CAAa;IAExC;;OAEG;;IAGH;;;;OAIG;IACI,IAAI,CAAC,MAAM,EAAE;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB,EAAE,0BAA0B,CAAC;QAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,6BAA6B,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,CAAA;KACzD,GAAG,IAAI;IAsBR;;;;;OAKG;IACU,KAAK,CAChB,WAAW,EAAE,WAAW,EACxB,cAAc,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,GACrC,OAAO,CAAC,IAAI,CAAC;IAiDhB;;;;OAIG;WACU,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM;IAIjC;;;;OAIG;IACU,IAAI,CACf,WAAW,CAAC,EAAE,QAAQ,GACrB,OAAO,CAAC,IAAI,CAAC;IAiChB;;;;OAIG;IACU,IAAI,CACf,WAAW,CAAC,EAAE,QAAQ,GACrB,OAAO,CAAC,IAAI,CAAC;IAiChB;;;OAGG;IACU,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IA8BpC;;;;OAIG;IACU,4BAA4B,CACvC,cAAc,CAAC,EAAE,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,CAAC,GACjD,OAAO,CAAC,IAAI,CAAC;CAsBjB"}
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionRecorder = void 0;
4
+ const session_recorder_common_1 = require("@multiplayer-app/session-recorder-common");
5
+ const api_service_1 = require("./services/api.service");
6
+ const helper_1 = require("./helper");
7
+ const config_1 = require("./config");
8
+ class SessionRecorder {
9
+ /**
10
+ * Initialize session recorder with default or custom configurations
11
+ */
12
+ constructor() {
13
+ this._isInitialized = false;
14
+ this._shortSessionId = false;
15
+ this._sessionType = session_recorder_common_1.SessionType.PLAIN;
16
+ this._sessionState = 'STOPPED';
17
+ this._apiService = new api_service_1.ApiService();
18
+ this._sessionShortIdGenerator = session_recorder_common_1.SessionRecorderSdk.getIdGenerator(session_recorder_common_1.MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH);
19
+ this._resourceAttributes = {};
20
+ }
21
+ /**
22
+ * @description Initialize the session recorder
23
+ * @param apiKey - multiplayer otlp key
24
+ * @param traceIdGenerator - multiplayer compatible trace id generator
25
+ */
26
+ init(config) {
27
+ var _a, _b;
28
+ this._resourceAttributes = config.resourceAttributes || {
29
+ [session_recorder_common_1.ATTR_MULTIPLAYER_SESSION_RECORDER_VERSION]: config_1.SESSION_RECORDER_VERSION
30
+ };
31
+ this._isInitialized = true;
32
+ if (typeof config.generateSessionShortIdLocally === 'function') {
33
+ this._sessionShortIdGenerator = config.generateSessionShortIdLocally;
34
+ }
35
+ if (!((_a = config === null || config === void 0 ? void 0 : config.apiKey) === null || _a === void 0 ? void 0 : _a.length)) {
36
+ throw new Error('Api key not provided');
37
+ }
38
+ if (!((_b = config === null || config === void 0 ? void 0 : config.traceIdGenerator) === null || _b === void 0 ? void 0 : _b.setSessionId)) {
39
+ throw new Error('Incompatible trace id generator');
40
+ }
41
+ this._traceIdGenerator = config.traceIdGenerator;
42
+ this._apiService.init({ apiKey: config.apiKey });
43
+ }
44
+ /**
45
+ * @description Start a new session
46
+ * @param {SessionType} SessionType - the type of session to start
47
+ * @param {ISession} [sessionPayload] - session metadata
48
+ * @returns {Promise<void>}
49
+ */
50
+ async start(sessionType, sessionPayload) {
51
+ var _a;
52
+ if (!this._isInitialized) {
53
+ throw new Error('Configuration not initialized. Call init() before performing any actions.');
54
+ }
55
+ if ((sessionPayload === null || sessionPayload === void 0 ? void 0 : sessionPayload.shortId)
56
+ && ((_a = sessionPayload === null || sessionPayload === void 0 ? void 0 : sessionPayload.shortId) === null || _a === void 0 ? void 0 : _a.length) !== session_recorder_common_1.MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH) {
57
+ throw new Error('Invalid short session id');
58
+ }
59
+ sessionPayload = sessionPayload || {};
60
+ if (this._sessionState !== 'STOPPED') {
61
+ throw new Error('Session should be ended before starting new one.');
62
+ }
63
+ this._sessionType = sessionType;
64
+ let session;
65
+ sessionPayload.name = sessionPayload.name
66
+ ? sessionPayload.name
67
+ : `Session on ${(0, helper_1.getFormattedDate)(Date.now())}`;
68
+ sessionPayload.resourceAttributes = {
69
+ ...this._resourceAttributes,
70
+ ...sessionPayload.resourceAttributes
71
+ };
72
+ if (this._sessionType === session_recorder_common_1.SessionType.CONTINUOUS) {
73
+ session = await this._apiService.startContinuousSession(sessionPayload);
74
+ }
75
+ else {
76
+ session = await this._apiService.startSession(sessionPayload);
77
+ }
78
+ this._shortSessionId = session.shortId;
79
+ this._traceIdGenerator.setSessionId(this._shortSessionId, this._sessionType);
80
+ this._sessionState = 'STARTED';
81
+ }
82
+ /**
83
+ * @description Save the continuous session
84
+ * @param {String} [reason]
85
+ * @returns {Promise<void>}
86
+ */
87
+ static async save(reason) {
88
+ session_recorder_common_1.SessionRecorderSdk.saveContinuousSession(reason);
89
+ }
90
+ /**
91
+ * @description Save the continuous session
92
+ * @param {ISession} [sessionData]
93
+ * @returns {Promise<void>}
94
+ */
95
+ async save(sessionData) {
96
+ try {
97
+ if (!this._isInitialized) {
98
+ throw new Error('Configuration not initialized. Call init() before performing any actions.');
99
+ }
100
+ if (this._sessionState === 'STOPPED'
101
+ || typeof this._shortSessionId !== 'string') {
102
+ throw new Error('Session should be active or paused');
103
+ }
104
+ if (this._sessionType !== session_recorder_common_1.SessionType.CONTINUOUS) {
105
+ throw new Error('Invalid session type');
106
+ }
107
+ await this._apiService.saveContinuousSession(this._shortSessionId, {
108
+ ...(sessionData || {}),
109
+ name: (sessionData === null || sessionData === void 0 ? void 0 : sessionData.name)
110
+ ? sessionData.name
111
+ : `Session on ${(0, helper_1.getFormattedDate)(Date.now())}`
112
+ });
113
+ }
114
+ catch (e) {
115
+ throw e;
116
+ }
117
+ }
118
+ /**
119
+ * @description Stop the current session with an optional comment
120
+ * @param {ISession} [sessionData] - user-provided comment to include in session metadata
121
+ * @returns {Promise<void>}
122
+ */
123
+ async stop(sessionData) {
124
+ try {
125
+ if (!this._isInitialized) {
126
+ throw new Error('Configuration not initialized. Call init() before performing any actions.');
127
+ }
128
+ if (this._sessionState === 'STOPPED'
129
+ || typeof this._shortSessionId !== 'string') {
130
+ throw new Error('Session should be active or paused');
131
+ }
132
+ if (this._sessionType !== session_recorder_common_1.SessionType.PLAIN) {
133
+ throw new Error('Invalid session type');
134
+ }
135
+ await this._apiService.stopSession(this._shortSessionId, sessionData || {});
136
+ }
137
+ catch (e) {
138
+ throw e;
139
+ }
140
+ finally {
141
+ this._traceIdGenerator.setSessionId('');
142
+ this._shortSessionId = false;
143
+ this._sessionState = 'STOPPED';
144
+ }
145
+ }
146
+ /**
147
+ * @description Cancel the current session
148
+ * @returns {Promise<void>}
149
+ */
150
+ async cancel() {
151
+ try {
152
+ if (!this._isInitialized) {
153
+ throw new Error('Configuration not initialized. Call init() before performing any actions.');
154
+ }
155
+ if (this._sessionState === 'STOPPED'
156
+ || typeof this._shortSessionId !== 'string') {
157
+ throw new Error('Session should be active or paused');
158
+ }
159
+ if (this._sessionType === session_recorder_common_1.SessionType.CONTINUOUS) {
160
+ await this._apiService.stopContinuousSession(this._shortSessionId);
161
+ }
162
+ else if (this._sessionType === session_recorder_common_1.SessionType.PLAIN) {
163
+ await this._apiService.cancelSession(this._shortSessionId);
164
+ }
165
+ }
166
+ catch (e) {
167
+ throw e;
168
+ }
169
+ finally {
170
+ this._traceIdGenerator.setSessionId('');
171
+ this._shortSessionId = false;
172
+ this._sessionState = 'STOPPED';
173
+ }
174
+ }
175
+ /**
176
+ * @description Check if continuous session should be started/stopped automatically
177
+ * @param {ISession} [sessionPayload]
178
+ * @returns {Promise<void>}
179
+ */
180
+ async checkRemoteContinuousSession(sessionPayload) {
181
+ if (!this._isInitialized) {
182
+ throw new Error('Configuration not initialized. Call init() before performing any actions.');
183
+ }
184
+ sessionPayload = sessionPayload || {};
185
+ sessionPayload.resourceAttributes = {
186
+ ...(sessionPayload.resourceAttributes || {}),
187
+ ...this._resourceAttributes,
188
+ };
189
+ const { state } = await this._apiService.checkRemoteSession(sessionPayload);
190
+ if (state == 'START' && this._sessionState !== 'STARTED') {
191
+ await this.start(session_recorder_common_1.SessionType.CONTINUOUS, sessionPayload);
192
+ }
193
+ else if (state == 'STOP' && this._sessionState !== 'STOPPED') {
194
+ await this.stop();
195
+ }
196
+ }
197
+ }
198
+ exports.SessionRecorder = SessionRecorder;
199
+ //# sourceMappingURL=sessionRecorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionRecorder.js","sourceRoot":"","sources":["../src/sessionRecorder.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@multiplayer-app/session-recorder-node",
3
+ "version": "0.0.1",
4
+ "description": "Multiplayer Fullstack Session Recorder for Node.js",
5
+ "author": {
6
+ "name": "Multiplayer Software, Inc.",
7
+ "url": "https://www.multiplayer.app"
8
+ },
9
+ "license": "MIT",
10
+ "main": "dist/src/index.js",
11
+ "engines": {
12
+ "node": ">=18",
13
+ "npm": ">=8"
14
+ },
15
+ "keywords": [
16
+ "multiplayer",
17
+ "debugger",
18
+ "platform",
19
+ "platform debugger",
20
+ "session recorder",
21
+ "otlp",
22
+ "fullstack session recorder"
23
+ ],
24
+ "scripts": {
25
+ "lint": "eslint src/**/*.ts --config ../../.eslintrc",
26
+ "preversion": "npm run lint",
27
+ "postversion:skip": "git push && git push --tags",
28
+ "build": "tsc --build tsconfig.json",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "dependencies": {
32
+ "@multiplayer-app/session-recorder-common": "0.0.1",
33
+ "@opentelemetry/api": "^1.9.0",
34
+ "@opentelemetry/core": "^1.29.0",
35
+ "@opentelemetry/otlp-exporter-base": "^0.56.0",
36
+ "@opentelemetry/otlp-transformer": "^0.56.0",
37
+ "@opentelemetry/sdk-trace-base": "^1.29.0",
38
+ "axios": "^1.10.0",
39
+ "to-json-schema": "^0.2.5"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "24.0.12",
43
+ "typescript": "5.8.3"
44
+ }
45
+ }
package/src/config.ts ADDED
@@ -0,0 +1,5 @@
1
+ import pkg from '../package.json';
2
+
3
+ export const MULTIPLAYER_BASE_API_URL = process.env.MULTIPLAYER_BASE_API_URL || 'https://api.multiplayer.app'
4
+
5
+ export const SESSION_RECORDER_VERSION = process.env.SESSION_RECORDER_VERSION || pkg.version
package/src/helper.ts ADDED
@@ -0,0 +1,13 @@
1
+ export const getFormattedDate = (date, options?) => {
2
+ return new Date(date).toLocaleDateString(
3
+ 'en-US',
4
+ options || {
5
+ month: 'short',
6
+ year: 'numeric',
7
+ day: 'numeric',
8
+ hour: 'numeric',
9
+ minute: '2-digit',
10
+ second: '2-digit',
11
+ },
12
+ )
13
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { SessionRecorder } from './sessionRecorder'
2
+
3
+ export const sessionRecorder = new SessionRecorder()
4
+
5
+ export * from '@multiplayer-app/session-recorder-common'
@@ -0,0 +1,203 @@
1
+ import { MULTIPLAYER_BASE_API_URL } from '../config'
2
+ import { ISession } from '../types'
3
+
4
+ export interface ApiServiceConfig {
5
+ apiKey?: string
6
+ exporterApiBaseUrl?: string
7
+ continuousDebugging?: boolean
8
+ }
9
+
10
+ export interface StartSessionRequest {
11
+ name?: string
12
+ resourceAttributes?: Record<string, any>
13
+ sessionAttributes?: Record<string, any>
14
+ tags?: {
15
+ key?: string
16
+ value: string
17
+ }[]
18
+ }
19
+
20
+ export interface StopSessionRequest {
21
+ sessionAttributes?: {
22
+ email?: string
23
+ comment?: string
24
+ },
25
+ }
26
+
27
+ export class ApiService {
28
+ private config: ApiServiceConfig
29
+
30
+ constructor() {
31
+ this.config = {
32
+ exporterApiBaseUrl: MULTIPLAYER_BASE_API_URL,
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Initialize the API service
38
+ * @param config - API service configuration
39
+ */
40
+ public init(config: ApiServiceConfig) {
41
+ this.config = { ...this.config, ...config }
42
+ }
43
+
44
+ /**
45
+ * Update the API service configuration
46
+ * @param config - Partial configuration to update
47
+ */
48
+ public updateConfigs(config: Partial<ApiServiceConfig>) {
49
+ this.config = { ...this.config, ...config }
50
+ }
51
+
52
+ /**
53
+ * Start a new debug session
54
+ * @param requestBody - Session start request data
55
+ * @param signal - Optional AbortSignal for request cancellation
56
+ */
57
+ async startSession(
58
+ requestBody: StartSessionRequest,
59
+ signal?: AbortSignal,
60
+ ): Promise<ISession> {
61
+ return this.makeRequest(
62
+ '/debug-sessions/start',
63
+ 'POST',
64
+ requestBody,
65
+ signal,
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Stop an active debug session
71
+ * @param sessionId - ID of the session to stop
72
+ * @param requestBody - Session stop request data
73
+ */
74
+ async stopSession(
75
+ sessionId: string,
76
+ requestBody: StopSessionRequest,
77
+ ): Promise<any> {
78
+ return this.makeRequest(
79
+ `/debug-sessions/${sessionId}/stop`,
80
+ 'PATCH',
81
+ requestBody,
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Cancel an active session
87
+ * @param sessionId - ID of the session to cancel
88
+ */
89
+ async cancelSession(sessionId: string): Promise<any> {
90
+ return this.makeRequest(
91
+ `/debug-sessions/${sessionId}/cancel`,
92
+ 'DELETE',
93
+ )
94
+ }
95
+
96
+ /**
97
+ * Start a new session
98
+ * @param requestBody - Session start request data
99
+ * @param signal - Optional AbortSignal for request cancellation
100
+ */
101
+ async startContinuousSession(
102
+ requestBody: StartSessionRequest,
103
+ signal?: AbortSignal,
104
+ ): Promise<any> {
105
+ return this.makeRequest(
106
+ '/continuous-debug-sessions/start',
107
+ 'POST',
108
+ requestBody,
109
+ signal,
110
+ )
111
+ }
112
+
113
+ /**
114
+ * Save a continuous session
115
+ * @param sessionId - ID of the session to save
116
+ * @param requestBody - Session save request data
117
+ * @param signal - Optional AbortSignal for request cancellation
118
+ */
119
+ async saveContinuousSession(
120
+ sessionId: string,
121
+ requestBody: StartSessionRequest,
122
+ signal?: AbortSignal,
123
+ ): Promise<any> {
124
+ return this.makeRequest(
125
+ `/continuous-debug-sessions/${sessionId}/save`,
126
+ 'POST',
127
+ requestBody,
128
+ signal,
129
+ )
130
+ }
131
+
132
+ /**
133
+ * Cancel an active debug session
134
+ * @param sessionId - ID of the session to cancel
135
+ */
136
+ async stopContinuousSession(sessionId: string): Promise<any> {
137
+ return this.makeRequest(
138
+ `/continuous-debug-sessions/${sessionId}/cancel`,
139
+ 'DELETE',
140
+ )
141
+ }
142
+
143
+ /**
144
+ * Check debug session should be started remotely
145
+ */
146
+ async checkRemoteSession(
147
+ requestBody: StartSessionRequest,
148
+ signal?: AbortSignal,
149
+ ): Promise<{ state: 'START' | 'STOP' }> {
150
+ return this.makeRequest(
151
+ `/remote-debug-session/check`,
152
+ 'POST',
153
+ requestBody,
154
+ signal,
155
+ )
156
+ }
157
+
158
+ /**
159
+ * Make a request to the session API
160
+ * @param path - API endpoint path (relative to the base URL)
161
+ * @param method - HTTP method (GET, POST, PATCH, etc.)
162
+ * @param body - request payload
163
+ * @param signal - AbortSignal to set request's signal
164
+ */
165
+ private async makeRequest(
166
+ path: string,
167
+ method: string,
168
+ body?: any,
169
+ signal?: AbortSignal,
170
+ ): Promise<any> {
171
+ const url = `${this.config.exporterApiBaseUrl}/v0/radar${path}`
172
+ const params = {
173
+ method,
174
+ body: body ? JSON.stringify(body) : null,
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ ...(this.config.apiKey && { 'X-Api-Key': this.config.apiKey }),
178
+ },
179
+ }
180
+
181
+ try {
182
+ const response = await fetch(url, {
183
+ ...params,
184
+ credentials: 'include',
185
+ signal,
186
+ })
187
+
188
+ if (!response.ok) {
189
+ throw new Error('Network response was not ok: ' + response.statusText)
190
+ }
191
+
192
+ if (response.status === 204) {
193
+ return null
194
+ }
195
+
196
+ return await response.json()
197
+ } catch (error: any) {
198
+ if (error?.name === 'AbortError') {
199
+ throw new Error('Request aborted')
200
+ }
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,273 @@
1
+ import {
2
+ SessionType,
3
+ SessionRecorderSdk,
4
+ SessionRecorderIdGenerator,
5
+ ATTR_MULTIPLAYER_SESSION_RECORDER_VERSION,
6
+ MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH,
7
+ } from '@multiplayer-app/session-recorder-common'
8
+ import { ApiService } from './services/api.service'
9
+ import { ISession } from './types'
10
+ import { getFormattedDate } from './helper'
11
+ import { SESSION_RECORDER_VERSION } from './config'
12
+
13
+ export class SessionRecorder {
14
+ private _isInitialized = false
15
+
16
+ private _shortSessionId: string | boolean = false
17
+
18
+ private _traceIdGenerator: SessionRecorderIdGenerator | undefined
19
+ private _sessionType: SessionType = SessionType.PLAIN
20
+ private _sessionState: 'STARTED' | 'STOPPED' | 'PAUSED' = 'STOPPED'
21
+ private _apiService = new ApiService()
22
+ private _sessionShortIdGenerator = SessionRecorderSdk.getIdGenerator(MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH)
23
+
24
+ private _resourceAttributes: object = {}
25
+
26
+ /**
27
+ * Initialize session recorder with default or custom configurations
28
+ */
29
+ constructor() { }
30
+
31
+ /**
32
+ * @description Initialize the session recorder
33
+ * @param apiKey - multiplayer otlp key
34
+ * @param traceIdGenerator - multiplayer compatible trace id generator
35
+ */
36
+ public init(config: {
37
+ apiKey: string,
38
+ traceIdGenerator: SessionRecorderIdGenerator,
39
+ resourceAttributes?: object,
40
+ generateSessionShortIdLocally?: boolean | (() => string)
41
+ }): void {
42
+ this._resourceAttributes = config.resourceAttributes || {
43
+ [ATTR_MULTIPLAYER_SESSION_RECORDER_VERSION]: SESSION_RECORDER_VERSION
44
+ }
45
+ this._isInitialized = true
46
+
47
+ if (typeof config.generateSessionShortIdLocally === 'function') {
48
+ this._sessionShortIdGenerator = config.generateSessionShortIdLocally
49
+ }
50
+
51
+ if (!config?.apiKey?.length) {
52
+ throw new Error('Api key not provided')
53
+ }
54
+
55
+ if (!config?.traceIdGenerator?.setSessionId) {
56
+ throw new Error('Incompatible trace id generator')
57
+ }
58
+
59
+ this._traceIdGenerator = config.traceIdGenerator
60
+ this._apiService.init({ apiKey: config.apiKey })
61
+ }
62
+
63
+ /**
64
+ * @description Start a new session
65
+ * @param {SessionType} SessionType - the type of session to start
66
+ * @param {ISession} [sessionPayload] - session metadata
67
+ * @returns {Promise<void>}
68
+ */
69
+ public async start(
70
+ sessionType: SessionType,
71
+ sessionPayload?: Omit<ISession, '_id'>
72
+ ): Promise<void> {
73
+ if (!this._isInitialized) {
74
+ throw new Error(
75
+ 'Configuration not initialized. Call init() before performing any actions.',
76
+ )
77
+ }
78
+
79
+ if (
80
+ sessionPayload?.shortId
81
+ && sessionPayload?.shortId?.length !== MULTIPLAYER_TRACE_DEBUG_SESSION_SHORT_ID_LENGTH
82
+ ) {
83
+ throw new Error('Invalid short session id')
84
+ }
85
+
86
+ sessionPayload = sessionPayload || {}
87
+
88
+ if (this._sessionState !== 'STOPPED') {
89
+ throw new Error('Session should be ended before starting new one.')
90
+ }
91
+
92
+ this._sessionType = sessionType
93
+
94
+ let session: ISession
95
+
96
+ sessionPayload.name = sessionPayload.name
97
+ ? sessionPayload.name
98
+ : `Session on ${getFormattedDate(Date.now())}`
99
+
100
+ sessionPayload.resourceAttributes = {
101
+ ...this._resourceAttributes,
102
+ ...sessionPayload.resourceAttributes
103
+ }
104
+
105
+ if (this._sessionType === SessionType.CONTINUOUS) {
106
+ session = await this._apiService.startContinuousSession(sessionPayload)
107
+ } else {
108
+ session = await this._apiService.startSession(sessionPayload)
109
+ }
110
+
111
+ this._shortSessionId = session.shortId as string
112
+
113
+ (this._traceIdGenerator as SessionRecorderIdGenerator).setSessionId(
114
+ this._shortSessionId,
115
+ this._sessionType
116
+ )
117
+
118
+ this._sessionState = 'STARTED'
119
+ }
120
+
121
+ /**
122
+ * @description Save the continuous session
123
+ * @param {String} [reason]
124
+ * @returns {Promise<void>}
125
+ */
126
+ static async save(reason?: string) {
127
+ SessionRecorderSdk.saveContinuousSession(reason)
128
+ }
129
+
130
+ /**
131
+ * @description Save the continuous session
132
+ * @param {ISession} [sessionData]
133
+ * @returns {Promise<void>}
134
+ */
135
+ public async save(
136
+ sessionData?: ISession
137
+ ): Promise<void> {
138
+ try {
139
+ if (!this._isInitialized) {
140
+ throw new Error(
141
+ 'Configuration not initialized. Call init() before performing any actions.',
142
+ )
143
+ }
144
+
145
+ if (
146
+ this._sessionState === 'STOPPED'
147
+ || typeof this._shortSessionId !== 'string'
148
+ ) {
149
+ throw new Error('Session should be active or paused')
150
+ }
151
+
152
+ if (this._sessionType !== SessionType.CONTINUOUS) {
153
+ throw new Error('Invalid session type')
154
+ }
155
+
156
+ await this._apiService.saveContinuousSession(
157
+ this._shortSessionId,
158
+ {
159
+ ...(sessionData || {}),
160
+ name: sessionData?.name
161
+ ? sessionData.name
162
+ : `Session on ${getFormattedDate(Date.now())}`
163
+ },
164
+ )
165
+ } catch (e) {
166
+ throw e
167
+ }
168
+ }
169
+
170
+ /**
171
+ * @description Stop the current session with an optional comment
172
+ * @param {ISession} [sessionData] - user-provided comment to include in session metadata
173
+ * @returns {Promise<void>}
174
+ */
175
+ public async stop(
176
+ sessionData?: ISession
177
+ ): Promise<void> {
178
+ try {
179
+ if (!this._isInitialized) {
180
+ throw new Error(
181
+ 'Configuration not initialized. Call init() before performing any actions.',
182
+ )
183
+ }
184
+
185
+ if (
186
+ this._sessionState === 'STOPPED'
187
+ || typeof this._shortSessionId !== 'string'
188
+ ) {
189
+ throw new Error('Session should be active or paused')
190
+ }
191
+
192
+ if (this._sessionType !== SessionType.PLAIN) {
193
+ throw new Error('Invalid session type')
194
+ }
195
+
196
+ await this._apiService.stopSession(
197
+ this._shortSessionId,
198
+ sessionData || {},
199
+ )
200
+ } catch (e) {
201
+ throw e
202
+ } finally {
203
+ (this._traceIdGenerator as SessionRecorderIdGenerator).setSessionId('')
204
+
205
+ this._shortSessionId = false
206
+ this._sessionState = 'STOPPED'
207
+ }
208
+ }
209
+
210
+ /**
211
+ * @description Cancel the current session
212
+ * @returns {Promise<void>}
213
+ */
214
+ public async cancel(): Promise<void> {
215
+ try {
216
+ if (!this._isInitialized) {
217
+ throw new Error(
218
+ 'Configuration not initialized. Call init() before performing any actions.',
219
+ )
220
+ }
221
+
222
+ if (
223
+ this._sessionState === 'STOPPED'
224
+ || typeof this._shortSessionId !== 'string'
225
+ ) {
226
+ throw new Error('Session should be active or paused')
227
+ }
228
+
229
+ if (this._sessionType === SessionType.CONTINUOUS) {
230
+ await this._apiService.stopContinuousSession(this._shortSessionId)
231
+ } else if (this._sessionType === SessionType.PLAIN) {
232
+ await this._apiService.cancelSession(this._shortSessionId)
233
+ }
234
+ } catch (e) {
235
+ throw e
236
+ } finally {
237
+ (this._traceIdGenerator as SessionRecorderIdGenerator).setSessionId('')
238
+
239
+ this._shortSessionId = false
240
+ this._sessionState = 'STOPPED'
241
+ }
242
+ }
243
+
244
+ /**
245
+ * @description Check if continuous session should be started/stopped automatically
246
+ * @param {ISession} [sessionPayload]
247
+ * @returns {Promise<void>}
248
+ */
249
+ public async checkRemoteContinuousSession(
250
+ sessionPayload?: Omit<ISession, '_id' | 'shortId'>
251
+ ): Promise<void> {
252
+ if (!this._isInitialized) {
253
+ throw new Error(
254
+ 'Configuration not initialized. Call init() before performing any actions.',
255
+ )
256
+ }
257
+
258
+ sessionPayload = sessionPayload || {}
259
+
260
+ sessionPayload.resourceAttributes = {
261
+ ...(sessionPayload.resourceAttributes || {}),
262
+ ...this._resourceAttributes,
263
+ }
264
+
265
+ const { state } = await this._apiService.checkRemoteSession(sessionPayload)
266
+
267
+ if (state == 'START' && this._sessionState !== 'STARTED') {
268
+ await this.start(SessionType.CONTINUOUS, sessionPayload)
269
+ } else if (state == 'STOP' && this._sessionState !== 'STOPPED') {
270
+ await this.stop()
271
+ }
272
+ }
273
+ }
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface ISession {
2
+ _id?: string
3
+ shortId?: string
4
+ name?: string
5
+ resourceAttributes?: {
6
+ browserInfo?: string,
7
+ cookiesEnabled?: string,
8
+ deviceInfo?: string,
9
+ hardwareConcurrency?: string,
10
+ osInfo?: string,
11
+ pixelRatio?: string,
12
+ screenSize?: string,
13
+ } & object
14
+ sessionAttributes?: {
15
+ userEmail?: string
16
+ userId?: string,
17
+ userName?: string,
18
+ accountId?: string,
19
+ accountName?: string,
20
+
21
+ comment?: string
22
+ // notifyOnUpdates?: boolean // remove
23
+ } & object
24
+ tags?: {
25
+ key?: string
26
+ value: string
27
+ }[]
28
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "declarationMap": true,
5
+ "baseUrl": ".",
6
+ "noEmit": false,
7
+ "module": "commonjs",
8
+ "noImplicitAny": false,
9
+ "noUnusedParameters": false,
10
+ "allowJs": false,
11
+ "allowSyntheticDefaultImports": true,
12
+ "esModuleInterop": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "isolatedModules": true,
15
+ "moduleResolution": "node",
16
+ "noImplicitReturns": true,
17
+ "noImplicitThis": true,
18
+ "noUnusedLocals": false,
19
+ "preserveConstEnums": true,
20
+ "removeComments": false,
21
+ "resolveJsonModule": true,
22
+ "skipLibCheck": true,
23
+ "sourceMap": true,
24
+ "target": "ES2018",
25
+ "downlevelIteration": true,
26
+ "strict": true,
27
+ "composite": true,
28
+ "outDir": "./dist/",
29
+ "rootDir": "./src",
30
+ "preserveSymlinks": true,
31
+ "paths": {}
32
+ },
33
+ "exclude": ["dist"],
34
+ "references": []
35
+ }