@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/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: 'csr:rageClick';
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: 'csr:formFieldReEdit';
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: 'csr:scrollBack';
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: 'csr:click';
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: 'csr:input';
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: 'csr:deadClick';
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: 'csr:tabUnfocus';
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: 'csr:tabRefocus';
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: 'csr:routeChange';
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: 'csr:tabVisibility';
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: 'rrweb/console@1';
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: 'csr:networkRequest';
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: 'csr:routeChange';
306
+ plugin: typeof RecordingPluginName.RouteChange;
267
307
  payload: RouteChangePayload;
268
308
  };
269
309
 
270
310
  export type TagPluginData = {
271
- plugin: 'csr:tag';
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: 'csr:measure';
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: 'csr:flagEvaluation';
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: 'csr:errorMessage';
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: 'csr:dialogOpened';
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: 'csr:idleGap';
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: 'csr:awayGap';
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: Exclude<RecordingEventType, RecordingEventType.Custom | RecordingEventType.IncrementalSnapshot>;
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
- createUploader({ ...DEFAULTS, workerMode: 'dedicated', debugLogger: m => logs.push(m) });
116
- await tick();
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
- createUploader({ ...DEFAULTS, workerMode: 'dedicated', debugLogger: m => logs.push(m) });
140
- await tick();
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
- createUploader({ ...DEFAULTS, workerMode: 'shared', debugLogger: m => logs.push(m) });
167
- await tick();
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;
@@ -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. */