@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 +22 -0
- package/README.md +98 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/services/api.service.d.ts.map +1 -0
- package/dist/sessionRecorder.d.ts +64 -0
- package/dist/sessionRecorder.d.ts.map +1 -0
- package/dist/sessionRecorder.js +199 -0
- package/dist/sessionRecorder.js.map +1 -0
- package/package.json +45 -0
- package/src/config.ts +5 -0
- package/src/helper.ts +13 -0
- package/src/index.ts +5 -0
- package/src/services/api.service.ts +203 -0
- package/src/sessionRecorder.ts +273 -0
- package/src/types.ts +28 -0
- package/tsconfig.json +35 -0
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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
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,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
|
+
}
|