@spotify-confidence/csr-recorder 0.17.12 → 0.17.13
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 +44 -1
- package/dist/index.js +44 -1
- package/package.json +2 -2
- package/src/engine/rrweb-engine.test.ts +66 -1
- package/src/engine/rrweb-engine.ts +55 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.13](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.12...csr-recorder-v0.17.13) (2026-08-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ✨ New Features
|
|
7
|
+
|
|
8
|
+
* **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))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @spotify-confidence/csr-common bumped to 0.18.7
|
|
16
|
+
|
|
3
17
|
## [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
18
|
|
|
5
19
|
|
package/dist/index.cjs
CHANGED
|
@@ -11115,6 +11115,49 @@ 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
|
+
pendingClick = {
|
|
11131
|
+
button: click.button,
|
|
11132
|
+
altKey: click.altKey,
|
|
11133
|
+
ctrlKey: click.ctrlKey,
|
|
11134
|
+
metaKey: click.metaKey,
|
|
11135
|
+
shiftKey: click.shiftKey
|
|
11136
|
+
};
|
|
11137
|
+
queueMicrotask(() => {
|
|
11138
|
+
pendingClick = null;
|
|
11139
|
+
});
|
|
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
|
+
/**
|
|
11118
11161
|
* rrweb adapter — bundles rrweb so consumers don't take a peer-dep on it.
|
|
11119
11162
|
*/
|
|
11120
11163
|
var RrwebEngine = class {
|
|
@@ -11122,7 +11165,7 @@ var RrwebEngine = class {
|
|
|
11122
11165
|
start(config, onEvent) {
|
|
11123
11166
|
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
|
|
11124
11167
|
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
|
|
11125
|
-
const plugins = [];
|
|
11168
|
+
const plugins = [clickModifiersPlugin()];
|
|
11126
11169
|
const { captureConsoleLogs } = config;
|
|
11127
11170
|
if (captureConsoleLogs) {
|
|
11128
11171
|
const levels = captureConsoleLogs === true ? ALL_CONSOLE_LEVELS : captureConsoleLogs.levels;
|
package/dist/index.js
CHANGED
|
@@ -11114,6 +11114,49 @@ 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
|
+
pendingClick = {
|
|
11130
|
+
button: click.button,
|
|
11131
|
+
altKey: click.altKey,
|
|
11132
|
+
ctrlKey: click.ctrlKey,
|
|
11133
|
+
metaKey: click.metaKey,
|
|
11134
|
+
shiftKey: click.shiftKey
|
|
11135
|
+
};
|
|
11136
|
+
queueMicrotask(() => {
|
|
11137
|
+
pendingClick = null;
|
|
11138
|
+
});
|
|
11139
|
+
};
|
|
11140
|
+
win.addEventListener("click", onClick, true);
|
|
11141
|
+
return () => win.removeEventListener("click", onClick, true);
|
|
11142
|
+
},
|
|
11143
|
+
eventProcessor: (event) => {
|
|
11144
|
+
if (pendingClick && event.type === EventType.IncrementalSnapshot && event.data.source === IncrementalSource.MouseInteraction && event.data.type === MouseInteractions.Click) {
|
|
11145
|
+
const click = pendingClick;
|
|
11146
|
+
pendingClick = null;
|
|
11147
|
+
return {
|
|
11148
|
+
...event,
|
|
11149
|
+
data: {
|
|
11150
|
+
...event.data,
|
|
11151
|
+
...click
|
|
11152
|
+
}
|
|
11153
|
+
};
|
|
11154
|
+
}
|
|
11155
|
+
return event;
|
|
11156
|
+
}
|
|
11157
|
+
};
|
|
11158
|
+
}
|
|
11159
|
+
/**
|
|
11117
11160
|
* rrweb adapter — bundles rrweb so consumers don't take a peer-dep on it.
|
|
11118
11161
|
*/
|
|
11119
11162
|
var RrwebEngine = class {
|
|
@@ -11121,7 +11164,7 @@ var RrwebEngine = class {
|
|
|
11121
11164
|
start(config, onEvent) {
|
|
11122
11165
|
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
|
|
11123
11166
|
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
|
|
11124
|
-
const plugins = [];
|
|
11167
|
+
const plugins = [clickModifiersPlugin()];
|
|
11125
11168
|
const { captureConsoleLogs } = config;
|
|
11126
11169
|
if (captureConsoleLogs) {
|
|
11127
11170
|
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.13",
|
|
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,66 @@ describe('RrwebEngine', () => {
|
|
|
60
63
|
new RrwebEngine().start({}, () => {});
|
|
61
64
|
expect(recordSpy.mock.calls[0][0].slimDOMOptions).toBe('all');
|
|
62
65
|
});
|
|
66
|
+
|
|
67
|
+
it('adds native click modifier keys to the rrweb click event', () => {
|
|
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
|
+
|
|
84
|
+
expect(
|
|
85
|
+
plugin.eventProcessor({
|
|
86
|
+
type: 3,
|
|
87
|
+
timestamp: 1,
|
|
88
|
+
data: { source: 2, type: 2, id: 7, x: 10, y: 20 },
|
|
89
|
+
}),
|
|
90
|
+
).toEqual({
|
|
91
|
+
type: 3,
|
|
92
|
+
timestamp: 1,
|
|
93
|
+
data: {
|
|
94
|
+
source: 2,
|
|
95
|
+
type: 2,
|
|
96
|
+
id: 7,
|
|
97
|
+
x: 10,
|
|
98
|
+
y: 20,
|
|
99
|
+
button: 0,
|
|
100
|
+
altKey: true,
|
|
101
|
+
ctrlKey: true,
|
|
102
|
+
metaKey: true,
|
|
103
|
+
shiftKey: true,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
removeObserver();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('does not add stale modifiers to a later click', async () => {
|
|
111
|
+
new RrwebEngine().start({}, () => {});
|
|
112
|
+
const plugin = recordSpy.mock.calls[0][0].plugins.find(
|
|
113
|
+
({ name }: { name: string }) => name === 'csr/click-modifiers@1',
|
|
114
|
+
);
|
|
115
|
+
const removeObserver = plugin.observer(() => {}, window);
|
|
116
|
+
document.dispatchEvent(new MouseEvent('click', { metaKey: true }));
|
|
117
|
+
await Promise.resolve();
|
|
118
|
+
|
|
119
|
+
const event = {
|
|
120
|
+
type: 3,
|
|
121
|
+
timestamp: 1,
|
|
122
|
+
data: { source: 2, type: 2, id: 7 },
|
|
123
|
+
};
|
|
124
|
+
expect(plugin.eventProcessor(event)).toBe(event);
|
|
125
|
+
|
|
126
|
+
removeObserver();
|
|
127
|
+
});
|
|
63
128
|
});
|
|
@@ -1,11 +1,63 @@
|
|
|
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
|
+
pendingClick = {
|
|
28
|
+
button: click.button,
|
|
29
|
+
altKey: click.altKey,
|
|
30
|
+
ctrlKey: click.ctrlKey,
|
|
31
|
+
metaKey: click.metaKey,
|
|
32
|
+
shiftKey: click.shiftKey,
|
|
33
|
+
};
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
pendingClick = null;
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
win.addEventListener('click', onClick, true);
|
|
40
|
+
return () => win.removeEventListener('click', onClick, true);
|
|
41
|
+
},
|
|
42
|
+
eventProcessor: event => {
|
|
43
|
+
if (
|
|
44
|
+
pendingClick &&
|
|
45
|
+
event.type === EventType.IncrementalSnapshot &&
|
|
46
|
+
event.data.source === IncrementalSource.MouseInteraction &&
|
|
47
|
+
event.data.type === MouseInteractions.Click
|
|
48
|
+
) {
|
|
49
|
+
const click = pendingClick;
|
|
50
|
+
pendingClick = null;
|
|
51
|
+
return {
|
|
52
|
+
...event,
|
|
53
|
+
data: { ...event.data, ...click },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return event;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
9
61
|
/**
|
|
10
62
|
* rrweb adapter — bundles rrweb so consumers don't take a peer-dep on it.
|
|
11
63
|
*/
|
|
@@ -16,7 +68,7 @@ export class RrwebEngine implements RecordingEngine {
|
|
|
16
68
|
const maskSelectors = config.maskSelectors ?? DEFAULT_MASK_SELECTORS;
|
|
17
69
|
const blockSelectors = config.blockSelectors ?? DEFAULT_BLOCK_SELECTORS;
|
|
18
70
|
|
|
19
|
-
const plugins = [];
|
|
71
|
+
const plugins: RrwebPlugin[] = [clickModifiersPlugin()];
|
|
20
72
|
const { captureConsoleLogs } = config;
|
|
21
73
|
if (captureConsoleLogs) {
|
|
22
74
|
const levels = captureConsoleLogs === true ? ALL_CONSOLE_LEVELS : captureConsoleLogs.levels;
|