@spotify-confidence/session-recording 0.0.0 → 0.17.2
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/CHANGELOG.md +36 -0
- package/README.md +136 -0
- package/dist/index.cjs +13228 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +13209 -0
- package/package.json +49 -1
- package/src/index.test.ts +195 -0
- package/src/index.ts +218 -0
- package/src/version.ts +1 -0
package/package.json
CHANGED
|
@@ -1 +1,49 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@spotify-confidence/session-recording",
|
|
3
|
+
"license": "Apache-2.0",
|
|
4
|
+
"version": "0.17.2",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/spotify/confidence-sdk-js.git",
|
|
8
|
+
"directory": "csr/session-recording"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"prebuild": "node sync-version.mjs",
|
|
15
|
+
"build": "yarn run -T tsdown",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"registry": "https://registry.npmjs.org/",
|
|
24
|
+
"access": "public",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.cjs",
|
|
27
|
+
"module": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"require": "./dist/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@spotify-confidence/csr-common": "^0.17.2",
|
|
39
|
+
"@spotify-confidence/csr-recorder": "^0.17.2"
|
|
40
|
+
},
|
|
41
|
+
"module": "./dist/index.js",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"import": "./dist/index.js",
|
|
46
|
+
"require": "./dist/index.cjs"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const createUploader = vi.hoisted(() => vi.fn());
|
|
4
|
+
const record = vi.hoisted(() => vi.fn());
|
|
5
|
+
|
|
6
|
+
vi.mock('@spotify-confidence/csr-common/uploader', () => ({
|
|
7
|
+
createUploader,
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('@spotify-confidence/csr-recorder', () => ({
|
|
10
|
+
record,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { initSessionRecorder } from './index';
|
|
14
|
+
|
|
15
|
+
function flushPromises() {
|
|
16
|
+
return new Promise(r => setTimeout(r, 0));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mockUploader() {
|
|
20
|
+
const fn = Object.assign(vi.fn(), { close: vi.fn() });
|
|
21
|
+
return fn;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('initSessionRecorder', () => {
|
|
25
|
+
afterEach(() => vi.resetAllMocks());
|
|
26
|
+
|
|
27
|
+
it('always returns a SessionRecorder', () => {
|
|
28
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
29
|
+
record.mockReturnValueOnce(() => {});
|
|
30
|
+
|
|
31
|
+
const recorder = initSessionRecorder({ clientSecret: 'secret' });
|
|
32
|
+
|
|
33
|
+
expect(recorder.start).toBeTypeOf('function');
|
|
34
|
+
expect(recorder.stop).toBeTypeOf('function');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('automatic mode inits and records immediately', async () => {
|
|
38
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
39
|
+
record.mockReturnValueOnce(() => {});
|
|
40
|
+
|
|
41
|
+
initSessionRecorder({ clientSecret: 'secret' });
|
|
42
|
+
await flushPromises();
|
|
43
|
+
|
|
44
|
+
expect(createUploader).toHaveBeenCalledOnce();
|
|
45
|
+
expect(createUploader.mock.calls[0][0]).toMatchObject({
|
|
46
|
+
forceRecord: false,
|
|
47
|
+
});
|
|
48
|
+
expect(record).toHaveBeenCalledOnce();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('forwards options to createUploader and record', async () => {
|
|
52
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
53
|
+
record.mockReturnValueOnce(() => {});
|
|
54
|
+
|
|
55
|
+
const ctx = { buildVersion: '2.3.1' };
|
|
56
|
+
initSessionRecorder({
|
|
57
|
+
clientSecret: 'secret',
|
|
58
|
+
targetingKey: 'user-42',
|
|
59
|
+
context: ctx,
|
|
60
|
+
maskSelectors: ['.private'],
|
|
61
|
+
blockSelectors: ['video', '.third-party'],
|
|
62
|
+
maskInputs: false,
|
|
63
|
+
});
|
|
64
|
+
await flushPromises();
|
|
65
|
+
|
|
66
|
+
expect(createUploader.mock.calls[0][0]).toMatchObject({
|
|
67
|
+
apiUrl: 'https://recording.confidence.dev',
|
|
68
|
+
websocketUrl: 'wss://recording-ws.confidence.dev/sessions/stream',
|
|
69
|
+
clientSecret: 'secret',
|
|
70
|
+
targetingKey: 'user-42',
|
|
71
|
+
context: ctx,
|
|
72
|
+
});
|
|
73
|
+
expect(record.mock.calls[0][1]).toEqual({
|
|
74
|
+
maskSelectors: ['.private'],
|
|
75
|
+
blockSelectors: ['video', '.third-party'],
|
|
76
|
+
maskInputs: false,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('automatic mode does not call record when backend skips', async () => {
|
|
81
|
+
createUploader.mockResolvedValueOnce(null);
|
|
82
|
+
|
|
83
|
+
initSessionRecorder({ clientSecret: 'secret' });
|
|
84
|
+
await flushPromises();
|
|
85
|
+
|
|
86
|
+
expect(record).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not throw when createUploader rejects', async () => {
|
|
90
|
+
createUploader.mockRejectedValueOnce(new Error('boom'));
|
|
91
|
+
|
|
92
|
+
const recorder = initSessionRecorder({ clientSecret: 'secret' });
|
|
93
|
+
await flushPromises();
|
|
94
|
+
|
|
95
|
+
expect(recorder).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('manual mode does not init until start is called', async () => {
|
|
99
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
100
|
+
record.mockReturnValueOnce(() => {});
|
|
101
|
+
|
|
102
|
+
const recorder = initSessionRecorder({
|
|
103
|
+
clientSecret: 'secret',
|
|
104
|
+
mode: 'manual',
|
|
105
|
+
});
|
|
106
|
+
await flushPromises();
|
|
107
|
+
|
|
108
|
+
expect(createUploader).not.toHaveBeenCalled();
|
|
109
|
+
expect(record).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
recorder.start();
|
|
112
|
+
await flushPromises();
|
|
113
|
+
|
|
114
|
+
expect(createUploader).toHaveBeenCalledOnce();
|
|
115
|
+
expect(createUploader.mock.calls[0][0]).toMatchObject({
|
|
116
|
+
forceRecord: true,
|
|
117
|
+
});
|
|
118
|
+
expect(record).toHaveBeenCalledOnce();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('start is a no-op in automatic mode', async () => {
|
|
122
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
123
|
+
record.mockReturnValueOnce(() => {});
|
|
124
|
+
|
|
125
|
+
const recorder = initSessionRecorder({ clientSecret: 'secret' });
|
|
126
|
+
await flushPromises();
|
|
127
|
+
|
|
128
|
+
recorder.start();
|
|
129
|
+
await flushPromises();
|
|
130
|
+
|
|
131
|
+
expect(createUploader).toHaveBeenCalledOnce();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('stop tears down the recorder and closes the transport', async () => {
|
|
135
|
+
const stopFn = vi.fn();
|
|
136
|
+
const uploader = mockUploader();
|
|
137
|
+
createUploader.mockResolvedValueOnce(uploader);
|
|
138
|
+
record.mockReturnValueOnce(stopFn);
|
|
139
|
+
|
|
140
|
+
const recorder = initSessionRecorder({ clientSecret: 'secret' });
|
|
141
|
+
await flushPromises();
|
|
142
|
+
|
|
143
|
+
recorder.stop();
|
|
144
|
+
expect(stopFn).toHaveBeenCalledOnce();
|
|
145
|
+
expect(uploader.close).toHaveBeenCalledOnce();
|
|
146
|
+
|
|
147
|
+
recorder.stop();
|
|
148
|
+
expect(stopFn).toHaveBeenCalledOnce();
|
|
149
|
+
expect(uploader.close).toHaveBeenCalledOnce();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('isRecording reflects recording state', async () => {
|
|
153
|
+
const stopFn = vi.fn();
|
|
154
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
155
|
+
record.mockReturnValueOnce(stopFn);
|
|
156
|
+
|
|
157
|
+
const recorder = initSessionRecorder({ clientSecret: 'secret' });
|
|
158
|
+
expect(recorder.isRecording).toBe(false);
|
|
159
|
+
|
|
160
|
+
await flushPromises();
|
|
161
|
+
expect(recorder.isRecording).toBe(true);
|
|
162
|
+
|
|
163
|
+
recorder.stop();
|
|
164
|
+
expect(recorder.isRecording).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('isRecording is false in manual mode before start', async () => {
|
|
168
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
169
|
+
record.mockReturnValueOnce(() => {});
|
|
170
|
+
|
|
171
|
+
const recorder = initSessionRecorder({
|
|
172
|
+
clientSecret: 'secret',
|
|
173
|
+
mode: 'manual',
|
|
174
|
+
});
|
|
175
|
+
await flushPromises();
|
|
176
|
+
|
|
177
|
+
expect(recorder.isRecording).toBe(false);
|
|
178
|
+
|
|
179
|
+
recorder.start();
|
|
180
|
+
await flushPromises();
|
|
181
|
+
|
|
182
|
+
expect(recorder.isRecording).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('stop before init completes prevents recording', async () => {
|
|
186
|
+
createUploader.mockResolvedValueOnce(mockUploader());
|
|
187
|
+
record.mockReturnValueOnce(() => {});
|
|
188
|
+
|
|
189
|
+
const recorder = initSessionRecorder({ clientSecret: 'secret' });
|
|
190
|
+
recorder.stop();
|
|
191
|
+
await flushPromises();
|
|
192
|
+
|
|
193
|
+
expect(record).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { record } from '@spotify-confidence/csr-recorder';
|
|
2
|
+
import type { ConsoleLogLevel } from '@spotify-confidence/csr-common';
|
|
3
|
+
import {
|
|
4
|
+
RecordingEventType,
|
|
5
|
+
type TagPluginData,
|
|
6
|
+
type MeasurePluginData,
|
|
7
|
+
validateKey,
|
|
8
|
+
validateTagValue,
|
|
9
|
+
validateMeasureValue,
|
|
10
|
+
} from '@spotify-confidence/csr-common';
|
|
11
|
+
import { createUploader, type ClientContext } from '@spotify-confidence/csr-common/uploader';
|
|
12
|
+
import { SDK_VERSION } from './version';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_API_URL = 'https://recording.confidence.dev';
|
|
15
|
+
const DEFAULT_WEBSOCKET_URL = 'wss://recording-ws.confidence.dev/sessions/stream';
|
|
16
|
+
export interface InitSessionRecorderOptions {
|
|
17
|
+
/** Per-tenant secret. */
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
/** End-user identifier (visitor / device id). */
|
|
20
|
+
targetingKey?: string;
|
|
21
|
+
/** CSS selectors whose text content should be masked. */
|
|
22
|
+
maskSelectors?: string[];
|
|
23
|
+
/** CSS selectors whose subtrees should be blocked (replaced with a placeholder, never serialized). */
|
|
24
|
+
blockSelectors?: string[];
|
|
25
|
+
/** Mask values of every `<input>` / `<textarea>` / `contenteditable`. Defaults to `true`. */
|
|
26
|
+
maskInputs?: boolean;
|
|
27
|
+
/** Capture browser console output. Defaults to `false`. Pass `true` for all levels or `{ levels: [...] }` for specific ones. */
|
|
28
|
+
captureConsoleLogs?: boolean | { levels: ConsoleLogLevel[] };
|
|
29
|
+
/** Capture fetch/XHR metadata (method, URL, status, duration). Defaults to `false`. */
|
|
30
|
+
captureNetworkRequests?: boolean;
|
|
31
|
+
/** Capture client-side route changes (pathname only). Defaults to `true`. */
|
|
32
|
+
captureRouteChanges?: boolean;
|
|
33
|
+
/** Backend base URL. Defaults to the Confidence production endpoint. */
|
|
34
|
+
apiUrl?: string;
|
|
35
|
+
/** WebSocket ingest URL. Defaults to the Confidence production endpoint. */
|
|
36
|
+
websocketUrl?: string;
|
|
37
|
+
/** Application version or commit hash, e.g. "1.2.3" or "abc1234". Stored on the recording for filtering. */
|
|
38
|
+
appVersion?: string;
|
|
39
|
+
/** Custom dimensions merged into the session context alongside auto-collected browser metadata. */
|
|
40
|
+
context?: ClientContext;
|
|
41
|
+
/**
|
|
42
|
+
* `'automatic'` (default) — starts recording as soon as the session is established.
|
|
43
|
+
* `'manual'` — does nothing until `start()` is called, bypassing sampling and targeting rules.
|
|
44
|
+
*/
|
|
45
|
+
mode?: 'automatic' | 'manual';
|
|
46
|
+
/** Verbose tracer for debugging — called with one-line lifecycle/transport messages. */
|
|
47
|
+
debugLogger?: (msg: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SessionRecorder {
|
|
51
|
+
/** Start recording. In `automatic` mode this is a no-op. In `manual` mode it establishes a session and begins recording. */
|
|
52
|
+
start(): void;
|
|
53
|
+
/** Stop recording permanently. Idempotent. */
|
|
54
|
+
stop(): void;
|
|
55
|
+
/** Attach a custom tag to this recording. Tags with the same key accumulate values. Omit value for a valueless marker. */
|
|
56
|
+
tag(key: string, value?: string): void;
|
|
57
|
+
/** Record a numeric measurement. Measurements with the same key are summed. Omit value to count occurrences (each call adds 1). */
|
|
58
|
+
measure(key: string, value?: number): void;
|
|
59
|
+
/** Whether the recorder is actively capturing events. */
|
|
60
|
+
readonly isRecording: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function csrDebugLogger(): ((msg: string) => void) | undefined {
|
|
64
|
+
try {
|
|
65
|
+
if (sessionStorage.getItem('CSR_DEBUG')) {
|
|
66
|
+
// Debug logger intentionally uses console — only active when CSR_DEBUG is set.
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
return (msg: string) => console.log(msg);
|
|
69
|
+
}
|
|
70
|
+
} catch (_e) {
|
|
71
|
+
// sessionStorage may be unavailable (sandboxed iframe, etc.)
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a session recorder. In `automatic` mode (default) recording begins
|
|
78
|
+
* as soon as a session is established. In `manual` mode nothing happens
|
|
79
|
+
* until {@link SessionRecorder.start} is called.
|
|
80
|
+
*
|
|
81
|
+
* Always returns a {@link SessionRecorder} — safe to call, never throws.
|
|
82
|
+
*/
|
|
83
|
+
export function initSessionRecorder(options: InitSessionRecorderOptions): SessionRecorder {
|
|
84
|
+
const userLogger = options.debugLogger ?? csrDebugLogger();
|
|
85
|
+
const debugLogger = userLogger ? (msg: string) => userLogger(`[CSR] ${msg}`) : undefined;
|
|
86
|
+
|
|
87
|
+
let stopRecorder: (() => void) | null = null;
|
|
88
|
+
let closeUploader: (() => void) | null = null;
|
|
89
|
+
let sendEvent: ((event: unknown) => void) | null = null;
|
|
90
|
+
let started = false;
|
|
91
|
+
let stopped = false;
|
|
92
|
+
|
|
93
|
+
const recordingConfig = {
|
|
94
|
+
maskSelectors: options.maskSelectors,
|
|
95
|
+
blockSelectors: options.blockSelectors,
|
|
96
|
+
maskInputs: options.maskInputs,
|
|
97
|
+
captureConsoleLogs: options.captureConsoleLogs,
|
|
98
|
+
captureNetworkRequests: options.captureNetworkRequests,
|
|
99
|
+
captureRouteChanges: options.captureRouteChanges,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
async function initAndRecord(forceRecord: boolean) {
|
|
103
|
+
try {
|
|
104
|
+
const uploader = await createUploader({
|
|
105
|
+
apiUrl: options.apiUrl ?? DEFAULT_API_URL,
|
|
106
|
+
websocketUrl: options.websocketUrl ?? DEFAULT_WEBSOCKET_URL,
|
|
107
|
+
clientSecret: options.clientSecret,
|
|
108
|
+
targetingKey: options.targetingKey,
|
|
109
|
+
context: {
|
|
110
|
+
...options.context,
|
|
111
|
+
_csr_sdk_version: SDK_VERSION,
|
|
112
|
+
...(options.appVersion ? { _app_version: options.appVersion } : {}),
|
|
113
|
+
},
|
|
114
|
+
forceRecord,
|
|
115
|
+
debugLogger,
|
|
116
|
+
onTerminate: ({ reason }) => {
|
|
117
|
+
debugLogger?.(`Recording terminated: ${reason}`);
|
|
118
|
+
stopRecorder?.();
|
|
119
|
+
stopRecorder = null;
|
|
120
|
+
stopped = true;
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (stopped) {
|
|
125
|
+
uploader?.close();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (uploader === null) {
|
|
130
|
+
debugLogger?.('Recording skipped by backend');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
closeUploader = () => uploader.close();
|
|
135
|
+
|
|
136
|
+
sendEvent = event => {
|
|
137
|
+
try {
|
|
138
|
+
uploader(event);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
debugLogger?.(`Event dropped: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
stopRecorder = record(sendEvent, recordingConfig);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
debugLogger?.(`Recording disabled: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const mode = options.mode ?? 'automatic';
|
|
151
|
+
|
|
152
|
+
if (mode === 'automatic') {
|
|
153
|
+
started = true;
|
|
154
|
+
void initAndRecord(false);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
start() {
|
|
159
|
+
if (started || stopped) return;
|
|
160
|
+
started = true;
|
|
161
|
+
void initAndRecord(true);
|
|
162
|
+
},
|
|
163
|
+
stop() {
|
|
164
|
+
if (stopped) return;
|
|
165
|
+
stopped = true;
|
|
166
|
+
stopRecorder?.();
|
|
167
|
+
stopRecorder = null;
|
|
168
|
+
sendEvent = null;
|
|
169
|
+
closeUploader?.();
|
|
170
|
+
closeUploader = null;
|
|
171
|
+
},
|
|
172
|
+
tag(key: string, value?: string) {
|
|
173
|
+
const keyErr = validateKey(key);
|
|
174
|
+
if (keyErr) {
|
|
175
|
+
debugLogger?.(`tag() dropped: ${keyErr}`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const valErr = validateTagValue(value);
|
|
179
|
+
if (valErr) {
|
|
180
|
+
debugLogger?.(`tag() dropped: ${valErr}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const data: TagPluginData = {
|
|
184
|
+
plugin: 'csr:tag',
|
|
185
|
+
payload: value !== undefined ? { key, value } : { key },
|
|
186
|
+
};
|
|
187
|
+
sendEvent?.({
|
|
188
|
+
type: RecordingEventType.Plugin,
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
data,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
measure(key: string, value?: number) {
|
|
194
|
+
const keyErr = validateKey(key);
|
|
195
|
+
if (keyErr) {
|
|
196
|
+
debugLogger?.(`measure() dropped: ${keyErr}`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const valErr = validateMeasureValue(value);
|
|
200
|
+
if (valErr) {
|
|
201
|
+
debugLogger?.(`measure() dropped: ${valErr}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const data: MeasurePluginData = {
|
|
205
|
+
plugin: 'csr:measure',
|
|
206
|
+
payload: value !== undefined ? { key, value } : { key },
|
|
207
|
+
};
|
|
208
|
+
sendEvent?.({
|
|
209
|
+
type: RecordingEventType.Plugin,
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
data,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
get isRecording() {
|
|
215
|
+
return stopRecorder !== null;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SDK_VERSION = '0.17.2';
|