@statsig/cli-session-replay-node 3.20.0

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,14 @@
1
+ ISC License (ISC)
2
+ Copyright (c) 2021, Statsig, Inc.
3
+
4
+ Permission to use, copy, modify, and/or distribute this software for any purpose
5
+ with or without fee is hereby granted, provided that the above copyright notice
6
+ and this permission notice appear in all copies.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
10
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
12
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
13
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
14
+ THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ Statsig helps you move faster with feature gates (feature flags), and/or dynamic configs. It also allows you to run A/B/n tests to validate your new features and understand their impact on your KPIs. If you're new to Statsig, check out our product and create an account at [statsig.com](https://www.statsig.com/?ref=gh_jsm).
2
+
3
+ <h1 align="center">
4
+ <a href="https://statsig.com/?ref=gh_jsm">
5
+ <img src="https://github.com/statsig-io/js-client-monorepo/assets/95646168/ae5499ed-20ff-4584-bf21-8857f800d485" />
6
+ </a>
7
+ <div />
8
+ <a href="https://statsig.com/?ref=gh_jsm">Statsig</a>
9
+ </h1>
10
+
11
+ <p align="center">
12
+ <a href="https://github.com/statsig-io/js-client-monorepo/blob/main/LICENSE">
13
+ <img src="https://img.shields.io/badge/license-ISC-blue.svg?colorA=1b2528&colorB=ccfbc7&style=for-the-badge">
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@statsig/js-client">
16
+ <img src="https://img.shields.io/npm/v/@statsig/js-client.svg?colorA=1b2528&colorB=b2d3ff&style=for-the-badge">
17
+ </a>
18
+ <a href="https://statsig.com/community?ref=gh_jsm">
19
+ <img src="https://img.shields.io/badge/slack-statsig-brightgreen.svg?logo=slack&colorA=1b2528&colorB=FFF8BA&style=for-the-badge">
20
+ </a>
21
+ </p>
22
+
23
+ ## Getting Started
24
+
25
+ Read through the [Documentation](https://docs.statsig.com/client/javascript-sdk?ref=gh_jsm) or check out the [Samples](samples/).
26
+
27
+ ## Packages
28
+
29
+ Clients
30
+
31
+ - Precomputed Evaluations (Recommended) [[npm](https://www.npmjs.com/package/@statsig/js-client)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/js-client)]
32
+ - On Device Evaluations [[npm](https://www.npmjs.com/package/@statsig/js-on-device-eval-client)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/js-on-device-eval-client)]
33
+
34
+ Product Bundles
35
+
36
+ - Session Replay [[npm](https://www.npmjs.com/package/@statsig/session-replay)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/session-replay)]
37
+ - Web Analytics [[npm](https://www.npmjs.com/package/@statsig/web-analytics)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/web-analytics)]
38
+
39
+ Framework Specific Bindings
40
+
41
+ - React [[npm](https://www.npmjs.com/package/@statsig/react-bindings)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/react-bindings)]
42
+
43
+ - React Native [[npm](https://www.npmjs.com/package/@statsig/react-native-bindings)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/react-native-bindings)]
44
+
45
+ - React Native (Expo) [[npm](https://www.npmjs.com/package/@statsig/expo-bindings)] [[source](https://github.com/statsig-io/js-client-monorepo/tree/main/packages/expo-bindings)]
46
+
47
+ ## Community
48
+
49
+ If you need any assistance or just have a question, feel free to reach out to us on [Slack](https://statsig.com/community?ref=gh_jsm).
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@statsig/cli-session-replay-node",
3
+ "version": "3.20.0",
4
+ "license": "ISC",
5
+ "homepage": "https://github.com/statsig-io/js-client-monorepo",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "github:statsig-io/js-client-monorepo",
9
+ "directory": "packages/cli-session-replay-node"
10
+ },
11
+ "dependencies": {
12
+ "@statsig/client-core": "3.20.0",
13
+ "intercept-stdout": "^0.1.2"
14
+ },
15
+ "type": "commonjs",
16
+ "main": "./src/index.js",
17
+ "typings": "./src/index.d.ts",
18
+ "devDependencies": {
19
+ "@types/intercept-stdout": "^0.1.3",
20
+ "@types/node": "18.19.18"
21
+ }
22
+ }
@@ -0,0 +1,39 @@
1
+ export interface AsciicastHeader {
2
+ version: 2;
3
+ width: number;
4
+ height: number;
5
+ timestamp?: number;
6
+ duration?: number;
7
+ idle_time_limit?: number;
8
+ command?: string;
9
+ title?: string;
10
+ env?: Record<string, string | undefined>;
11
+ theme?: {
12
+ fg?: string;
13
+ bg?: string;
14
+ palette?: string;
15
+ };
16
+ }
17
+ /**
18
+ * Event codes:
19
+ * - o: Output
20
+ * - i: Input
21
+ * - m: Marker
22
+ * - r: Resize
23
+ * - x: Exit (Only defined by asciicast v3, but v2 supports custom events)
24
+ */
25
+ export type AsciicastEventCode = 'o' | 'i' | 'm' | 'r' | 'x' | (string & {});
26
+ export declare const AsciicastEventCode: {
27
+ Output: "o";
28
+ Input: "i";
29
+ Marker: "m";
30
+ Resize: "r";
31
+ /**
32
+ * Exit code
33
+ * Note: Only defined by asciicast v3, but v2 supports custom events
34
+ */
35
+ Exit: "x";
36
+ };
37
+ export type AsciicastEventData = unknown;
38
+ export type AsciicastEvent = [number, AsciicastEventCode, AsciicastEventData];
39
+ export type AsciicastArray = [AsciicastHeader, ...AsciicastEvent[]];
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AsciicastEventCode = void 0;
4
+ exports.AsciicastEventCode = {
5
+ Output: 'o',
6
+ Input: 'i',
7
+ Marker: 'm',
8
+ Resize: 'r',
9
+ /**
10
+ * Exit code
11
+ * Note: Only defined by asciicast v3, but v2 supports custom events
12
+ */
13
+ Exit: 'x',
14
+ };
@@ -0,0 +1,60 @@
1
+ import { PrecomputedEvaluationsInterface } from '@statsig/client-core';
2
+ import { AsciicastEventCode, AsciicastEventData, AsciicastHeader } from './AsciicastTypes';
3
+ export type StatsigAsciicastHeader = Omit<AsciicastHeader, 'version' | 'width' | 'height' | 'timestamp'>;
4
+ export interface CliRecordingPrivateHooks {
5
+ onResize(width: number, height: number): void;
6
+ onOutput(contents: string): void;
7
+ addCustomEvent(code: AsciicastEventCode, contents: AsciicastEventData): void;
8
+ }
9
+ export interface CliRecordingAdapter {
10
+ getSize(): {
11
+ width: number;
12
+ height: number;
13
+ };
14
+ start(): void;
15
+ stop(): void;
16
+ }
17
+ export type CliAdapterFactory = (hooks: CliRecordingPrivateHooks) => CliRecordingAdapter;
18
+ export interface CliRecordingOptions {
19
+ /**
20
+ * Override the start timestamp. Defined in milliseconds.
21
+ * @default Date.now()
22
+ */
23
+ startTimestamp?: number;
24
+ asciicastHeader?: StatsigAsciicastHeader;
25
+ }
26
+ /**
27
+ * CliRecording represents a recording. This is a singleton to ensure there's only one active recording at a time.
28
+ */
29
+ export declare class CliRecording {
30
+ private static _currentRecording;
31
+ static get currentRecording(): CliRecording | undefined;
32
+ static record(client: PrecomputedEvaluationsInterface, adapterFactory: CliAdapterFactory, options?: CliRecordingOptions): CliRecording;
33
+ static finish(): void;
34
+ static isRecording(): boolean;
35
+ startTimestamp: number;
36
+ endTimestamp?: number;
37
+ recording: boolean;
38
+ private _client;
39
+ private _sessionID;
40
+ private _adapter;
41
+ private _asciicastLines;
42
+ private _payloadSize;
43
+ private _unsubscribe?;
44
+ private constructor();
45
+ finish(endReason?: string): void;
46
+ /**
47
+ * Start recording
48
+ */
49
+ private _startRecording;
50
+ private _buildController;
51
+ private _appendEvent;
52
+ /**
53
+ * Create logging event and send it to Statsig
54
+ * @param endReason
55
+ */
56
+ private _logRecording;
57
+ private _getSessionIdFromClient;
58
+ private _recTime;
59
+ private _shutdown;
60
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CliRecording = void 0;
4
+ const client_core_1 = require("@statsig/client-core");
5
+ const TIMEOUT_MS = 1000 * 60 * 60 * 4; // 4 hours
6
+ const MAX_RECORDING_STRLEN = 1024 * 1024; // 1 MB
7
+ /**
8
+ * CliRecording represents a recording. This is a singleton to ensure there's only one active recording at a time.
9
+ */
10
+ class CliRecording {
11
+ static get currentRecording() {
12
+ return this._currentRecording;
13
+ }
14
+ static record(client, adapterFactory, options) {
15
+ if (!this._currentRecording) {
16
+ this._currentRecording = new CliRecording(client, adapterFactory, options);
17
+ }
18
+ return this._currentRecording;
19
+ }
20
+ static finish() {
21
+ var _a;
22
+ (_a = this._currentRecording) === null || _a === void 0 ? void 0 : _a.finish();
23
+ }
24
+ static isRecording() {
25
+ return this._currentRecording !== undefined;
26
+ }
27
+ constructor(client, adapterFactory, options) {
28
+ var _a;
29
+ this.recording = true;
30
+ this._asciicastLines = [];
31
+ this._payloadSize = 0;
32
+ this._client = client;
33
+ this._adapter = adapterFactory(this._buildController());
34
+ // Read data from client
35
+ const clientContext = this._client.getContext();
36
+ this._sessionID = this._getSessionIdFromClient(clientContext);
37
+ // Subscribe to events
38
+ const timeout = setTimeout(() => {
39
+ this.finish('timeout');
40
+ }, TIMEOUT_MS);
41
+ const shutdownHandler = () => this.finish();
42
+ const sessionExpiredHandler = () => this.finish();
43
+ this._client.$on('pre_shutdown', shutdownHandler);
44
+ this._client.$on('session_expired', sessionExpiredHandler);
45
+ this._unsubscribe = () => {
46
+ clearTimeout(timeout);
47
+ this._client.off('pre_shutdown', shutdownHandler);
48
+ this._client.off('session_expired', sessionExpiredHandler);
49
+ };
50
+ // Create recording header
51
+ const { width, height } = this._adapter.getSize();
52
+ this.startTimestamp = (_a = options === null || options === void 0 ? void 0 : options.startTimestamp) !== null && _a !== void 0 ? _a : Date.now();
53
+ const header = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.asciicastHeader), { version: 2, width,
54
+ height, timestamp: Math.floor(this.startTimestamp / 1000) });
55
+ this._asciicastLines.push(JSON.stringify(header));
56
+ // Start recording
57
+ this._startRecording();
58
+ }
59
+ finish(endReason) {
60
+ this.endTimestamp = Date.now();
61
+ this._shutdown();
62
+ this._logRecording(endReason);
63
+ }
64
+ /**
65
+ * Start recording
66
+ */
67
+ _startRecording() {
68
+ client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'true' });
69
+ this._adapter.start();
70
+ }
71
+ _buildController() {
72
+ return {
73
+ onResize: (width, height) => {
74
+ this._appendEvent('r', `${width}x${height}`);
75
+ },
76
+ onOutput: (contents) => {
77
+ this._appendEvent('o', contents);
78
+ },
79
+ addCustomEvent: (type, contents) => {
80
+ this._appendEvent(type, contents);
81
+ },
82
+ };
83
+ }
84
+ _appendEvent(type, content) {
85
+ if (!this.recording) {
86
+ return;
87
+ }
88
+ try {
89
+ if (this._sessionID !== this._getSessionIdFromClient()) {
90
+ this.finish('session_expired');
91
+ return;
92
+ }
93
+ const now = Date.now();
94
+ // Always keep a running end timestamp
95
+ this.endTimestamp = now;
96
+ const asciicastEvent = [
97
+ this._recTime(now),
98
+ type,
99
+ content,
100
+ ];
101
+ const asciicastEventLength = (0, client_core_1._fastApproxSizeOf)(asciicastEvent, MAX_RECORDING_STRLEN);
102
+ if (this._payloadSize + asciicastEventLength >= MAX_RECORDING_STRLEN) {
103
+ this.finish('max_size_exceeded');
104
+ return;
105
+ }
106
+ this._asciicastLines.push(JSON.stringify(asciicastEvent));
107
+ this._payloadSize += asciicastEventLength;
108
+ }
109
+ catch (e) {
110
+ // If we get an error here, we need to stop recording to avoid an infinite loop
111
+ this._asciicastLines.push(JSON.stringify([
112
+ this._recTime(),
113
+ 'e',
114
+ e instanceof Error ? e.message : `${e}`,
115
+ ]));
116
+ this.finish('error');
117
+ }
118
+ }
119
+ /**
120
+ * Create logging event and send it to Statsig
121
+ * @param endReason
122
+ */
123
+ _logRecording(endReason) {
124
+ const file = this._asciicastLines.join('\n');
125
+ const metadata = {
126
+ session_start_ts: String(this.startTimestamp),
127
+ session_end_ts: String(this.endTimestamp),
128
+ asciinema_events: file,
129
+ asciinema_payload_size: String(file.length),
130
+ session_replay_sdk_version: client_core_1.SDK_VERSION,
131
+ platform: 'cli',
132
+ sdk_instance_id: this._client.getContext().sdkInstanceID,
133
+ };
134
+ if (endReason) {
135
+ metadata[endReason] = 'true';
136
+ }
137
+ this._client.logEvent({
138
+ eventName: 'statsig::session_recording',
139
+ value: this._sessionID,
140
+ metadata,
141
+ });
142
+ }
143
+ _getSessionIdFromClient(context = this._client.getContext()) {
144
+ return context.session.data.sessionID;
145
+ }
146
+ _recTime(now = Date.now()) {
147
+ return (now - this.startTimestamp) / 1000;
148
+ }
149
+ _shutdown() {
150
+ var _a;
151
+ this.recording = false;
152
+ this._adapter.stop();
153
+ (_a = this._unsubscribe) === null || _a === void 0 ? void 0 : _a.call(this);
154
+ this._unsubscribe = undefined;
155
+ client_core_1.StatsigMetadataProvider.add({ isRecordingSession: 'false' });
156
+ CliRecording._currentRecording = undefined;
157
+ }
158
+ }
159
+ exports.CliRecording = CliRecording;
@@ -0,0 +1,14 @@
1
+ import { CliAdapterFactory, CliRecordingAdapter, CliRecordingPrivateHooks } from './CliRecording';
2
+ export declare class CliRecordingNodeAdapter implements CliRecordingAdapter {
3
+ private _hooks;
4
+ private _stopIntercept;
5
+ constructor(_hooks: CliRecordingPrivateHooks);
6
+ start(): void;
7
+ getSize(): {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ stop(): void;
12
+ private _resizeHandler;
13
+ }
14
+ export declare const CliRecordingNodeAdapterFactory: CliAdapterFactory;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CliRecordingNodeAdapterFactory = exports.CliRecordingNodeAdapter = void 0;
4
+ const process = require("process");
5
+ const intercept = require("intercept-stdout");
6
+ class CliRecordingNodeAdapter {
7
+ constructor(_hooks) {
8
+ this._hooks = _hooks;
9
+ this._resizeHandler = () => {
10
+ this._hooks.onResize(process.stdout.columns, process.stdout.rows);
11
+ };
12
+ }
13
+ start() {
14
+ this._stopIntercept = intercept((str) => {
15
+ this._hooks.onOutput(str.replaceAll('\n', '\r\n'));
16
+ });
17
+ process.stdout.on('resize', this._resizeHandler);
18
+ }
19
+ getSize() {
20
+ return {
21
+ width: process.stdout.columns,
22
+ height: process.stdout.rows,
23
+ };
24
+ }
25
+ stop() {
26
+ if (this._stopIntercept) {
27
+ this._stopIntercept();
28
+ this._stopIntercept = undefined;
29
+ }
30
+ process.stdout.off('resize', this._resizeHandler);
31
+ }
32
+ }
33
+ exports.CliRecordingNodeAdapter = CliRecordingNodeAdapter;
34
+ const CliRecordingNodeAdapterFactory = (hooks) => new CliRecordingNodeAdapter(hooks);
35
+ exports.CliRecordingNodeAdapterFactory = CliRecordingNodeAdapterFactory;
@@ -0,0 +1,8 @@
1
+ import { PrecomputedEvaluationsInterface, StatsigPlugin } from '@statsig/client-core';
2
+ import { CliRecordingOptions } from './CliRecording';
3
+ export declare class StatsigCliSessionReplayPlugin implements StatsigPlugin<PrecomputedEvaluationsInterface> {
4
+ private readonly options?;
5
+ readonly __plugin = "cli-session-replay-node";
6
+ constructor(options?: CliRecordingOptions | undefined);
7
+ bind(client: PrecomputedEvaluationsInterface): void;
8
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StatsigCliSessionReplayPlugin = void 0;
4
+ const CliRecording_1 = require("./CliRecording");
5
+ const CliRecordingNodeAdapter_1 = require("./CliRecordingNodeAdapter");
6
+ class StatsigCliSessionReplayPlugin {
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.__plugin = 'cli-session-replay-node';
10
+ }
11
+ bind(client) {
12
+ CliRecording_1.CliRecording.record(client, CliRecordingNodeAdapter_1.CliRecordingNodeAdapterFactory, this.options);
13
+ }
14
+ }
15
+ exports.StatsigCliSessionReplayPlugin = StatsigCliSessionReplayPlugin;
package/src/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './AsciicastTypes';
2
+ export * from './CliRecordingNodeAdapter';
3
+ export * from './CliSessionReplayPlugin';
4
+ export * from './CliRecording';
package/src/index.js ADDED
@@ -0,0 +1,32 @@
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
+ const client_core_1 = require("@statsig/client-core");
18
+ const AsciicastTypes_1 = require("./AsciicastTypes");
19
+ const CliRecording_1 = require("./CliRecording");
20
+ const CliRecordingNodeAdapter_1 = require("./CliRecordingNodeAdapter");
21
+ const CliSessionReplayPlugin_1 = require("./CliSessionReplayPlugin");
22
+ __exportStar(require("./AsciicastTypes"), exports);
23
+ __exportStar(require("./CliRecordingNodeAdapter"), exports);
24
+ __exportStar(require("./CliSessionReplayPlugin"), exports);
25
+ __exportStar(require("./CliRecording"), exports);
26
+ Object.assign((0, client_core_1._getStatsigGlobal)(), {
27
+ AsciicastEventCode: AsciicastTypes_1.AsciicastEventCode,
28
+ CliRecording: CliRecording_1.CliRecording,
29
+ CliRecordingNodeAdapter: CliRecordingNodeAdapter_1.CliRecordingNodeAdapter,
30
+ CliRecordingNodeAdapterFactory: CliRecordingNodeAdapter_1.CliRecordingNodeAdapterFactory,
31
+ StatsigCliSessionReplayPlugin: CliSessionReplayPlugin_1.StatsigCliSessionReplayPlugin,
32
+ });