@spotify-confidence/csr-common 0.18.7 → 0.18.9
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 +11 -70
- package/dist/index.d.cts +86 -23
- package/dist/index.d.ts +86 -23
- package/dist/index.js +2 -67
- package/dist/session-activity-CJ1wae06.js +153 -0
- package/dist/session-activity-HfuXKqGd.cjs +212 -0
- package/dist/{types-CtBIVgp2.d.cts → types-Bg0UKTVQ.d.cts} +2 -0
- package/dist/{types-CtBIVgp2.d.ts → types-Bg0UKTVQ.d.ts} +2 -0
- package/dist/uploader/index.cjs +2 -0
- package/dist/uploader/index.d.cts +1 -1
- package/dist/uploader/index.d.ts +1 -1
- package/dist/uploader/index.js +2 -0
- package/package.json +1 -1
- package/src/events.ts +79 -21
- package/src/index.ts +9 -0
- package/src/metrics.ts +15 -0
- package/src/session-activity.test.ts +72 -0
- package/src/session-activity.ts +64 -0
- package/src/uploader/create-uploader.test.ts +96 -10
- package/src/uploader/create-uploader.ts +2 -0
- package/src/uploader/types.ts +2 -0
package/src/events.ts
CHANGED
|
@@ -23,6 +23,35 @@ export enum RecordingEventType {
|
|
|
23
23
|
Plugin = 6,
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export const RecordingCustomEventTag = {
|
|
27
|
+
RageClick: 'csr:rageClick',
|
|
28
|
+
FormFieldReEdit: 'csr:formFieldReEdit',
|
|
29
|
+
ScrollBack: 'csr:scrollBack',
|
|
30
|
+
Click: 'csr:click',
|
|
31
|
+
Input: 'csr:input',
|
|
32
|
+
DeadClick: 'csr:deadClick',
|
|
33
|
+
TabUnfocus: 'csr:tabUnfocus',
|
|
34
|
+
TabRefocus: 'csr:tabRefocus',
|
|
35
|
+
RouteChange: 'csr:routeChange',
|
|
36
|
+
ErrorMessage: 'csr:errorMessage',
|
|
37
|
+
DialogOpened: 'csr:dialogOpened',
|
|
38
|
+
IdleGap: 'csr:idleGap',
|
|
39
|
+
AwayGap: 'csr:awayGap',
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export const RecordingPluginName = {
|
|
43
|
+
BlockedElementLabels: 'csr/blocked-element-labels@1',
|
|
44
|
+
Clipboard: 'csr/clipboard@1',
|
|
45
|
+
ClickModifiers: 'csr/click-modifiers@1',
|
|
46
|
+
TabVisibility: 'csr:tabVisibility',
|
|
47
|
+
ConsoleLog: 'rrweb/console@1',
|
|
48
|
+
NetworkRequest: 'csr:networkRequest',
|
|
49
|
+
RouteChange: 'csr:routeChange',
|
|
50
|
+
Tag: 'csr:tag',
|
|
51
|
+
Measure: 'csr:measure',
|
|
52
|
+
FlagEvaluation: 'csr:flagEvaluation',
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
26
55
|
/**
|
|
27
56
|
* Incremental snapshot sub-types.
|
|
28
57
|
*/
|
|
@@ -107,7 +136,7 @@ export type OpaqueIncrementalData = {
|
|
|
107
136
|
export type IncrementalSnapshotData = MouseInteractionData | SelectionData | OpaqueIncrementalData;
|
|
108
137
|
|
|
109
138
|
export type RageClickCustomData = {
|
|
110
|
-
tag:
|
|
139
|
+
tag: typeof RecordingCustomEventTag.RageClick;
|
|
111
140
|
payload: {
|
|
112
141
|
eventId?: string;
|
|
113
142
|
targetId: number;
|
|
@@ -119,7 +148,7 @@ export type RageClickCustomData = {
|
|
|
119
148
|
};
|
|
120
149
|
|
|
121
150
|
export type FormFieldReEditCustomData = {
|
|
122
|
-
tag:
|
|
151
|
+
tag: typeof RecordingCustomEventTag.FormFieldReEdit;
|
|
123
152
|
payload: {
|
|
124
153
|
eventId?: string;
|
|
125
154
|
targetId: number;
|
|
@@ -130,7 +159,7 @@ export type FormFieldReEditCustomData = {
|
|
|
130
159
|
};
|
|
131
160
|
|
|
132
161
|
export type ScrollBackCustomData = {
|
|
133
|
-
tag:
|
|
162
|
+
tag: typeof RecordingCustomEventTag.ScrollBack;
|
|
134
163
|
payload: {
|
|
135
164
|
eventId?: string;
|
|
136
165
|
scrollBackPx: number;
|
|
@@ -148,7 +177,7 @@ export type ElementDescriptor = {
|
|
|
148
177
|
};
|
|
149
178
|
|
|
150
179
|
export type ClickCustomData = {
|
|
151
|
-
tag:
|
|
180
|
+
tag: typeof RecordingCustomEventTag.Click;
|
|
152
181
|
payload: {
|
|
153
182
|
eventId?: string;
|
|
154
183
|
targetId: number;
|
|
@@ -163,7 +192,7 @@ export type ClickCustomData = {
|
|
|
163
192
|
};
|
|
164
193
|
|
|
165
194
|
export type InputCustomData = {
|
|
166
|
-
tag:
|
|
195
|
+
tag: typeof RecordingCustomEventTag.Input;
|
|
167
196
|
payload: {
|
|
168
197
|
eventId?: string;
|
|
169
198
|
targetId: number;
|
|
@@ -171,11 +200,22 @@ export type InputCustomData = {
|
|
|
171
200
|
pathname?: string;
|
|
172
201
|
fieldType?: string;
|
|
173
202
|
hasValue?: boolean;
|
|
203
|
+
source?: 'paste';
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export type ClipboardAction = 'copy' | 'cut' | 'paste';
|
|
208
|
+
|
|
209
|
+
export type ClipboardPluginData = {
|
|
210
|
+
plugin: typeof RecordingPluginName.Clipboard;
|
|
211
|
+
payload: {
|
|
212
|
+
action: ClipboardAction;
|
|
213
|
+
targetId: number;
|
|
174
214
|
};
|
|
175
215
|
};
|
|
176
216
|
|
|
177
217
|
export type DeadClickCustomData = {
|
|
178
|
-
tag:
|
|
218
|
+
tag: typeof RecordingCustomEventTag.DeadClick;
|
|
179
219
|
payload: {
|
|
180
220
|
eventId?: string;
|
|
181
221
|
targetId: number;
|
|
@@ -185,7 +225,7 @@ export type DeadClickCustomData = {
|
|
|
185
225
|
};
|
|
186
226
|
|
|
187
227
|
export type TabUnfocusCustomData = {
|
|
188
|
-
tag:
|
|
228
|
+
tag: typeof RecordingCustomEventTag.TabUnfocus;
|
|
189
229
|
payload: {
|
|
190
230
|
eventId?: string;
|
|
191
231
|
pathname?: string;
|
|
@@ -193,7 +233,7 @@ export type TabUnfocusCustomData = {
|
|
|
193
233
|
};
|
|
194
234
|
|
|
195
235
|
export type TabRefocusCustomData = {
|
|
196
|
-
tag:
|
|
236
|
+
tag: typeof RecordingCustomEventTag.TabRefocus;
|
|
197
237
|
payload: {
|
|
198
238
|
eventId?: string;
|
|
199
239
|
awayDurationMs: number;
|
|
@@ -210,7 +250,7 @@ export type RouteChangePayload = {
|
|
|
210
250
|
};
|
|
211
251
|
|
|
212
252
|
export type RouteChangeCustomData = {
|
|
213
|
-
tag:
|
|
253
|
+
tag: typeof RecordingCustomEventTag.RouteChange;
|
|
214
254
|
// Intersected rather than added to RouteChangePayload so the plugin-event
|
|
215
255
|
// shape (RouteChangePluginData) stays free of analyzer identity.
|
|
216
256
|
payload: RouteChangePayload & { eventId?: string };
|
|
@@ -220,7 +260,7 @@ export type RouteChangeCustomData = {
|
|
|
220
260
|
* Plugin event data emitted by the recorder for tab visibility changes.
|
|
221
261
|
*/
|
|
222
262
|
export type TabVisibilityPluginData = {
|
|
223
|
-
plugin:
|
|
263
|
+
plugin: typeof RecordingPluginName.TabVisibility;
|
|
224
264
|
payload: { hidden: boolean };
|
|
225
265
|
};
|
|
226
266
|
|
|
@@ -231,7 +271,7 @@ export type ConsoleLogLevel = 'log' | 'warn' | 'error' | 'debug' | 'info';
|
|
|
231
271
|
* Shape matches `@rrweb/rrweb-plugin-console-record` LogData.
|
|
232
272
|
*/
|
|
233
273
|
export type ConsoleLogPluginData = {
|
|
234
|
-
plugin:
|
|
274
|
+
plugin: typeof RecordingPluginName.ConsoleLog;
|
|
235
275
|
payload: {
|
|
236
276
|
level: ConsoleLogLevel;
|
|
237
277
|
payload: string[];
|
|
@@ -249,7 +289,7 @@ export type GraphQLRequestMetadata = {
|
|
|
249
289
|
* Plugin event data emitted by the recorder for network requests.
|
|
250
290
|
*/
|
|
251
291
|
export type NetworkRequestPluginData = {
|
|
252
|
-
plugin:
|
|
292
|
+
plugin: typeof RecordingPluginName.NetworkRequest;
|
|
253
293
|
payload: {
|
|
254
294
|
initiator: NetworkRequestInitiator;
|
|
255
295
|
method: string;
|
|
@@ -263,12 +303,12 @@ export type NetworkRequestPluginData = {
|
|
|
263
303
|
};
|
|
264
304
|
|
|
265
305
|
export type RouteChangePluginData = {
|
|
266
|
-
plugin:
|
|
306
|
+
plugin: typeof RecordingPluginName.RouteChange;
|
|
267
307
|
payload: RouteChangePayload;
|
|
268
308
|
};
|
|
269
309
|
|
|
270
310
|
export type TagPluginData = {
|
|
271
|
-
plugin:
|
|
311
|
+
plugin: typeof RecordingPluginName.Tag;
|
|
272
312
|
payload: {
|
|
273
313
|
key: string;
|
|
274
314
|
value?: string;
|
|
@@ -276,7 +316,7 @@ export type TagPluginData = {
|
|
|
276
316
|
};
|
|
277
317
|
|
|
278
318
|
export type MeasurePluginData = {
|
|
279
|
-
plugin:
|
|
319
|
+
plugin: typeof RecordingPluginName.Measure;
|
|
280
320
|
payload: {
|
|
281
321
|
key: string;
|
|
282
322
|
value?: number;
|
|
@@ -284,7 +324,7 @@ export type MeasurePluginData = {
|
|
|
284
324
|
};
|
|
285
325
|
|
|
286
326
|
export type FlagEvaluationPluginData = {
|
|
287
|
-
plugin:
|
|
327
|
+
plugin: typeof RecordingPluginName.FlagEvaluation;
|
|
288
328
|
payload: {
|
|
289
329
|
flagKey: string;
|
|
290
330
|
variant: string;
|
|
@@ -292,8 +332,18 @@ export type FlagEvaluationPluginData = {
|
|
|
292
332
|
};
|
|
293
333
|
};
|
|
294
334
|
|
|
335
|
+
export type PluginEventData =
|
|
336
|
+
| ClipboardPluginData
|
|
337
|
+
| TabVisibilityPluginData
|
|
338
|
+
| ConsoleLogPluginData
|
|
339
|
+
| NetworkRequestPluginData
|
|
340
|
+
| RouteChangePluginData
|
|
341
|
+
| TagPluginData
|
|
342
|
+
| MeasurePluginData
|
|
343
|
+
| FlagEvaluationPluginData;
|
|
344
|
+
|
|
295
345
|
export type ErrorMessageCustomData = {
|
|
296
|
-
tag:
|
|
346
|
+
tag: typeof RecordingCustomEventTag.ErrorMessage;
|
|
297
347
|
payload: {
|
|
298
348
|
eventId?: string;
|
|
299
349
|
text: string;
|
|
@@ -301,7 +351,7 @@ export type ErrorMessageCustomData = {
|
|
|
301
351
|
};
|
|
302
352
|
|
|
303
353
|
export type DialogOpenedCustomData = {
|
|
304
|
-
tag:
|
|
354
|
+
tag: typeof RecordingCustomEventTag.DialogOpened;
|
|
305
355
|
payload: {
|
|
306
356
|
eventId?: string;
|
|
307
357
|
content: string[];
|
|
@@ -309,7 +359,7 @@ export type DialogOpenedCustomData = {
|
|
|
309
359
|
};
|
|
310
360
|
|
|
311
361
|
export type IdleGapCustomData = {
|
|
312
|
-
tag:
|
|
362
|
+
tag: typeof RecordingCustomEventTag.IdleGap;
|
|
313
363
|
payload: {
|
|
314
364
|
eventId?: string;
|
|
315
365
|
visibleGapS: number;
|
|
@@ -320,7 +370,7 @@ export type IdleGapCustomData = {
|
|
|
320
370
|
};
|
|
321
371
|
|
|
322
372
|
export type AwayGapCustomData = {
|
|
323
|
-
tag:
|
|
373
|
+
tag: typeof RecordingCustomEventTag.AwayGap;
|
|
324
374
|
payload: {
|
|
325
375
|
eventId?: string;
|
|
326
376
|
totalGapS: number;
|
|
@@ -371,7 +421,15 @@ export type RecordingEvent =
|
|
|
371
421
|
data: IncrementalSnapshotData;
|
|
372
422
|
}
|
|
373
423
|
| {
|
|
374
|
-
type:
|
|
424
|
+
type: RecordingEventType.Plugin;
|
|
425
|
+
timestamp: number;
|
|
426
|
+
data: PluginEventData;
|
|
427
|
+
}
|
|
428
|
+
| {
|
|
429
|
+
type: Exclude<
|
|
430
|
+
RecordingEventType,
|
|
431
|
+
RecordingEventType.Custom | RecordingEventType.IncrementalSnapshot | RecordingEventType.Plugin
|
|
432
|
+
>;
|
|
375
433
|
timestamp: number;
|
|
376
434
|
data: unknown;
|
|
377
435
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export {
|
|
2
2
|
SerializedNodeType,
|
|
3
3
|
RecordingEventType,
|
|
4
|
+
RecordingCustomEventTag,
|
|
5
|
+
RecordingPluginName,
|
|
4
6
|
IncrementalSource,
|
|
5
7
|
MouseInteractions,
|
|
6
8
|
type RecordingEvent,
|
|
9
|
+
type PluginEventData,
|
|
7
10
|
type IncrementalSnapshotData,
|
|
8
11
|
type MouseInteractionData,
|
|
9
12
|
type OpaqueIncrementalData,
|
|
@@ -12,6 +15,8 @@ export {
|
|
|
12
15
|
type CustomEventData,
|
|
13
16
|
type ClickCustomData,
|
|
14
17
|
type InputCustomData,
|
|
18
|
+
type ClipboardAction,
|
|
19
|
+
type ClipboardPluginData,
|
|
15
20
|
type RageClickCustomData,
|
|
16
21
|
type FormFieldReEditCustomData,
|
|
17
22
|
type ScrollBackCustomData,
|
|
@@ -40,6 +45,10 @@ export {
|
|
|
40
45
|
|
|
41
46
|
export { stripUrl } from './url';
|
|
42
47
|
|
|
48
|
+
export { RecordingMetricKey } from './metrics';
|
|
49
|
+
|
|
50
|
+
export { isSessionActivityEvent, isUserInteractionEvent, isUserInteractionMetric } from './session-activity';
|
|
51
|
+
|
|
43
52
|
export {
|
|
44
53
|
MAX_KEY_LENGTH,
|
|
45
54
|
MAX_TAG_VALUE_LENGTH,
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const RecordingMetricKey = {
|
|
2
|
+
Click: 'clicks',
|
|
3
|
+
Input: 'inputs',
|
|
4
|
+
RageClick: 'rageClicks',
|
|
5
|
+
DeadClick: 'deadClicks',
|
|
6
|
+
ScrollBack: 'scrollBacks',
|
|
7
|
+
TabUnfocus: 'tabUnfocuses',
|
|
8
|
+
ErrorMessage: 'errorMessages',
|
|
9
|
+
FailedNetworkRequest: 'networkRequestsFailed',
|
|
10
|
+
NetworkRequest: 'networkRequests',
|
|
11
|
+
ConsoleLog: 'consoleLogs',
|
|
12
|
+
ConsoleWarning: 'consoleWarnings',
|
|
13
|
+
ConsoleError: 'consoleErrors',
|
|
14
|
+
RouteChange: 'routeChanges',
|
|
15
|
+
} as const;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { IncrementalSource, RecordingCustomEventTag, RecordingEventType, RecordingPluginName } from './events';
|
|
3
|
+
import { RecordingMetricKey } from './metrics';
|
|
4
|
+
import { isSessionActivityEvent, isUserInteractionEvent, isUserInteractionMetric } from './session-activity';
|
|
5
|
+
|
|
6
|
+
describe('session activity', () => {
|
|
7
|
+
it.each([
|
|
8
|
+
{
|
|
9
|
+
type: RecordingEventType.IncrementalSnapshot,
|
|
10
|
+
data: { source: IncrementalSource.MouseMove },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: RecordingEventType.IncrementalSnapshot,
|
|
14
|
+
data: { source: IncrementalSource.MouseInteraction },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: RecordingEventType.Custom,
|
|
18
|
+
data: { tag: RecordingCustomEventTag.Input },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: RecordingEventType.Plugin,
|
|
22
|
+
data: { plugin: RecordingPluginName.RouteChange },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: RecordingEventType.Custom,
|
|
26
|
+
data: { tag: RecordingCustomEventTag.TabUnfocus },
|
|
27
|
+
},
|
|
28
|
+
])('identifies user interaction events', event => {
|
|
29
|
+
expect(isUserInteractionEvent(event)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it.each([
|
|
33
|
+
{
|
|
34
|
+
type: RecordingEventType.IncrementalSnapshot,
|
|
35
|
+
data: { source: IncrementalSource.Mutation },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: RecordingEventType.Plugin,
|
|
39
|
+
data: { plugin: RecordingPluginName.NetworkRequest },
|
|
40
|
+
},
|
|
41
|
+
null,
|
|
42
|
+
{},
|
|
43
|
+
])('ignores passive and malformed events', event => {
|
|
44
|
+
expect(isUserInteractionEvent(event)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('treats page metadata as session activity without treating it as an interaction', () => {
|
|
48
|
+
const event = { type: RecordingEventType.Meta, data: {} };
|
|
49
|
+
|
|
50
|
+
expect(isUserInteractionEvent(event)).toBe(false);
|
|
51
|
+
expect(isSessionActivityEvent(event)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
RecordingMetricKey.Click,
|
|
56
|
+
RecordingMetricKey.Input,
|
|
57
|
+
RecordingMetricKey.RageClick,
|
|
58
|
+
RecordingMetricKey.DeadClick,
|
|
59
|
+
RecordingMetricKey.ScrollBack,
|
|
60
|
+
RecordingMetricKey.TabUnfocus,
|
|
61
|
+
RecordingMetricKey.RouteChange,
|
|
62
|
+
])('identifies user interaction metrics', metricKey => {
|
|
63
|
+
expect(isUserInteractionMetric(metricKey)).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each([RecordingMetricKey.NetworkRequest, RecordingMetricKey.ConsoleError, 'unknownMetric'])(
|
|
67
|
+
'ignores passive metrics',
|
|
68
|
+
metricKey => {
|
|
69
|
+
expect(isUserInteractionMetric(metricKey)).toBe(false);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IncrementalSource, RecordingCustomEventTag, RecordingEventType, RecordingPluginName } from './events';
|
|
2
|
+
import { RecordingMetricKey } from './metrics';
|
|
3
|
+
|
|
4
|
+
const USER_INTERACTION_SOURCES = new Set<number>([
|
|
5
|
+
IncrementalSource.MouseMove,
|
|
6
|
+
IncrementalSource.MouseInteraction,
|
|
7
|
+
IncrementalSource.Scroll,
|
|
8
|
+
IncrementalSource.Input,
|
|
9
|
+
IncrementalSource.TouchMove,
|
|
10
|
+
IncrementalSource.MediaInteraction,
|
|
11
|
+
IncrementalSource.Drag,
|
|
12
|
+
IncrementalSource.Selection,
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const USER_INTERACTION_TAGS = new Set<string>([
|
|
16
|
+
RecordingCustomEventTag.Click,
|
|
17
|
+
RecordingCustomEventTag.Input,
|
|
18
|
+
RecordingCustomEventTag.RageClick,
|
|
19
|
+
RecordingCustomEventTag.DeadClick,
|
|
20
|
+
RecordingCustomEventTag.ScrollBack,
|
|
21
|
+
RecordingCustomEventTag.TabUnfocus,
|
|
22
|
+
RecordingCustomEventTag.TabRefocus,
|
|
23
|
+
RecordingCustomEventTag.FormFieldReEdit,
|
|
24
|
+
RecordingCustomEventTag.RouteChange,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const USER_INTERACTION_PLUGINS = new Set<string>([RecordingPluginName.RouteChange]);
|
|
28
|
+
|
|
29
|
+
const USER_INTERACTION_METRIC_KEYS = new Set<string>([
|
|
30
|
+
RecordingMetricKey.Click,
|
|
31
|
+
RecordingMetricKey.Input,
|
|
32
|
+
RecordingMetricKey.RageClick,
|
|
33
|
+
RecordingMetricKey.DeadClick,
|
|
34
|
+
RecordingMetricKey.ScrollBack,
|
|
35
|
+
RecordingMetricKey.TabUnfocus,
|
|
36
|
+
RecordingMetricKey.RouteChange,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
|
|
40
|
+
|
|
41
|
+
export const isUserInteractionEvent = (event: unknown): boolean => {
|
|
42
|
+
if (!isObject(event) || !isObject(event.data)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (event.type === RecordingEventType.IncrementalSnapshot) {
|
|
47
|
+
return typeof event.data.source === 'number' && USER_INTERACTION_SOURCES.has(event.data.source);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (event.type === RecordingEventType.Custom) {
|
|
51
|
+
return typeof event.data.tag === 'string' && USER_INTERACTION_TAGS.has(event.data.tag);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
event.type === RecordingEventType.Plugin &&
|
|
56
|
+
typeof event.data.plugin === 'string' &&
|
|
57
|
+
USER_INTERACTION_PLUGINS.has(event.data.plugin)
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const isUserInteractionMetric = (metricKey: string): boolean => USER_INTERACTION_METRIC_KEYS.has(metricKey);
|
|
62
|
+
|
|
63
|
+
export const isSessionActivityEvent = (event: unknown): boolean =>
|
|
64
|
+
(isObject(event) && event.type === RecordingEventType.Meta) || isUserInteractionEvent(event);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { IncrementalSource, RecordingEventType, RecordingPluginName } from '../events';
|
|
2
3
|
import type { CreateUploaderOptions } from './types';
|
|
3
4
|
|
|
4
5
|
vi.mock('./worker/worker-script', () => ({
|
|
@@ -61,10 +62,6 @@ function installWorkerStubs(opts?: { workerThrows?: boolean; sharedWorkerThrows?
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function tick() {
|
|
65
|
-
return new Promise(r => setTimeout(r, 0));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
65
|
describe('createUploader', () => {
|
|
69
66
|
const blobUrls: string[] = [];
|
|
70
67
|
|
|
@@ -93,6 +90,7 @@ describe('createUploader', () => {
|
|
|
93
90
|
|
|
94
91
|
afterEach(() => {
|
|
95
92
|
vi.restoreAllMocks();
|
|
93
|
+
vi.unstubAllGlobals();
|
|
96
94
|
blobUrls.length = 0;
|
|
97
95
|
delete (globalThis as Record<string, unknown>).SharedWorker;
|
|
98
96
|
});
|
|
@@ -112,8 +110,14 @@ describe('createUploader', () => {
|
|
|
112
110
|
|
|
113
111
|
const createUploader = await loadCreateUploader();
|
|
114
112
|
const logs: string[] = [];
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
await expect(
|
|
114
|
+
createUploader({
|
|
115
|
+
...DEFAULTS,
|
|
116
|
+
workerMode: 'dedicated',
|
|
117
|
+
debugLogger: m => logs.push(m),
|
|
118
|
+
_welcomeTimeoutMs: 0,
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toThrow(/welcome timeout/);
|
|
117
121
|
|
|
118
122
|
expect(blobUrls).toHaveLength(0);
|
|
119
123
|
expect(logs.some(l => l.includes('via data'))).toBe(true);
|
|
@@ -136,8 +140,14 @@ describe('createUploader', () => {
|
|
|
136
140
|
|
|
137
141
|
const createUploader = await loadCreateUploader();
|
|
138
142
|
const logs: string[] = [];
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
await expect(
|
|
144
|
+
createUploader({
|
|
145
|
+
...DEFAULTS,
|
|
146
|
+
workerMode: 'dedicated',
|
|
147
|
+
debugLogger: m => logs.push(m),
|
|
148
|
+
_welcomeTimeoutMs: 0,
|
|
149
|
+
}),
|
|
150
|
+
).rejects.toThrow(/welcome timeout/);
|
|
141
151
|
|
|
142
152
|
expect(blobUrls).toHaveLength(1);
|
|
143
153
|
expect(constructedUrl).toBe(blobUrls[0]);
|
|
@@ -163,8 +173,14 @@ describe('createUploader', () => {
|
|
|
163
173
|
|
|
164
174
|
const createUploader = await loadCreateUploader();
|
|
165
175
|
const logs: string[] = [];
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
await expect(
|
|
177
|
+
createUploader({
|
|
178
|
+
...DEFAULTS,
|
|
179
|
+
workerMode: 'shared',
|
|
180
|
+
debugLogger: m => logs.push(m),
|
|
181
|
+
_welcomeTimeoutMs: 0,
|
|
182
|
+
}),
|
|
183
|
+
).rejects.toThrow(/welcome timeout/);
|
|
168
184
|
|
|
169
185
|
expect(blobUrls).toHaveLength(1);
|
|
170
186
|
expect(constructedUrl).toBe(blobUrls[0]);
|
|
@@ -309,4 +325,74 @@ describe('createUploader', () => {
|
|
|
309
325
|
);
|
|
310
326
|
});
|
|
311
327
|
});
|
|
328
|
+
|
|
329
|
+
it('marks active and passive frames for the backend inactivity timeout', async () => {
|
|
330
|
+
const messages: unknown[] = [];
|
|
331
|
+
vi.stubGlobal('window', {
|
|
332
|
+
addEventListener() {},
|
|
333
|
+
devicePixelRatio: 1,
|
|
334
|
+
innerHeight: 800,
|
|
335
|
+
innerWidth: 1200,
|
|
336
|
+
location: { origin: 'https://example.com', pathname: '/' },
|
|
337
|
+
screen: { height: 800, width: 1200 },
|
|
338
|
+
});
|
|
339
|
+
vi.stubGlobal('document', {
|
|
340
|
+
addEventListener() {},
|
|
341
|
+
referrer: '',
|
|
342
|
+
visibilityState: 'visible',
|
|
343
|
+
});
|
|
344
|
+
vi.stubGlobal('navigator', {
|
|
345
|
+
language: 'en',
|
|
346
|
+
userAgent: 'test',
|
|
347
|
+
});
|
|
348
|
+
vi.stubGlobal(
|
|
349
|
+
'Worker',
|
|
350
|
+
class {
|
|
351
|
+
onerror: ((event: Event) => void) | null = null;
|
|
352
|
+
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
353
|
+
|
|
354
|
+
postMessage(message: unknown) {
|
|
355
|
+
messages.push(message);
|
|
356
|
+
if (typeof message === 'object' && message !== null && 'type' in message && message.type === 'hello') {
|
|
357
|
+
queueMicrotask(() =>
|
|
358
|
+
this.onmessage?.(
|
|
359
|
+
new MessageEvent('message', {
|
|
360
|
+
data: {
|
|
361
|
+
type: 'welcome',
|
|
362
|
+
result: { sessionId: 'sessions/test', sessionToken: 'token' },
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
vi.stubGlobal('SharedWorker', undefined);
|
|
372
|
+
|
|
373
|
+
const createUploader = await loadCreateUploader();
|
|
374
|
+
const uploader = await createUploader({ ...DEFAULTS, workerMode: 'dedicated' });
|
|
375
|
+
|
|
376
|
+
uploader?.({
|
|
377
|
+
type: RecordingEventType.Plugin,
|
|
378
|
+
data: { plugin: RecordingPluginName.NetworkRequest },
|
|
379
|
+
});
|
|
380
|
+
uploader?.({
|
|
381
|
+
type: RecordingEventType.IncrementalSnapshot,
|
|
382
|
+
data: { source: IncrementalSource.MouseInteraction },
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(messages).toEqual(
|
|
386
|
+
expect.arrayContaining([
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
type: 'frame',
|
|
389
|
+
frame: expect.objectContaining({ userActivity: false }),
|
|
390
|
+
}),
|
|
391
|
+
expect.objectContaining({
|
|
392
|
+
type: 'frame',
|
|
393
|
+
frame: expect.objectContaining({ userActivity: true }),
|
|
394
|
+
}),
|
|
395
|
+
]),
|
|
396
|
+
);
|
|
397
|
+
});
|
|
312
398
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CreateUploaderOptions, Frame, Uploader } from './types';
|
|
2
2
|
import { ClientContext, collectUserAgentContext } from './client-context';
|
|
3
|
+
import { isSessionActivityEvent } from '../session-activity';
|
|
3
4
|
import { workerScript } from './worker/worker-script';
|
|
4
5
|
|
|
5
6
|
const STORAGE_TAB_ID = 'csr:tabId';
|
|
@@ -213,6 +214,7 @@ export async function createUploader(opts: CreateUploaderOptions): Promise<Uploa
|
|
|
213
214
|
tabId: effectiveTabId,
|
|
214
215
|
eventCounter: counter,
|
|
215
216
|
data: event,
|
|
217
|
+
userActivity: isSessionActivityEvent(event),
|
|
216
218
|
...(nextAdoptionMeta ?? {}),
|
|
217
219
|
};
|
|
218
220
|
counter += 1;
|
package/src/uploader/types.ts
CHANGED
|
@@ -94,6 +94,8 @@ export interface Frame {
|
|
|
94
94
|
eventCounter: number;
|
|
95
95
|
/** Opaque payload from the recorder. */
|
|
96
96
|
data: unknown;
|
|
97
|
+
/** Whether the frame should refresh the backend's session inactivity timeout. */
|
|
98
|
+
userActivity?: boolean;
|
|
97
99
|
/** Set only on the first frame emitted after the tab was adopted into a different session. */
|
|
98
100
|
adoptedFromSessionId?: string;
|
|
99
101
|
/** Epoch millis of the adoption event. */
|