@spotify-confidence/csr-recorder 0.17.16 → 0.17.18
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 +28 -0
- package/README.md +1 -1
- package/dist/index.cjs +9 -8
- package/dist/index.js +10 -9
- 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 +46 -7
- package/src/recorder.test.ts +18 -21
- package/src/recorder.ts +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.18](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.17...csr-recorder-v0.17.18) (2026-09-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### 🐛 Bug Fixes
|
|
7
|
+
|
|
8
|
+
* strip query and hash from recording URLs ([#452](https://github.com/spotify/confidence-sdk-js/issues/452)) ([1ce37f3](https://github.com/spotify/confidence-sdk-js/commit/1ce37f3f43c05bfea4a90839a6de42314096bb40))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @spotify-confidence/csr-common bumped to 0.18.10
|
|
16
|
+
|
|
17
|
+
## [0.17.17](https://github.com/spotify/confidence-sdk-js/compare/csr-recorder-v0.17.16...csr-recorder-v0.17.17) (2026-09-07)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### 🐛 Bug Fixes
|
|
21
|
+
|
|
22
|
+
* 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))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Dependencies
|
|
26
|
+
|
|
27
|
+
* The following workspace dependencies were updated
|
|
28
|
+
* dependencies
|
|
29
|
+
* @spotify-confidence/csr-common bumped to 0.18.9
|
|
30
|
+
|
|
3
31
|
## [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
32
|
|
|
5
33
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ const stop = record(event => {}, {
|
|
|
53
53
|
});
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
The same parameterization is also applied to the `href` in rrweb Meta events.
|
|
56
|
+
The same parameterization is also applied to the `href` in rrweb Meta events. Query strings and fragments are removed from Meta event URLs and the session's document referrer before recording.
|
|
57
57
|
|
|
58
58
|
## rrweb version
|
|
59
59
|
|
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({
|
|
@@ -221,12 +221,13 @@ var Recorder = class Recorder {
|
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
223
|
parameterizeHref(href) {
|
|
224
|
+
const strippedHref = (0, _spotify_confidence_csr_common.stripUrlQueryAndHash)(href);
|
|
224
225
|
try {
|
|
225
|
-
const url = new URL(
|
|
226
|
+
const url = new URL(strippedHref);
|
|
226
227
|
url.pathname = this.parameterizeRoute(url.pathname);
|
|
227
228
|
return url.toString();
|
|
228
229
|
} catch (_e) {
|
|
229
|
-
return this.parameterizeRoute(
|
|
230
|
+
return this.parameterizeRoute(strippedHref);
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
emitRouteChange(from, to, trigger) {
|
|
@@ -235,7 +236,7 @@ var Recorder = class Recorder {
|
|
|
235
236
|
const paramTo = this.parameterizeRoute(to);
|
|
236
237
|
if (paramFrom === paramTo) return;
|
|
237
238
|
const data = {
|
|
238
|
-
plugin:
|
|
239
|
+
plugin: _spotify_confidence_csr_common.RecordingPluginName.RouteChange,
|
|
239
240
|
payload: {
|
|
240
241
|
from: paramFrom,
|
|
241
242
|
to: paramTo,
|
|
@@ -11140,7 +11141,7 @@ function labelBlockedElement(node) {
|
|
|
11140
11141
|
*/
|
|
11141
11142
|
function blockedElementLabelsPlugin() {
|
|
11142
11143
|
return {
|
|
11143
|
-
name:
|
|
11144
|
+
name: _spotify_confidence_csr_common.RecordingPluginName.BlockedElementLabels,
|
|
11144
11145
|
options: {},
|
|
11145
11146
|
observer: () => () => {},
|
|
11146
11147
|
eventProcessor: (event) => {
|
|
@@ -11158,7 +11159,7 @@ function blockedElementLabelsPlugin() {
|
|
|
11158
11159
|
function clipboardActionsPlugin() {
|
|
11159
11160
|
let getId;
|
|
11160
11161
|
return {
|
|
11161
|
-
name:
|
|
11162
|
+
name: _spotify_confidence_csr_common.RecordingPluginName.Clipboard,
|
|
11162
11163
|
options: {},
|
|
11163
11164
|
getMirror: ({ nodeMirror }) => {
|
|
11164
11165
|
getId = (node) => nodeMirror.getId(node);
|
|
@@ -11190,7 +11191,7 @@ function clipboardActionsPlugin() {
|
|
|
11190
11191
|
function clickModifiersPlugin() {
|
|
11191
11192
|
let pendingClick = null;
|
|
11192
11193
|
return {
|
|
11193
|
-
name:
|
|
11194
|
+
name: _spotify_confidence_csr_common.RecordingPluginName.ClickModifiers,
|
|
11194
11195
|
options: {},
|
|
11195
11196
|
observer: (_callback, win) => {
|
|
11196
11197
|
const onClick = (event) => {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RecordingEventType } from "@spotify-confidence/csr-common";
|
|
1
|
+
import { RecordingEventType, RecordingPluginName, stripUrlQueryAndHash } 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({
|
|
@@ -220,12 +220,13 @@ var Recorder = class Recorder {
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
parameterizeHref(href) {
|
|
223
|
+
const strippedHref = stripUrlQueryAndHash(href);
|
|
223
224
|
try {
|
|
224
|
-
const url = new URL(
|
|
225
|
+
const url = new URL(strippedHref);
|
|
225
226
|
url.pathname = this.parameterizeRoute(url.pathname);
|
|
226
227
|
return url.toString();
|
|
227
228
|
} catch (_e) {
|
|
228
|
-
return this.parameterizeRoute(
|
|
229
|
+
return this.parameterizeRoute(strippedHref);
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
232
|
emitRouteChange(from, to, trigger) {
|
|
@@ -234,7 +235,7 @@ var Recorder = class Recorder {
|
|
|
234
235
|
const paramTo = this.parameterizeRoute(to);
|
|
235
236
|
if (paramFrom === paramTo) return;
|
|
236
237
|
const data = {
|
|
237
|
-
plugin:
|
|
238
|
+
plugin: RecordingPluginName.RouteChange,
|
|
238
239
|
payload: {
|
|
239
240
|
from: paramFrom,
|
|
240
241
|
to: paramTo,
|
|
@@ -11139,7 +11140,7 @@ function labelBlockedElement(node) {
|
|
|
11139
11140
|
*/
|
|
11140
11141
|
function blockedElementLabelsPlugin() {
|
|
11141
11142
|
return {
|
|
11142
|
-
name:
|
|
11143
|
+
name: RecordingPluginName.BlockedElementLabels,
|
|
11143
11144
|
options: {},
|
|
11144
11145
|
observer: () => () => {},
|
|
11145
11146
|
eventProcessor: (event) => {
|
|
@@ -11157,7 +11158,7 @@ function blockedElementLabelsPlugin() {
|
|
|
11157
11158
|
function clipboardActionsPlugin() {
|
|
11158
11159
|
let getId;
|
|
11159
11160
|
return {
|
|
11160
|
-
name:
|
|
11161
|
+
name: RecordingPluginName.Clipboard,
|
|
11161
11162
|
options: {},
|
|
11162
11163
|
getMirror: ({ nodeMirror }) => {
|
|
11163
11164
|
getId = (node) => nodeMirror.getId(node);
|
|
@@ -11189,7 +11190,7 @@ function clipboardActionsPlugin() {
|
|
|
11189
11190
|
function clickModifiersPlugin() {
|
|
11190
11191
|
let pendingClick = null;
|
|
11191
11192
|
return {
|
|
11192
|
-
name:
|
|
11193
|
+
name: RecordingPluginName.ClickModifiers,
|
|
11193
11194
|
options: {},
|
|
11194
11195
|
observer: (_callback, win) => {
|
|
11195
11196
|
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.18",
|
|
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.10",
|
|
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', () => {
|
|
@@ -233,21 +234,59 @@ describe('Recorder route change capture', () => {
|
|
|
233
234
|
recorder.stop();
|
|
234
235
|
});
|
|
235
236
|
|
|
237
|
+
it.each([
|
|
238
|
+
['https://example.com/users/123?email=alice@example.com#token', 'https://example.com/users/:id'],
|
|
239
|
+
['https://example.com/users/123#token?email=alice@example.com', 'https://example.com/users/:id'],
|
|
240
|
+
['https://example.com/users/123?email=alice@example.com', 'https://example.com/users/:id'],
|
|
241
|
+
['https://example.com/users/123#token', 'https://example.com/users/:id'],
|
|
242
|
+
['/users/123?email=alice@example.com#token', '/users/:id'],
|
|
243
|
+
['invalid-url?email=alice@example.com#token', 'invalid-url'],
|
|
244
|
+
['', ''],
|
|
245
|
+
])('strips query/hash from Meta href %s even with route capture disabled', (href, expected) => {
|
|
246
|
+
const engine = new MockEngine();
|
|
247
|
+
const event: RecordingEvent = {
|
|
248
|
+
type: RecordingEventType.Meta,
|
|
249
|
+
timestamp: 1,
|
|
250
|
+
data: { href, width: 1920, height: 1080 },
|
|
251
|
+
};
|
|
252
|
+
engine.eventsOnStart = [event];
|
|
253
|
+
const onEvent = vi.fn();
|
|
254
|
+
const recorder = new Recorder({ engine, onEvent });
|
|
255
|
+
recorder.start({ captureRouteChanges: false });
|
|
256
|
+
// Later snapshots must be sanitized too.
|
|
257
|
+
engine.emit(event);
|
|
258
|
+
|
|
259
|
+
expect(onEvent).toHaveBeenCalledTimes(2);
|
|
260
|
+
for (const [recorded] of onEvent.mock.calls) {
|
|
261
|
+
expect(recorded).toEqual({ ...event, data: { ...event.data, href: expected } });
|
|
262
|
+
}
|
|
263
|
+
expect(event.data).toEqual({ href, width: 1920, height: 1080 });
|
|
264
|
+
|
|
265
|
+
recorder.stop();
|
|
266
|
+
});
|
|
267
|
+
|
|
236
268
|
it('parameterizes meta href using custom parameterizeRoute', () => {
|
|
237
269
|
const engine = new MockEngine();
|
|
238
270
|
engine.eventsOnStart = [
|
|
239
271
|
{
|
|
240
272
|
type: RecordingEventType.Meta,
|
|
241
273
|
timestamp: 1,
|
|
242
|
-
data: {
|
|
274
|
+
data: {
|
|
275
|
+
href: 'https://example.com/teams/acme-corp/dashboard?email=alice@example.com#token',
|
|
276
|
+
width: 1920,
|
|
277
|
+
height: 1080,
|
|
278
|
+
},
|
|
243
279
|
},
|
|
244
280
|
];
|
|
245
281
|
const onEvent = vi.fn();
|
|
246
282
|
const recorder = new Recorder({ engine, onEvent });
|
|
283
|
+
const parameterizeRoute = vi.fn((route: string) => route.replace(/\/teams\/[^/]+/, '/teams/:slug'));
|
|
247
284
|
recorder.start({
|
|
248
|
-
parameterizeRoute
|
|
285
|
+
parameterizeRoute,
|
|
249
286
|
});
|
|
250
287
|
|
|
288
|
+
expect(parameterizeRoute).toHaveBeenCalledWith('/teams/acme-corp/dashboard');
|
|
289
|
+
|
|
251
290
|
const metaEvents = (onEvent.mock.calls as [RecordingEvent][])
|
|
252
291
|
.map(([e]) => e)
|
|
253
292
|
.filter(e => e.type === RecordingEventType.Meta);
|
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,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RecordingEvent,
|
|
3
3
|
RecordingEventType,
|
|
4
|
+
RecordingPluginName,
|
|
5
|
+
stripUrlQueryAndHash,
|
|
4
6
|
type TabVisibilityPluginData,
|
|
5
7
|
type NetworkRequestPluginData,
|
|
6
8
|
type RouteChangePluginData,
|
|
@@ -54,7 +56,7 @@ export class Recorder {
|
|
|
54
56
|
if (typeof document !== 'undefined') {
|
|
55
57
|
this.visibilityHandler = () => {
|
|
56
58
|
const data: TabVisibilityPluginData = {
|
|
57
|
-
plugin:
|
|
59
|
+
plugin: RecordingPluginName.TabVisibility,
|
|
58
60
|
payload: { hidden: document.hidden },
|
|
59
61
|
};
|
|
60
62
|
this.onEvent({
|
|
@@ -86,7 +88,7 @@ export class Recorder {
|
|
|
86
88
|
|
|
87
89
|
private emitNetworkRequest(payload: NetworkRequestPluginData['payload']): void {
|
|
88
90
|
const data: NetworkRequestPluginData = {
|
|
89
|
-
plugin:
|
|
91
|
+
plugin: RecordingPluginName.NetworkRequest,
|
|
90
92
|
payload,
|
|
91
93
|
};
|
|
92
94
|
this.onEvent({
|
|
@@ -201,12 +203,13 @@ export class Recorder {
|
|
|
201
203
|
}
|
|
202
204
|
|
|
203
205
|
private parameterizeHref(href: string): string {
|
|
206
|
+
const strippedHref = stripUrlQueryAndHash(href);
|
|
204
207
|
try {
|
|
205
|
-
const url = new URL(
|
|
208
|
+
const url = new URL(strippedHref);
|
|
206
209
|
url.pathname = this.parameterizeRoute(url.pathname);
|
|
207
210
|
return url.toString();
|
|
208
211
|
} catch (_e) {
|
|
209
|
-
return this.parameterizeRoute(
|
|
212
|
+
return this.parameterizeRoute(strippedHref);
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -216,7 +219,7 @@ export class Recorder {
|
|
|
216
219
|
const paramTo = this.parameterizeRoute(to);
|
|
217
220
|
if (paramFrom === paramTo) return;
|
|
218
221
|
const data: RouteChangePluginData = {
|
|
219
|
-
plugin:
|
|
222
|
+
plugin: RecordingPluginName.RouteChange,
|
|
220
223
|
payload: { from: paramFrom, to: paramTo, trigger },
|
|
221
224
|
};
|
|
222
225
|
this.onEvent({
|