@spotify-confidence/csr-recorder 0.17.12 → 0.17.14
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 +21 -0
- package/dist/index.cjs +45 -1
- package/dist/index.js +45 -1
- package/package.json +2 -2
- package/src/engine/rrweb-engine.test.ts +69 -1
- package/src/engine/rrweb-engine.ts +56 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.14](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.13...csr-recorder-v0.17.14) (2026-08-20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### 🐛 Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **csr-recorder:** preserve native click modifiers ([#440](https://github.com/spotify/confidence-sdk-js/issues/440)) ([9575f1a](https://github.com/spotify/confidence-sdk-js/commit/9575f1a915a1a71ae16b1bd59053085d2456b0da))
|
|
9
|
+
|
|
10
|
+
## [0.17.13](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.12...csr-recorder-v0.17.13) (2026-08-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ✨ New Features
|
|
14
|
+
|
|
15
|
+
* **csr-recorder:** capture click modifier keys ([#435](https://github.com/spotify/confidence-sdk-js/issues/435)) ([a1e7a96](https://github.com/spotify/confidence-sdk-js/commit/a1e7a96996cd2f361f3b44cc924d6b4d5ff0b003))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Dependencies
|
|
19
|
+
|
|
20
|
+
* The following workspace dependencies were updated
|
|
21
|
+
* dependencies
|
|
22
|
+
* @spotify-confidence/csr-common bumped to 0.18.7
|
|
23
|
+
|
|
3
24
|
## [0.17.12](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.11...csr-recorder-v0.17.12) (2026-08-18)
|
|
4
25
|
|
|
5
26
|
|
package/dist/index.cjs
CHANGED
|
@@ -11115,6 +11115,50 @@ const ALL_CONSOLE_LEVELS = [
|
|
|
11115
11115
|
"info"
|
|
11116
11116
|
];
|
|
11117
11117
|
/**
|
|
11118
|
+
* rrweb does not include modifier keys in mouse-interaction events. Capture
|
|
11119
|
+
* the native click first, then add its safe, non-text metadata to the rrweb
|
|
11120
|
+
* event emitted during the same browser event dispatch.
|
|
11121
|
+
*/
|
|
11122
|
+
function clickModifiersPlugin() {
|
|
11123
|
+
let pendingClick = null;
|
|
11124
|
+
return {
|
|
11125
|
+
name: "csr/click-modifiers@1",
|
|
11126
|
+
options: {},
|
|
11127
|
+
observer: (_callback, win) => {
|
|
11128
|
+
const onClick = (event) => {
|
|
11129
|
+
const click = event;
|
|
11130
|
+
const modifiers = {
|
|
11131
|
+
button: click.button,
|
|
11132
|
+
altKey: click.altKey,
|
|
11133
|
+
ctrlKey: click.ctrlKey,
|
|
11134
|
+
metaKey: click.metaKey,
|
|
11135
|
+
shiftKey: click.shiftKey
|
|
11136
|
+
};
|
|
11137
|
+
pendingClick = modifiers;
|
|
11138
|
+
win.setTimeout(() => {
|
|
11139
|
+
if (pendingClick === modifiers) pendingClick = null;
|
|
11140
|
+
}, 0);
|
|
11141
|
+
};
|
|
11142
|
+
win.addEventListener("click", onClick, true);
|
|
11143
|
+
return () => win.removeEventListener("click", onClick, true);
|
|
11144
|
+
},
|
|
11145
|
+
eventProcessor: (event) => {
|
|
11146
|
+
if (pendingClick && event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.MouseInteraction && event.data.type === MouseInteractions.Click) {
|
|
11147
|
+
const click = pendingClick;
|
|
11148
|
+
pendingClick = null;
|
|
11149
|
+
return {
|
|
11150
|
+
...event,
|
|
11151
|
+
data: {
|
|
11152
|
+
...event.data,
|
|
11153
|
+
...click
|
|
11154
|
+
}
|
|
11155
|
+
};
|
|
11156
|
+
}
|
|
11157
|
+
return event;
|
|
11158
|
+
}
|
|
11159
|
+
};
|
|
11160
|
+
}
|
|
11161
|
+
/**
|
|
11118
11162
|
* rrweb adapter — bundles rrweb so consumers don't take a peer-dep on it.
|
|
11119
11163
|
*/
|
|
11120
11164
|
var RrwebEngine = class {
|
|
@@ -11122,7 +11166,7 @@ var RrwebEngine = class {
|
|
|
11122
11166
|
start(config, onEvent) {
|
|
11123
11167
|
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
|
|
11124
11168
|
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
|
|
11125
|
-
const plugins = [];
|
|
11169
|
+
const plugins = [clickModifiersPlugin()];
|
|
11126
11170
|
const { captureConsoleLogs } = config;
|
|
11127
11171
|
if (captureConsoleLogs) {
|
|
11128
11172
|
const levels = captureConsoleLogs === true ? ALL_CONSOLE_LEVELS : captureConsoleLogs.levels;
|
package/dist/index.js
CHANGED
|
@@ -11114,6 +11114,50 @@ const ALL_CONSOLE_LEVELS = [
|
|
|
11114
11114
|
"info"
|
|
11115
11115
|
];
|
|
11116
11116
|
/**
|
|
11117
|
+
* rrweb does not include modifier keys in mouse-interaction events. Capture
|
|
11118
|
+
* the native click first, then add its safe, non-text metadata to the rrweb
|
|
11119
|
+
* event emitted during the same browser event dispatch.
|
|
11120
|
+
*/
|
|
11121
|
+
function clickModifiersPlugin() {
|
|
11122
|
+
let pendingClick = null;
|
|
11123
|
+
return {
|
|
11124
|
+
name: "csr/click-modifiers@1",
|
|
11125
|
+
options: {},
|
|
11126
|
+
observer: (_callback, win) => {
|
|
11127
|
+
const onClick = (event) => {
|
|
11128
|
+
const click = event;
|
|
11129
|
+
const modifiers = {
|
|
11130
|
+
button: click.button,
|
|
11131
|
+
altKey: click.altKey,
|
|
11132
|
+
ctrlKey: click.ctrlKey,
|
|
11133
|
+
metaKey: click.metaKey,
|
|
11134
|
+
shiftKey: click.shiftKey
|
|
11135
|
+
};
|
|
11136
|
+
pendingClick = modifiers;
|
|
11137
|
+
win.setTimeout(() => {
|
|
11138
|
+
if (pendingClick === modifiers) pendingClick = null;
|
|
11139
|
+
}, 0);
|
|
11140
|
+
};
|
|
11141
|
+
win.addEventListener("click", onClick, true);
|
|
11142
|
+
return () => win.removeEventListener("click", onClick, true);
|
|
11143
|
+
},
|
|
11144
|
+
eventProcessor: (event) => {
|
|
11145
|
+
if (pendingClick && event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.MouseInteraction && event.data.type === MouseInteractions.Click) {
|
|
11146
|
+
const click = pendingClick;
|
|
11147
|
+
pendingClick = null;
|
|
11148
|
+
return {
|
|
11149
|
+
...event,
|
|
11150
|
+
data: {
|
|
11151
|
+
...event.data,
|
|
11152
|
+
...click
|
|
11153
|
+
}
|
|
11154
|
+
};
|
|
11155
|
+
}
|
|
11156
|
+
return event;
|
|
11157
|
+
}
|
|
11158
|
+
};
|
|
11159
|
+
}
|
|
11160
|
+
/**
|
|
11117
11161
|
* rrweb adapter — bundles rrweb so consumers don't take a peer-dep on it.
|
|
11118
11162
|
*/
|
|
11119
11163
|
var RrwebEngine = class {
|
|
@@ -11121,7 +11165,7 @@ var RrwebEngine = class {
|
|
|
11121
11165
|
start(config, onEvent) {
|
|
11122
11166
|
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
|
|
11123
11167
|
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
|
|
11124
|
-
const plugins = [];
|
|
11168
|
+
const plugins = [clickModifiersPlugin()];
|
|
11125
11169
|
const { captureConsoleLogs } = config;
|
|
11126
11170
|
if (captureConsoleLogs) {
|
|
11127
11171
|
const levels = captureConsoleLogs === true ? ALL_CONSOLE_LEVELS : captureConsoleLogs.levels;
|
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.14",
|
|
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.7",
|
|
39
39
|
"rrweb": "^2.0.1"
|
|
40
40
|
},
|
|
41
41
|
"module": "./dist/index.js",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
1
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
4
|
import { RrwebEngine } from './rrweb-engine';
|
|
3
5
|
|
|
4
6
|
const recordSpy = vi.fn().mockReturnValue(() => {});
|
|
5
7
|
|
|
6
|
-
vi.mock('rrweb',
|
|
8
|
+
vi.mock('rrweb', async importOriginal => ({
|
|
9
|
+
...(await importOriginal<typeof import('rrweb')>()),
|
|
7
10
|
record: (opts: unknown) => recordSpy(opts),
|
|
8
11
|
}));
|
|
9
12
|
|
|
@@ -60,4 +63,69 @@ describe('RrwebEngine', () => {
|
|
|
60
63
|
new RrwebEngine().start({}, () => {});
|
|
61
64
|
expect(recordSpy.mock.calls[0][0].slimDOMOptions).toBe('all');
|
|
62
65
|
});
|
|
66
|
+
|
|
67
|
+
it('keeps native click modifiers through a browser microtask checkpoint', async () => {
|
|
68
|
+
new RrwebEngine().start({}, () => {});
|
|
69
|
+
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
70
|
+
({ name }: { name: string }) => name === 'csr/click-modifiers@1',
|
|
71
|
+
);
|
|
72
|
+
const removeObserver = plugin.observer(() => {}, window);
|
|
73
|
+
|
|
74
|
+
document.dispatchEvent(
|
|
75
|
+
new MouseEvent('click', {
|
|
76
|
+
button: 0,
|
|
77
|
+
altKey: true,
|
|
78
|
+
ctrlKey: true,
|
|
79
|
+
metaKey: true,
|
|
80
|
+
shiftKey: true,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
// Browsers can run a microtask checkpoint between the window capture
|
|
84
|
+
// listener above and rrweb's document listener for a trusted click.
|
|
85
|
+
await Promise.resolve();
|
|
86
|
+
|
|
87
|
+
expect(
|
|
88
|
+
plugin.eventProcessor({
|
|
89
|
+
type: 3,
|
|
90
|
+
timestamp: 1,
|
|
91
|
+
data: { source: 2, type: 2, id: 7, x: 10, y: 20 },
|
|
92
|
+
}),
|
|
93
|
+
).toEqual({
|
|
94
|
+
type: 3,
|
|
95
|
+
timestamp: 1,
|
|
96
|
+
data: {
|
|
97
|
+
source: 2,
|
|
98
|
+
type: 2,
|
|
99
|
+
id: 7,
|
|
100
|
+
x: 10,
|
|
101
|
+
y: 20,
|
|
102
|
+
button: 0,
|
|
103
|
+
altKey: true,
|
|
104
|
+
ctrlKey: true,
|
|
105
|
+
metaKey: true,
|
|
106
|
+
shiftKey: true,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
removeObserver();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not add stale modifiers to a later click', async () => {
|
|
114
|
+
new RrwebEngine().start({}, () => {});
|
|
115
|
+
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
116
|
+
({ name }: { name: string }) => name === 'csr/click-modifiers@1',
|
|
117
|
+
);
|
|
118
|
+
const removeObserver = plugin.observer(() => {}, window);
|
|
119
|
+
document.dispatchEvent(new MouseEvent('click', { metaKey: true }));
|
|
120
|
+
await new Promise(resolve => window.setTimeout(resolve, 0));
|
|
121
|
+
|
|
122
|
+
const event = {
|
|
123
|
+
type: 3,
|
|
124
|
+
timestamp: 1,
|
|
125
|
+
data: { source: 2, type: 2, id: 7 },
|
|
126
|
+
};
|
|
127
|
+
expect(plugin.eventProcessor(event)).toBe(event);
|
|
128
|
+
|
|
129
|
+
removeObserver();
|
|
130
|
+
});
|
|
63
131
|
});
|
|
@@ -1,11 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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
|
-
import { record } from 'rrweb';
|
|
4
|
+
import { EventType, IncrementalSource, MouseInteractions, record, type recordOptions } from 'rrweb';
|
|
5
5
|
import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';
|
|
6
6
|
|
|
7
7
|
const ALL_CONSOLE_LEVELS: ConsoleLogLevel[] = ['log', 'warn', 'error', 'debug', 'info'];
|
|
8
8
|
|
|
9
|
+
type RrwebPlugin = NonNullable<recordOptions<RecordingEvent>['plugins']>[number];
|
|
10
|
+
|
|
11
|
+
type ClickModifiers = Pick<MouseEvent, 'button' | 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey'>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* rrweb does not include modifier keys in mouse-interaction events. Capture
|
|
15
|
+
* the native click first, then add its safe, non-text metadata to the rrweb
|
|
16
|
+
* event emitted during the same browser event dispatch.
|
|
17
|
+
*/
|
|
18
|
+
function clickModifiersPlugin(): RrwebPlugin {
|
|
19
|
+
let pendingClick: ClickModifiers | null = null;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
name: 'csr/click-modifiers@1',
|
|
23
|
+
options: {},
|
|
24
|
+
observer: (_callback, win) => {
|
|
25
|
+
const onClick = (event: Event) => {
|
|
26
|
+
const click = event as MouseEvent;
|
|
27
|
+
const modifiers = {
|
|
28
|
+
button: click.button,
|
|
29
|
+
altKey: click.altKey,
|
|
30
|
+
ctrlKey: click.ctrlKey,
|
|
31
|
+
metaKey: click.metaKey,
|
|
32
|
+
shiftKey: click.shiftKey,
|
|
33
|
+
};
|
|
34
|
+
pendingClick = modifiers;
|
|
35
|
+
win.setTimeout(() => {
|
|
36
|
+
if (pendingClick === modifiers) pendingClick = null;
|
|
37
|
+
}, 0);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
win.addEventListener('click', onClick, true);
|
|
41
|
+
return () => win.removeEventListener('click', onClick, true);
|
|
42
|
+
},
|
|
43
|
+
eventProcessor: event => {
|
|
44
|
+
if (
|
|
45
|
+
pendingClick &&
|
|
46
|
+
event.type === EventType.IncrementalSnapshot &&
|
|
47
|
+
event.data.source === IncrementalSource.MouseInteraction &&
|
|
48
|
+
event.data.type === MouseInteractions.Click
|
|
49
|
+
) {
|
|
50
|
+
const click = pendingClick;
|
|
51
|
+
pendingClick = null;
|
|
52
|
+
return {
|
|
53
|
+
...event,
|
|
54
|
+
data: { ...event.data, ...click },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return event;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
9
62
|
/**
|
|
10
63
|
* rrweb adapter — bundles rrweb so consumers don't take a peer-dep on it.
|
|
11
64
|
*/
|
|
@@ -16,7 +69,7 @@ export class RrwebEngine implements RecordingEngine {
|
|
|
16
69
|
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
|
|
17
70
|
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
|
|
18
71
|
|
|
19
|
-
const plugins = [];
|
|
72
|
+
const plugins: RrwebPlugin[] = [clickModifiersPlugin()];
|
|
20
73
|
const { captureConsoleLogs } = config;
|
|
21
74
|
if (captureConsoleLogs) {
|
|
22
75
|
const levels = captureConsoleLogs === true ? ALL_CONSOLE_LEVELS : captureConsoleLogs.levels;
|