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.
- package/README.md +35 -0
- package/dist/esnext/Lib.d.ts +4 -0
- package/dist/esnext/Lib.js +138 -16
- package/dist/esnext/Lib.js.map +1 -1
- package/dist/swetrix.cjs.js +139 -17
- package/dist/swetrix.cjs.js.map +1 -1
- package/dist/swetrix.es5.js +139 -17
- package/dist/swetrix.es5.js.map +1 -1
- package/dist/swetrix.js +1 -1
- package/dist/swetrix.js.map +1 -1
- package/package.json +1 -1
- package/src/Lib.ts +192 -20
- package/tests/sessionReplay.test.ts +211 -0
|
@@ -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')
|