swetrix 4.3.0 → 4.4.0

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.
@@ -167,6 +167,78 @@ describe('Session replay tracking', () => {
167
167
  expect(stopRecording).toHaveBeenCalledTimes(1)
168
168
  })
169
169
 
170
+ test('uses backend replay id and chunk index across page loads without browser storage', async () => {
171
+ const replayId = 'server-replay-id'
172
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
173
+ const getItemSpy = jest.spyOn(Storage.prototype, 'getItem')
174
+ let startCount = 0
175
+
176
+ fetchMock.mockImplementation((url) => {
177
+ if (String(url).includes('/session-replay/start')) {
178
+ const nextChunkIndex = startCount === 0 ? 0 : 1200
179
+ startCount += 1
180
+
181
+ return Promise.resolve({
182
+ ok: true,
183
+ json: async () => ({ replayId, nextChunkIndex }),
184
+ })
185
+ }
186
+
187
+ return Promise.resolve({ ok: true })
188
+ })
189
+
190
+ const { recordOptions } = usePackageRrweb()
191
+ const { init, startSessionReplay } = await loadTracker()
192
+
193
+ init(PROJECT_ID, { devMode: true })
194
+ const firstActions = await startSessionReplay({ flushIntervalMs: 60_000 })
195
+
196
+ recordOptions().emit({ type: 2, timestamp: 100 })
197
+ await firstActions.flush()
198
+
199
+ const firstChunkCall = fetchMock.mock.calls.find(([url]) =>
200
+ String(url).includes('/session-replay/chunk'),
201
+ )
202
+ const firstChunkBody = JSON.parse(firstChunkCall![1].body as string)
203
+ expect(firstChunkBody).toEqual(
204
+ expect.objectContaining({
205
+ replayId,
206
+ chunkIndex: 0,
207
+ }),
208
+ )
209
+
210
+ const secondModule = await loadTracker()
211
+ secondModule.init(PROJECT_ID, { devMode: true })
212
+ const secondActions = await secondModule.startSessionReplay({
213
+ flushIntervalMs: 60_000,
214
+ })
215
+ const startCalls = fetchMock.mock.calls.filter(([url]) =>
216
+ String(url).includes('/session-replay/start'),
217
+ )
218
+ expect(startCalls).toHaveLength(2)
219
+
220
+ recordOptions().emit({ type: 2, timestamp: 200 })
221
+ await secondActions.flush()
222
+
223
+ const chunkCalls = fetchMock.mock.calls.filter(([url]) =>
224
+ String(url).includes('/session-replay/chunk'),
225
+ )
226
+ const secondChunkBody = JSON.parse(chunkCalls[1][1].body as string)
227
+ expect(secondChunkBody).toEqual(
228
+ expect.objectContaining({
229
+ replayId,
230
+ chunkIndex: 1200,
231
+ }),
232
+ )
233
+ expect(setItemSpy).not.toHaveBeenCalled()
234
+ expect(getItemSpy).not.toHaveBeenCalled()
235
+
236
+ await secondActions.stop()
237
+ await firstActions.stop()
238
+ setItemSpy.mockRestore()
239
+ getItemSpy.mockRestore()
240
+ })
241
+
170
242
  test('sampleRate can skip recording before loading rrweb', async () => {
171
243
  const { init, startSessionReplay } = await loadTracker()
172
244
 
@@ -242,28 +314,48 @@ describe('Session replay tracking', () => {
242
314
  'total',
243
315
  { blockSelector: '.secret', emit: jest.fn() },
244
316
  emit,
317
+ false,
245
318
  )
246
319
  expect(total.maskAllInputs).toBe(true)
247
320
  expect(total.maskTextSelector).toBe('*')
248
321
  expect(total.blockSelector).toContain('.secret')
322
+ expect(total.blockSelector).toContain('iframe')
249
323
  expect(total.blockSelector).toContain('img')
250
324
  expect(total.recordCanvas).toBe(false)
325
+ expect(total.recordCrossOriginIframes).toBe(false)
326
+ expect(total.collectFonts).toBe(false)
251
327
  expect(total.inlineImages).toBe(false)
328
+ expect(total.sampling).toEqual({
329
+ mousemove: 50,
330
+ scroll: 150,
331
+ input: 'last',
332
+ })
333
+ expect(total.slimDOMOptions).toEqual(
334
+ expect.objectContaining({
335
+ script: true,
336
+ comment: true,
337
+ headMetaSocial: true,
338
+ }),
339
+ )
252
340
  expect(total.emit).toBe(emit)
253
341
 
254
342
  const normal = (lib as any).getSessionReplayRecordOptions(
255
343
  'normal',
256
344
  {},
257
345
  emit,
346
+ false,
258
347
  )
259
348
  expect(normal.maskAllInputs).toBe(true)
349
+ expect(normal.blockSelector).toBe('iframe')
260
350
  expect(normal.emit).toBe(emit)
261
351
 
262
352
  const freeLove = (lib as any).getSessionReplayRecordOptions(
263
353
  'none',
264
354
  { maskInputOptions: { email: false } },
265
355
  emit,
356
+ false,
266
357
  )
358
+ expect(freeLove.blockSelector).toBe('iframe')
267
359
  expect(freeLove.maskInputOptions).toEqual({
268
360
  email: false,
269
361
  password: true,
@@ -271,6 +363,66 @@ describe('Session replay tracking', () => {
271
363
  expect(freeLove.emit).toBe(emit)
272
364
  })
273
365
 
366
+ test('recordIframes opt-in keeps iframe capture available', () => {
367
+ const lib = new Lib(PROJECT_ID, { devMode: true })
368
+ const emit = jest.fn()
369
+
370
+ const options = (lib as any).getSessionReplayRecordOptions(
371
+ 'normal',
372
+ {
373
+ blockSelector: '.secret',
374
+ recordCrossOriginIframes: true,
375
+ sampling: { scroll: 500 },
376
+ },
377
+ emit,
378
+ true,
379
+ )
380
+
381
+ expect(options.blockSelector).toBe('.secret')
382
+ expect(options.recordCrossOriginIframes).toBe(true)
383
+ expect(options.sampling).toEqual({
384
+ mousemove: 50,
385
+ scroll: 500,
386
+ input: 'last',
387
+ })
388
+ expect(options.emit).toBe(emit)
389
+ })
390
+
391
+ test('maskAllText can be enabled outside total privacy', () => {
392
+ const lib = new Lib(PROJECT_ID, { devMode: true })
393
+ const emit = jest.fn()
394
+
395
+ const options = (lib as any).getSessionReplayRecordOptions(
396
+ 'normal',
397
+ {},
398
+ emit,
399
+ false,
400
+ true,
401
+ )
402
+
403
+ expect(options.maskAllInputs).toBe(true)
404
+ expect(options.maskTextSelector).toBe('*')
405
+ expect(options.emit).toBe(emit)
406
+ })
407
+
408
+ test('maskAllText can be disabled for total privacy', () => {
409
+ const lib = new Lib(PROJECT_ID, { devMode: true })
410
+ const emit = jest.fn()
411
+
412
+ const options = (lib as any).getSessionReplayRecordOptions(
413
+ 'total',
414
+ { maskTextSelector: '.masked' },
415
+ emit,
416
+ false,
417
+ false,
418
+ )
419
+
420
+ expect(options.maskAllInputs).toBe(true)
421
+ expect(options.maskTextSelector).toBe('.masked')
422
+ expect(options.blockSelector).toContain('img')
423
+ expect(options.emit).toBe(emit)
424
+ })
425
+
274
426
  test('invalid privacy values fall back to total privacy', async () => {
275
427
  const { recordOptions } = usePackageRrweb()
276
428
  const { init, startSessionReplay } = await loadTracker()
@@ -292,6 +444,65 @@ describe('Session replay tracking', () => {
292
444
  await actions.stop()
293
445
  })
294
446
 
447
+ test('oversized replay events are dropped before upload', async () => {
448
+ const { recordOptions } = usePackageRrweb()
449
+ const { init, startSessionReplay } = await loadTracker()
450
+
451
+ init(PROJECT_ID, { devMode: true })
452
+ const actions = await startSessionReplay({
453
+ flushIntervalMs: 60_000,
454
+ maxBytesPerEvent: 30,
455
+ })
456
+
457
+ recordOptions().emit({
458
+ type: 3,
459
+ timestamp: 400,
460
+ data: { text: 'x'.repeat(200) },
461
+ })
462
+ await actions.flush()
463
+
464
+ expect(
465
+ fetchMock.mock.calls.filter(([url]) =>
466
+ String(url).includes('/session-replay/chunk'),
467
+ ),
468
+ ).toHaveLength(0)
469
+
470
+ await actions.stop()
471
+ })
472
+
473
+ test('fractional byte limits below one fall back to defaults', async () => {
474
+ const { recordOptions } = usePackageRrweb()
475
+ const { init, startSessionReplay } = await loadTracker()
476
+
477
+ init(PROJECT_ID, { devMode: true })
478
+ const actions = await startSessionReplay({
479
+ flushIntervalMs: 60_000,
480
+ maxBytesPerChunk: 0.5,
481
+ maxBytesPerEvent: 0.5,
482
+ })
483
+
484
+ recordOptions().emit({ type: 3, timestamp: 500 })
485
+ await Promise.resolve()
486
+ await Promise.resolve()
487
+
488
+ expect(
489
+ fetchMock.mock.calls.filter(([url]) =>
490
+ String(url).includes('/session-replay/chunk'),
491
+ ),
492
+ ).toHaveLength(0)
493
+
494
+ await actions.flush()
495
+
496
+ expect(fetchMock).toHaveBeenCalledWith(
497
+ 'https://api.swetrix.com/log/session-replay/chunk',
498
+ expect.objectContaining({
499
+ body: expect.stringContaining('"timestamp":500'),
500
+ }),
501
+ )
502
+
503
+ await actions.stop()
504
+ })
505
+
295
506
  test('script rrweb loader clears failed loads so startSessionReplay can retry', async () => {
296
507
  const record = jest.fn(() => jest.fn())
297
508
  const trackerScript = document.createElement('script')