@cedarai/session-replay-sdk 0.3.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +148 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ function isCompressionSupported() {
24
24
 
25
25
  // src/transport.ts
26
26
  var BEACON_MAX_SIZE = 64 * 1024;
27
+ var MAX_QUEUE_LENGTH = 1e3;
27
28
  var Transport = class {
28
29
  config;
29
30
  queue = [];
@@ -65,6 +66,9 @@ var Transport = class {
65
66
  enqueue(event) {
66
67
  if (!this.started) return;
67
68
  this.queue.push(event);
69
+ if (this.queue.length > MAX_QUEUE_LENGTH) {
70
+ this.queue.splice(0, this.queue.length - MAX_QUEUE_LENGTH);
71
+ }
68
72
  const maxSize = this.config.batchMaxSize ?? 1024 * 512;
69
73
  const estimatedSize = this.estimateQueueSize();
70
74
  if (estimatedSize >= maxSize) {
@@ -294,10 +298,37 @@ var ErrorCapture = class {
294
298
 
295
299
  // src/network.ts
296
300
  var nextId = 0;
301
+ var RESPONSE_BODY_MAX_SIZE = 100 * 1024;
302
+ function extractHeaders(headers) {
303
+ if (!headers) return void 0;
304
+ const result = {};
305
+ if (headers instanceof Headers) {
306
+ headers.forEach((v, k) => {
307
+ result[k] = v;
308
+ });
309
+ } else if (Array.isArray(headers)) {
310
+ for (const [k, v] of headers) {
311
+ result[k] = v;
312
+ }
313
+ } else {
314
+ Object.assign(result, headers);
315
+ }
316
+ return Object.keys(result).length > 0 ? result : void 0;
317
+ }
318
+ function headersToRecord(headers) {
319
+ const result = {};
320
+ headers.forEach((v, k) => {
321
+ result[k] = v;
322
+ });
323
+ return Object.keys(result).length > 0 ? result : void 0;
324
+ }
297
325
  var NetworkCapture = class {
298
326
  onEvent;
299
327
  serverUrl;
300
328
  originalFetch = null;
329
+ originalXHROpen = null;
330
+ originalXHRSend = null;
331
+ originalXHRSetHeader = null;
301
332
  started = false;
302
333
  constructor(onEvent2, serverUrl) {
303
334
  this.onEvent = onEvent2;
@@ -306,6 +337,31 @@ var NetworkCapture = class {
306
337
  start() {
307
338
  if (this.started) return;
308
339
  this.started = true;
340
+ this.interceptFetch();
341
+ this.interceptXHR();
342
+ }
343
+ stop() {
344
+ if (!this.started) return;
345
+ this.started = false;
346
+ if (this.originalFetch) {
347
+ globalThis.fetch = this.originalFetch;
348
+ this.originalFetch = null;
349
+ }
350
+ if (this.originalXHROpen) {
351
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
352
+ this.originalXHROpen = null;
353
+ }
354
+ if (this.originalXHRSend) {
355
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
356
+ this.originalXHRSend = null;
357
+ }
358
+ if (this.originalXHRSetHeader) {
359
+ XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetHeader;
360
+ this.originalXHRSetHeader = null;
361
+ }
362
+ }
363
+ // ── Fetch interception ────────────────────────────────────────
364
+ interceptFetch() {
309
365
  this.originalFetch = globalThis.fetch;
310
366
  globalThis.fetch = async (input, init) => {
311
367
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
@@ -314,6 +370,7 @@ var NetworkCapture = class {
314
370
  }
315
371
  const method = init?.method?.toUpperCase() ?? "GET";
316
372
  const requestBody = init?.body ? String(init.body) : void 0;
373
+ const requestHeaders = extractHeaders(init?.headers);
317
374
  const startTime = performance.now();
318
375
  const id = String(++nextId);
319
376
  let graphqlOperationName;
@@ -330,10 +387,16 @@ var NetworkCapture = class {
330
387
  const response = await this.originalFetch(input, init);
331
388
  const endTime = performance.now();
332
389
  let responseBody;
333
- try {
334
- const cloned = response.clone();
335
- responseBody = await cloned.text();
336
- } catch {
390
+ const contentLength = parseInt(response.headers.get("content-length") ?? "0", 10);
391
+ if (contentLength === 0 || contentLength <= RESPONSE_BODY_MAX_SIZE) {
392
+ try {
393
+ const cloned = response.clone();
394
+ const text = await cloned.text();
395
+ if (text.length <= RESPONSE_BODY_MAX_SIZE) {
396
+ responseBody = text;
397
+ }
398
+ } catch {
399
+ }
337
400
  }
338
401
  this.onEvent({
339
402
  type: "network",
@@ -343,8 +406,10 @@ var NetworkCapture = class {
343
406
  method,
344
407
  url,
345
408
  graphqlOperationName,
409
+ requestHeaders,
346
410
  requestBody,
347
411
  status: response.status,
412
+ responseHeaders: headersToRecord(response.headers),
348
413
  responseBody,
349
414
  startTime,
350
415
  endTime,
@@ -362,6 +427,7 @@ var NetworkCapture = class {
362
427
  method,
363
428
  url,
364
429
  graphqlOperationName,
430
+ requestHeaders,
365
431
  requestBody,
366
432
  startTime,
367
433
  endTime,
@@ -373,18 +439,87 @@ var NetworkCapture = class {
373
439
  }
374
440
  };
375
441
  }
376
- stop() {
377
- if (!this.started) return;
378
- this.started = false;
379
- if (this.originalFetch) {
380
- globalThis.fetch = this.originalFetch;
381
- this.originalFetch = null;
382
- }
442
+ // ── XMLHttpRequest interception ───────────────────────────────
443
+ interceptXHR() {
444
+ const self = this;
445
+ const xhrData = /* @__PURE__ */ new WeakMap();
446
+ this.originalXHROpen = XMLHttpRequest.prototype.open;
447
+ this.originalXHRSend = XMLHttpRequest.prototype.send;
448
+ this.originalXHRSetHeader = XMLHttpRequest.prototype.setRequestHeader;
449
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
450
+ const urlStr = typeof url === "string" ? url : url.href;
451
+ xhrData.set(this, { method: method.toUpperCase(), url: urlStr, headers: {}, startTime: 0 });
452
+ return self.originalXHROpen.apply(this, [method, url, ...rest]);
453
+ };
454
+ XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
455
+ const data = xhrData.get(this);
456
+ if (data) data.headers[name] = value;
457
+ return self.originalXHRSetHeader.apply(this, [name, value]);
458
+ };
459
+ XMLHttpRequest.prototype.send = function(body) {
460
+ const data = xhrData.get(this);
461
+ if (data && data.url.startsWith(self.serverUrl)) {
462
+ return self.originalXHRSend.apply(this, [body]);
463
+ }
464
+ if (data) {
465
+ data.startTime = performance.now();
466
+ }
467
+ const requestBody = body != null ? String(body) : void 0;
468
+ this.addEventListener("loadend", function() {
469
+ if (!data) return;
470
+ const endTime = performance.now();
471
+ const rawHeaders = this.getAllResponseHeaders();
472
+ const responseHeaders = {};
473
+ if (rawHeaders) {
474
+ rawHeaders.trim().split(/[\r\n]+/).forEach((line) => {
475
+ const idx = line.indexOf(": ");
476
+ if (idx > 0) responseHeaders[line.substring(0, idx)] = line.substring(idx + 2);
477
+ });
478
+ }
479
+ let responseBody;
480
+ if (this.responseType === "" || this.responseType === "text") {
481
+ try {
482
+ const text = this.responseText;
483
+ if (text.length <= RESPONSE_BODY_MAX_SIZE) responseBody = text;
484
+ } catch {
485
+ }
486
+ }
487
+ self.onEvent({
488
+ type: "network",
489
+ timestamp: Date.now(),
490
+ data: {
491
+ id: String(++nextId),
492
+ method: data.method,
493
+ url: data.url,
494
+ requestHeaders: Object.keys(data.headers).length > 0 ? data.headers : void 0,
495
+ requestBody,
496
+ status: this.status,
497
+ responseHeaders: Object.keys(responseHeaders).length > 0 ? responseHeaders : void 0,
498
+ responseBody,
499
+ startTime: data.startTime,
500
+ endTime,
501
+ duration: endTime - data.startTime,
502
+ error: this.status === 0 ? "Network error" : void 0
503
+ }
504
+ });
505
+ });
506
+ return self.originalXHRSend.apply(this, [body]);
507
+ };
383
508
  }
384
509
  };
385
510
 
386
511
  // src/recorder.ts
387
512
  import { record } from "rrweb";
513
+ var DEFAULT_SAMPLING = {
514
+ mousemove: 50,
515
+ // throttle mousemove to one event per 50ms (default is every frame)
516
+ mouseInteraction: true,
517
+ // keep mouse clicks/interactions
518
+ scroll: 150,
519
+ // throttle scroll to one event per 150ms
520
+ input: "last"
521
+ // only capture the final input value, not every keystroke
522
+ };
388
523
  var RecorderCapture = class {
389
524
  onEvent;
390
525
  config;
@@ -398,6 +533,7 @@ var RecorderCapture = class {
398
533
  if (this.started) return;
399
534
  this.started = true;
400
535
  const { checkoutEveryNms, blockSelector, maskAllInputs, inlineStylesheet, sampling } = this.config;
536
+ const mergedSampling = { ...DEFAULT_SAMPLING, ...sampling };
401
537
  const result = record({
402
538
  emit: (event) => {
403
539
  if (!this.started) return;
@@ -410,7 +546,7 @@ var RecorderCapture = class {
410
546
  ...blockSelector !== void 0 && { blockSelector },
411
547
  ...maskAllInputs !== void 0 && { maskAllInputs },
412
548
  ...inlineStylesheet !== void 0 && { inlineStylesheet },
413
- ...sampling !== void 0 && { sampling }
549
+ sampling: mergedSampling
414
550
  });
415
551
  this.stopFn = result ?? null;
416
552
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarai/session-replay-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",