@spotify-confidence/csr-recorder 0.17.16 → 0.17.17
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 +14 -0
- package/dist/index.cjs +6 -6
- package/dist/index.js +7 -7
- package/package.json +2 -2
- package/src/engine/rrweb-engine.test.ts +7 -4
- package/src/engine/rrweb-engine.ts +4 -4
- package/src/recorder-routing.test.ts +6 -5
- package/src/recorder.test.ts +18 -21
- package/src/recorder.ts +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.17](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.16...csr-recorder-v0.17.17) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### 🐛 Bug Fixes
|
|
7
|
+
|
|
8
|
+
* stop passive recording frames extending sessions ([#449](https://github.com/spotify/confidence-sdk-js/issues/449)) ([df763a1](https://github.com/spotify/confidence-sdk-js/commit/df763a10d6f16ce5b871530ca3fe8b5c153c16d7))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @spotify-confidence/csr-common bumped to 0.18.9
|
|
16
|
+
|
|
3
17
|
## [0.17.16](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.15...csr-recorder-v0.17.16) (2026-08-28)
|
|
4
18
|
|
|
5
19
|
|
package/dist/index.cjs
CHANGED
|
@@ -105,7 +105,7 @@ var Recorder = class Recorder {
|
|
|
105
105
|
if (typeof document !== "undefined") {
|
|
106
106
|
this.visibilityHandler = () => {
|
|
107
107
|
const data = {
|
|
108
|
-
plugin:
|
|
108
|
+
plugin: _spotify_confidence_csr_common.RecordingPluginName.TabVisibility,
|
|
109
109
|
payload: { hidden: document.hidden }
|
|
110
110
|
};
|
|
111
111
|
this.onEvent({
|
|
@@ -127,7 +127,7 @@ var Recorder = class Recorder {
|
|
|
127
127
|
}
|
|
128
128
|
emitNetworkRequest(payload) {
|
|
129
129
|
const data = {
|
|
130
|
-
plugin:
|
|
130
|
+
plugin: _spotify_confidence_csr_common.RecordingPluginName.NetworkRequest,
|
|
131
131
|
payload
|
|
132
132
|
};
|
|
133
133
|
this.onEvent({
|
|
@@ -235,7 +235,7 @@ var Recorder = class Recorder {
|
|
|
235
235
|
const paramTo = this.parameterizeRoute(to);
|
|
236
236
|
if (paramFrom === paramTo) return;
|
|
237
237
|
const data = {
|
|
238
|
-
plugin:
|
|
238
|
+
plugin: _spotify_confidence_csr_common.RecordingPluginName.RouteChange,
|
|
239
239
|
payload: {
|
|
240
240
|
from: paramFrom,
|
|
241
241
|
to: paramTo,
|
|
@@ -11140,7 +11140,7 @@ function labelBlockedElement(node) {
|
|
|
11140
11140
|
*/
|
|
11141
11141
|
function blockedElementLabelsPlugin() {
|
|
11142
11142
|
return {
|
|
11143
|
-
name:
|
|
11143
|
+
name: _spotify_confidence_csr_common.RecordingPluginName.BlockedElementLabels,
|
|
11144
11144
|
options: {},
|
|
11145
11145
|
observer: () => () => {},
|
|
11146
11146
|
eventProcessor: (event) => {
|
|
@@ -11158,7 +11158,7 @@ function blockedElementLabelsPlugin() {
|
|
|
11158
11158
|
function clipboardActionsPlugin() {
|
|
11159
11159
|
let getId;
|
|
11160
11160
|
return {
|
|
11161
|
-
name:
|
|
11161
|
+
name: _spotify_confidence_csr_common.RecordingPluginName.Clipboard,
|
|
11162
11162
|
options: {},
|
|
11163
11163
|
getMirror: ({ nodeMirror }) => {
|
|
11164
11164
|
getId = (node) => nodeMirror.getId(node);
|
|
@@ -11190,7 +11190,7 @@ function clipboardActionsPlugin() {
|
|
|
11190
11190
|
function clickModifiersPlugin() {
|
|
11191
11191
|
let pendingClick = null;
|
|
11192
11192
|
return {
|
|
11193
|
-
name:
|
|
11193
|
+
name: _spotify_confidence_csr_common.RecordingPluginName.ClickModifiers,
|
|
11194
11194
|
options: {},
|
|
11195
11195
|
observer: (_callback, win) => {
|
|
11196
11196
|
const onClick = (event) => {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RecordingEventType } from "@spotify-confidence/csr-common";
|
|
1
|
+
import { RecordingEventType, RecordingPluginName } from "@spotify-confidence/csr-common";
|
|
2
2
|
//#region src/types.ts
|
|
3
3
|
const DEFAULT_MASK_SELECTORS = ["[data-csr-mask]"];
|
|
4
4
|
const DEFAULT_BLOCK_SELECTORS = ["[data-csr-block]", "video"];
|
|
@@ -104,7 +104,7 @@ var Recorder = class Recorder {
|
|
|
104
104
|
if (typeof document !== "undefined") {
|
|
105
105
|
this.visibilityHandler = () => {
|
|
106
106
|
const data = {
|
|
107
|
-
plugin:
|
|
107
|
+
plugin: RecordingPluginName.TabVisibility,
|
|
108
108
|
payload: { hidden: document.hidden }
|
|
109
109
|
};
|
|
110
110
|
this.onEvent({
|
|
@@ -126,7 +126,7 @@ var Recorder = class Recorder {
|
|
|
126
126
|
}
|
|
127
127
|
emitNetworkRequest(payload) {
|
|
128
128
|
const data = {
|
|
129
|
-
plugin:
|
|
129
|
+
plugin: RecordingPluginName.NetworkRequest,
|
|
130
130
|
payload
|
|
131
131
|
};
|
|
132
132
|
this.onEvent({
|
|
@@ -234,7 +234,7 @@ var Recorder = class Recorder {
|
|
|
234
234
|
const paramTo = this.parameterizeRoute(to);
|
|
235
235
|
if (paramFrom === paramTo) return;
|
|
236
236
|
const data = {
|
|
237
|
-
plugin:
|
|
237
|
+
plugin: RecordingPluginName.RouteChange,
|
|
238
238
|
payload: {
|
|
239
239
|
from: paramFrom,
|
|
240
240
|
to: paramTo,
|
|
@@ -11139,7 +11139,7 @@ function labelBlockedElement(node) {
|
|
|
11139
11139
|
*/
|
|
11140
11140
|
function blockedElementLabelsPlugin() {
|
|
11141
11141
|
return {
|
|
11142
|
-
name:
|
|
11142
|
+
name: RecordingPluginName.BlockedElementLabels,
|
|
11143
11143
|
options: {},
|
|
11144
11144
|
observer: () => () => {},
|
|
11145
11145
|
eventProcessor: (event) => {
|
|
@@ -11157,7 +11157,7 @@ function blockedElementLabelsPlugin() {
|
|
|
11157
11157
|
function clipboardActionsPlugin() {
|
|
11158
11158
|
let getId;
|
|
11159
11159
|
return {
|
|
11160
|
-
name:
|
|
11160
|
+
name: RecordingPluginName.Clipboard,
|
|
11161
11161
|
options: {},
|
|
11162
11162
|
getMirror: ({ nodeMirror }) => {
|
|
11163
11163
|
getId = (node) => nodeMirror.getId(node);
|
|
@@ -11189,7 +11189,7 @@ function clipboardActionsPlugin() {
|
|
|
11189
11189
|
function clickModifiersPlugin() {
|
|
11190
11190
|
let pendingClick = null;
|
|
11191
11191
|
return {
|
|
11192
|
-
name:
|
|
11192
|
+
name: RecordingPluginName.ClickModifiers,
|
|
11193
11193
|
options: {},
|
|
11194
11194
|
observer: (_callback, win) => {
|
|
11195
11195
|
const onClick = (event) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spotify-confidence/csr-recorder",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.17",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/spotify/confidence-sdk-js.git",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@rrweb/rrweb-plugin-console-record": "^2.0.1",
|
|
38
|
-
"@spotify-confidence/csr-common": "0.18.
|
|
38
|
+
"@spotify-confidence/csr-common": "0.18.9",
|
|
39
39
|
"rrweb": "^2.0.1"
|
|
40
40
|
},
|
|
41
41
|
"module": "./dist/index.js",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { RecordingPluginName } from '@spotify-confidence/csr-common';
|
|
4
5
|
import { RrwebEngine } from './rrweb-engine';
|
|
5
6
|
|
|
6
7
|
const recordSpy = vi.fn().mockReturnValue(() => {});
|
|
@@ -61,7 +62,7 @@ describe('RrwebEngine', () => {
|
|
|
61
62
|
it('rebuilds blocked elements as labelled inert placeholders', () => {
|
|
62
63
|
new RrwebEngine().start({}, () => {});
|
|
63
64
|
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
64
|
-
({ name }: { name: string }) => name ===
|
|
65
|
+
({ name }: { name: string }) => name === RecordingPluginName.BlockedElementLabels,
|
|
65
66
|
);
|
|
66
67
|
const event = {
|
|
67
68
|
type: 2,
|
|
@@ -120,7 +121,9 @@ describe('RrwebEngine', () => {
|
|
|
120
121
|
|
|
121
122
|
it('records copy, cut, and paste actions without reading clipboard contents', () => {
|
|
122
123
|
new RrwebEngine().start({}, () => {});
|
|
123
|
-
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
124
|
+
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
125
|
+
({ name }: { name: string }) => name === RecordingPluginName.Clipboard,
|
|
126
|
+
);
|
|
124
127
|
const getId = vi.fn().mockReturnValue(42);
|
|
125
128
|
plugin.getMirror({ nodeMirror: { getId } });
|
|
126
129
|
const callback = vi.fn();
|
|
@@ -154,7 +157,7 @@ describe('RrwebEngine', () => {
|
|
|
154
157
|
it('keeps native click modifiers through a browser microtask checkpoint', async () => {
|
|
155
158
|
new RrwebEngine().start({}, () => {});
|
|
156
159
|
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
157
|
-
({ name }: { name: string }) => name ===
|
|
160
|
+
({ name }: { name: string }) => name === RecordingPluginName.ClickModifiers,
|
|
158
161
|
);
|
|
159
162
|
const removeObserver = plugin.observer(() => {}, window);
|
|
160
163
|
|
|
@@ -200,7 +203,7 @@ describe('RrwebEngine', () => {
|
|
|
200
203
|
it('does not add stale modifiers to a later click', async () => {
|
|
201
204
|
new RrwebEngine().start({}, () => {});
|
|
202
205
|
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
203
|
-
({ name }: { name: string }) => name ===
|
|
206
|
+
({ name }: { name: string }) => name === RecordingPluginName.ClickModifiers,
|
|
204
207
|
);
|
|
205
208
|
const removeObserver = plugin.observer(() => {}, window);
|
|
206
209
|
document.dispatchEvent(new MouseEvent('click', { metaKey: true }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConsoleLogLevel, type RecordingEvent } from '@spotify-confidence/csr-common';
|
|
1
|
+
import { RecordingPluginName, type ConsoleLogLevel, type RecordingEvent } from '@spotify-confidence/csr-common';
|
|
2
2
|
import { RecordingConfig, DEFAULT_MASK_SELECTORS, DEFAULT_BLOCK_SELECTORS } from '../types';
|
|
3
3
|
import { RecordingEngine } from './index';
|
|
4
4
|
import { EventType, IncrementalSource, MouseInteractions, record, takeFullSnapshot, type recordOptions } from 'rrweb';
|
|
@@ -42,7 +42,7 @@ function labelBlockedElement(node: SerializedNode): void {
|
|
|
42
42
|
*/
|
|
43
43
|
function blockedElementLabelsPlugin(): RrwebPlugin {
|
|
44
44
|
return {
|
|
45
|
-
name:
|
|
45
|
+
name: RecordingPluginName.BlockedElementLabels,
|
|
46
46
|
options: {},
|
|
47
47
|
observer: () => () => {},
|
|
48
48
|
eventProcessor: event => {
|
|
@@ -68,7 +68,7 @@ function clipboardActionsPlugin(): RrwebPlugin {
|
|
|
68
68
|
let getId: ((node: Node) => number) | undefined;
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
|
-
name:
|
|
71
|
+
name: RecordingPluginName.Clipboard,
|
|
72
72
|
options: {},
|
|
73
73
|
getMirror: ({ nodeMirror }) => {
|
|
74
74
|
getId = node => nodeMirror.getId(node);
|
|
@@ -99,7 +99,7 @@ function clickModifiersPlugin(): RrwebPlugin {
|
|
|
99
99
|
let pendingClick: ClickModifiers | null = null;
|
|
100
100
|
|
|
101
101
|
return {
|
|
102
|
-
name:
|
|
102
|
+
name: RecordingPluginName.ClickModifiers,
|
|
103
103
|
options: {},
|
|
104
104
|
observer: (_callback, win) => {
|
|
105
105
|
const onClick = (event: Event) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
-
import { RecordingEvent, RecordingEventType,
|
|
3
|
+
import { RecordingEvent, RecordingEventType, RecordingPluginName } from '@spotify-confidence/csr-common';
|
|
4
4
|
import { Recorder } from './recorder';
|
|
5
5
|
import { RecordingEngine } from './engine';
|
|
6
6
|
|
|
@@ -32,10 +32,11 @@ class MockEngine implements RecordingEngine {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function routeChangeEvents(onEvent: ReturnType<typeof vi.fn>) {
|
|
35
|
-
return
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
return onEvent.mock.calls.flatMap(([event]: [RecordingEvent]) =>
|
|
36
|
+
event.type === RecordingEventType.Plugin && event.data.plugin === RecordingPluginName.RouteChange
|
|
37
|
+
? [event.data.payload]
|
|
38
|
+
: [],
|
|
39
|
+
);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
describe('Recorder route change capture', () => {
|
package/src/recorder.test.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
-
import { RecordingEvent, RecordingEventType,
|
|
2
|
+
import { RecordingEvent, RecordingEventType, RecordingPluginName } from '@spotify-confidence/csr-common';
|
|
3
3
|
import { Recorder } from './recorder';
|
|
4
4
|
import { RecordingEngine } from './engine';
|
|
5
5
|
import { RecorderState } from './types';
|
|
6
6
|
|
|
7
|
+
const networkRequestEvents = (onEvent: ReturnType<typeof vi.fn>) =>
|
|
8
|
+
onEvent.mock.calls.flatMap(([event]: [RecordingEvent]) =>
|
|
9
|
+
event.type === RecordingEventType.Plugin && event.data.plugin === RecordingPluginName.NetworkRequest
|
|
10
|
+
? [event.data]
|
|
11
|
+
: [],
|
|
12
|
+
);
|
|
13
|
+
|
|
7
14
|
function makeEvent(timestamp: number): RecordingEvent {
|
|
8
15
|
return { type: RecordingEventType.Meta, timestamp, data: {} };
|
|
9
16
|
}
|
|
@@ -130,12 +137,10 @@ describe('Recorder network request capture', () => {
|
|
|
130
137
|
|
|
131
138
|
await globalThis.fetch('https://api.example.com/data');
|
|
132
139
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
expect(
|
|
137
|
-
const data = pluginEvents[0].data as NetworkRequestPluginData;
|
|
138
|
-
expect(data.plugin).toBe('csr:networkRequest');
|
|
140
|
+
const events = networkRequestEvents(onEvent);
|
|
141
|
+
expect(events).toHaveLength(1);
|
|
142
|
+
const data = events[0];
|
|
143
|
+
expect(data.plugin).toBe(RecordingPluginName.NetworkRequest);
|
|
139
144
|
expect(data.payload.initiator).toBe('fetch');
|
|
140
145
|
expect(data.payload.method).toBe('GET');
|
|
141
146
|
expect(data.payload.url).toBe('https://api.example.com/data');
|
|
@@ -156,11 +161,9 @@ describe('Recorder network request capture', () => {
|
|
|
156
161
|
|
|
157
162
|
await globalThis.fetch('https://api.example.com/data').catch(() => {});
|
|
158
163
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(pluginEvents).toHaveLength(1);
|
|
163
|
-
const data = pluginEvents[0].data as NetworkRequestPluginData;
|
|
164
|
+
const events = networkRequestEvents(onEvent);
|
|
165
|
+
expect(events).toHaveLength(1);
|
|
166
|
+
const data = events[0];
|
|
164
167
|
expect(data.payload.status).toBe(0);
|
|
165
168
|
|
|
166
169
|
recorder.stop();
|
|
@@ -177,10 +180,7 @@ describe('Recorder network request capture', () => {
|
|
|
177
180
|
|
|
178
181
|
await globalThis.fetch('https://api.example.com/data', { method: 'post' });
|
|
179
182
|
|
|
180
|
-
const
|
|
181
|
-
.map(([e]: [RecordingEvent]) => e)
|
|
182
|
-
.filter(e => e.type === RecordingEventType.Plugin);
|
|
183
|
-
const data = pluginEvents[0].data as NetworkRequestPluginData;
|
|
183
|
+
const data = networkRequestEvents(onEvent)[0];
|
|
184
184
|
expect(data.payload.method).toBe('POST');
|
|
185
185
|
|
|
186
186
|
recorder.stop();
|
|
@@ -197,10 +197,7 @@ describe('Recorder network request capture', () => {
|
|
|
197
197
|
|
|
198
198
|
await globalThis.fetch(new Request('https://api.example.com/data', { method: 'DELETE' }));
|
|
199
199
|
|
|
200
|
-
const
|
|
201
|
-
.map(([e]: [RecordingEvent]) => e)
|
|
202
|
-
.filter(e => e.type === RecordingEventType.Plugin);
|
|
203
|
-
const data = pluginEvents[0].data as NetworkRequestPluginData;
|
|
200
|
+
const data = networkRequestEvents(onEvent)[0];
|
|
204
201
|
expect(data.payload.method).toBe('DELETE');
|
|
205
202
|
expect(data.payload.url).toBe('https://api.example.com/data');
|
|
206
203
|
|
|
@@ -228,7 +225,7 @@ describe('Recorder network request capture', () => {
|
|
|
228
225
|
}),
|
|
229
226
|
});
|
|
230
227
|
|
|
231
|
-
const data = onEvent
|
|
228
|
+
const data = networkRequestEvents(onEvent)[0];
|
|
232
229
|
expect(data.payload.graphql).toEqual({ operationName: 'GetUser' });
|
|
233
230
|
expect(JSON.stringify(data.payload)).not.toContain('private-user-id');
|
|
234
231
|
|
package/src/recorder.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RecordingEvent,
|
|
3
3
|
RecordingEventType,
|
|
4
|
+
RecordingPluginName,
|
|
4
5
|
type TabVisibilityPluginData,
|
|
5
6
|
type NetworkRequestPluginData,
|
|
6
7
|
type RouteChangePluginData,
|
|
@@ -54,7 +55,7 @@ export class Recorder {
|
|
|
54
55
|
if (typeof document !== 'undefined') {
|
|
55
56
|
this.visibilityHandler = () => {
|
|
56
57
|
const data: TabVisibilityPluginData = {
|
|
57
|
-
plugin:
|
|
58
|
+
plugin: RecordingPluginName.TabVisibility,
|
|
58
59
|
payload: { hidden: document.hidden },
|
|
59
60
|
};
|
|
60
61
|
this.onEvent({
|
|
@@ -86,7 +87,7 @@ export class Recorder {
|
|
|
86
87
|
|
|
87
88
|
private emitNetworkRequest(payload: NetworkRequestPluginData['payload']): void {
|
|
88
89
|
const data: NetworkRequestPluginData = {
|
|
89
|
-
plugin:
|
|
90
|
+
plugin: RecordingPluginName.NetworkRequest,
|
|
90
91
|
payload,
|
|
91
92
|
};
|
|
92
93
|
this.onEvent({
|
|
@@ -216,7 +217,7 @@ export class Recorder {
|
|
|
216
217
|
const paramTo = this.parameterizeRoute(to);
|
|
217
218
|
if (paramFrom === paramTo) return;
|
|
218
219
|
const data: RouteChangePluginData = {
|
|
219
|
-
plugin:
|
|
220
|
+
plugin: RecordingPluginName.RouteChange,
|
|
220
221
|
payload: { from: paramFrom, to: paramTo, trigger },
|
|
221
222
|
};
|
|
222
223
|
this.onEvent({
|