@openreplay/tracker 17.1.5 → 17.1.6

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/dist/cjs/entry.js CHANGED
@@ -1135,7 +1135,7 @@ const stars = 'repeat' in String.prototype
1135
1135
  ? (str) => '*'.repeat(str.length)
1136
1136
  : (str) => str.replace(/./g, '*');
1137
1137
  function normSpaces(str) {
1138
- return str.trim().replace(/\s+/g, ' ');
1138
+ return str ? str.trim().replace(/\s+/g, ' ') : '';
1139
1139
  }
1140
1140
  // isAbsoluteUrl regexp: /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
1141
1141
  function isURL(s) {
@@ -2332,7 +2332,13 @@ class CanvasRecorder {
2332
2332
  this.app = app;
2333
2333
  this.options = options;
2334
2334
  this.snapshots = {};
2335
- this.intervals = [];
2335
+ this.intervals = new Map();
2336
+ this.observers = new Map();
2337
+ this.uploadQueue = 0;
2338
+ this.MAX_CONCURRENT_UPLOADS = 2;
2339
+ this.MAX_QUEUE_SIZE = 50; // ~500 images max (50 batches × 10 images)
2340
+ this.pendingBatches = [];
2341
+ this.isProcessingQueue = false;
2336
2342
  this.restartTracking = () => {
2337
2343
  this.clear();
2338
2344
  this.app.nodes.scanTree(this.captureCanvas);
@@ -2343,34 +2349,33 @@ class CanvasRecorder {
2343
2349
  return;
2344
2350
  }
2345
2351
  const isIgnored = this.app.sanitizer.isObscured(id) || this.app.sanitizer.isHidden(id);
2346
- if (isIgnored || !hasTag(node, 'canvas') || this.snapshots[id]) {
2352
+ if (isIgnored || this.snapshots[id]) {
2347
2353
  return;
2348
2354
  }
2349
2355
  const observer = new IntersectionObserver((entries) => {
2350
2356
  entries.forEach((entry) => {
2351
2357
  if (entry.isIntersecting) {
2352
- if (entry.target) {
2353
- if (this.snapshots[id] && this.snapshots[id].createdAt) {
2354
- this.snapshots[id].paused = false;
2355
- }
2356
- else {
2357
- this.recordCanvas(entry.target, id);
2358
- }
2359
- /**
2360
- * We can switch this to start observing when element is in the view
2361
- * but otherwise right now we're just pausing when it's not
2362
- * just to save some bandwidth and space on backend
2363
- * */
2364
- // observer.unobserve(entry.target)
2358
+ if (this.snapshots[id] && this.snapshots[id].createdAt) {
2359
+ this.snapshots[id].paused = false;
2365
2360
  }
2366
2361
  else {
2367
- if (this.snapshots[id]) {
2368
- this.snapshots[id].paused = true;
2369
- }
2362
+ this.recordCanvas(entry.target, id);
2363
+ }
2364
+ /**
2365
+ * We can switch this to start observing when element is in the view
2366
+ * but otherwise right now we're just pausing when it's not
2367
+ * just to save some bandwidth and space on backend
2368
+ * */
2369
+ // observer.unobserve(entry.target)
2370
+ }
2371
+ else {
2372
+ if (this.snapshots[id]) {
2373
+ this.snapshots[id].paused = true;
2370
2374
  }
2371
2375
  }
2372
2376
  });
2373
2377
  });
2378
+ this.observers.set(id, observer);
2374
2379
  observer.observe(node);
2375
2380
  };
2376
2381
  this.recordCanvas = (node, id) => {
@@ -2380,15 +2385,23 @@ class CanvasRecorder {
2380
2385
  createdAt: ts,
2381
2386
  paused: false,
2382
2387
  dummy: document.createElement('canvas'),
2388
+ isCapturing: false,
2389
+ isStopped: false,
2383
2390
  };
2384
2391
  const canvasMsg = CanvasNode(id.toString(), ts);
2385
2392
  this.app.send(canvasMsg);
2393
+ const cachedCanvas = node;
2386
2394
  const captureFn = (canvas) => {
2395
+ if (!this.snapshots[id] || this.snapshots[id].isCapturing || this.snapshots[id].isStopped) {
2396
+ return;
2397
+ }
2398
+ this.snapshots[id].isCapturing = true;
2387
2399
  captureSnapshot(canvas, this.options.quality, this.snapshots[id].dummy, this.options.fixedScaling, this.fileExt, (blob) => {
2388
- if (!blob)
2400
+ if (this.snapshots[id]) {
2401
+ this.snapshots[id].isCapturing = false;
2402
+ }
2403
+ if (!blob || !this.snapshots[id] || this.snapshots[id].isStopped) {
2389
2404
  return;
2390
- if (!this.snapshots[id]) {
2391
- return this.app.debug.warn('Canvas not present in snapshots after capture:', this.snapshots, id);
2392
2405
  }
2393
2406
  this.snapshots[id].images.push({ id: this.app.timestamp(), data: blob });
2394
2407
  if (this.snapshots[id].images.length > 9) {
@@ -2398,32 +2411,29 @@ class CanvasRecorder {
2398
2411
  });
2399
2412
  };
2400
2413
  const int = setInterval(() => {
2401
- const cid = this.app.nodes.getID(node);
2402
- const canvas = cid ? this.app.nodes.getNode(cid) : undefined;
2403
- if (!this.snapshots[id]) {
2414
+ const snapshot = this.snapshots[id];
2415
+ if (!snapshot || snapshot.isStopped) {
2404
2416
  this.app.debug.log('Canvas is not present in {snapshots}');
2405
- clearInterval(int);
2417
+ this.cleanupCanvas(id);
2406
2418
  return;
2407
2419
  }
2408
- if (!canvas || !hasTag(canvas, 'canvas') || canvas !== node) {
2409
- this.app.debug.log('Canvas element not in sync', canvas, node);
2410
- clearInterval(int);
2420
+ if (!document.contains(cachedCanvas)) {
2421
+ this.app.debug.log('Canvas element not in sync', cachedCanvas, node);
2422
+ this.cleanupCanvas(id);
2411
2423
  return;
2412
2424
  }
2413
- else {
2414
- if (!this.snapshots[id].paused) {
2415
- if (this.options.useAnimationFrame) {
2416
- requestAnimationFrame(() => {
2417
- captureFn(canvas);
2418
- });
2419
- }
2420
- else {
2421
- captureFn(canvas);
2422
- }
2425
+ if (!snapshot.paused) {
2426
+ if (this.options.useAnimationFrame) {
2427
+ requestAnimationFrame(() => {
2428
+ captureFn(cachedCanvas);
2429
+ });
2430
+ }
2431
+ else {
2432
+ captureFn(cachedCanvas);
2423
2433
  }
2424
2434
  }
2425
2435
  }, this.interval);
2426
- this.intervals.push(int);
2436
+ this.intervals.set(id, int);
2427
2437
  };
2428
2438
  this.fileExt = options.fileExt ?? 'webp';
2429
2439
  this.interval = 1000 / options.fps;
@@ -2438,6 +2448,30 @@ class CanvasRecorder {
2438
2448
  if (Object.keys(this.snapshots).length === 0) {
2439
2449
  return;
2440
2450
  }
2451
+ if (this.pendingBatches.length >= this.MAX_QUEUE_SIZE) {
2452
+ this.app.debug.warn('Upload queue full, dropping canvas batch');
2453
+ return;
2454
+ }
2455
+ this.pendingBatches.push({ images, canvasId, createdAt });
2456
+ if (!this.isProcessingQueue) {
2457
+ this.processUploadQueue();
2458
+ }
2459
+ }
2460
+ async processUploadQueue() {
2461
+ this.isProcessingQueue = true;
2462
+ while (this.pendingBatches.length > 0) {
2463
+ if (this.uploadQueue >= this.MAX_CONCURRENT_UPLOADS) {
2464
+ await new Promise((resolve) => setTimeout(resolve, 100));
2465
+ continue;
2466
+ }
2467
+ const batch = this.pendingBatches.shift();
2468
+ if (!batch)
2469
+ break;
2470
+ this.uploadBatch(batch.images, batch.canvasId, batch.createdAt);
2471
+ }
2472
+ this.isProcessingQueue = false;
2473
+ }
2474
+ uploadBatch(images, canvasId, createdAt) {
2441
2475
  const formData = new FormData();
2442
2476
  images.forEach((snapshot) => {
2443
2477
  const blob = snapshot.data;
@@ -2455,6 +2489,7 @@ class CanvasRecorder {
2455
2489
  void this.app.start({}, true);
2456
2490
  }, 250);
2457
2491
  };
2492
+ this.uploadQueue++;
2458
2493
  fetch(this.app.options.ingestPoint + '/v1/web/images', {
2459
2494
  method: 'POST',
2460
2495
  headers: {
@@ -2470,10 +2505,50 @@ class CanvasRecorder {
2470
2505
  })
2471
2506
  .catch((e) => {
2472
2507
  this.app.debug.error('error saving canvas', e);
2508
+ })
2509
+ .finally(() => {
2510
+ this.uploadQueue--;
2473
2511
  });
2474
2512
  }
2513
+ cleanupCanvas(id) {
2514
+ if (this.snapshots[id]) {
2515
+ this.snapshots[id].isStopped = true;
2516
+ }
2517
+ const interval = this.intervals.get(id);
2518
+ if (interval) {
2519
+ clearInterval(interval);
2520
+ this.intervals.delete(id);
2521
+ }
2522
+ const observer = this.observers.get(id);
2523
+ if (observer) {
2524
+ observer.disconnect();
2525
+ this.observers.delete(id);
2526
+ }
2527
+ if (this.snapshots[id]?.dummy) {
2528
+ const dummy = this.snapshots[id].dummy;
2529
+ dummy.width = 0;
2530
+ dummy.height = 0;
2531
+ }
2532
+ delete this.snapshots[id];
2533
+ }
2475
2534
  clear() {
2476
- this.intervals.forEach((int) => clearInterval(int));
2535
+ // Flush remaining images before cleanup
2536
+ Object.keys(this.snapshots).forEach((idStr) => {
2537
+ const id = parseInt(idStr, 10);
2538
+ const snapshot = this.snapshots[id];
2539
+ if (snapshot && snapshot.images.length > 0) {
2540
+ this.sendSnaps(snapshot.images, id, snapshot.createdAt);
2541
+ snapshot.images = [];
2542
+ }
2543
+ });
2544
+ Object.keys(this.snapshots).forEach((idStr) => {
2545
+ const id = parseInt(idStr, 10);
2546
+ this.cleanupCanvas(id);
2547
+ });
2548
+ // don't clear pendingBatches or stop queue processing
2549
+ // to allow flushed images to finish uploading in the background
2550
+ this.intervals.clear();
2551
+ this.observers.clear();
2477
2552
  this.snapshots = {};
2478
2553
  }
2479
2554
  }
@@ -4349,7 +4424,7 @@ class App {
4349
4424
  this.stopCallbacks = [];
4350
4425
  this.commitCallbacks = [];
4351
4426
  this.activityState = ActivityState.NotActive;
4352
- this.version = '17.1.5'; // TODO: version compatability check inside each plugin.
4427
+ this.version = '17.1.6'; // TODO: version compatability check inside each plugin.
4353
4428
  this.socketMode = false;
4354
4429
  this.compressionThreshold = 24 * 1000;
4355
4430
  this.bc = null;
@@ -9186,7 +9261,7 @@ class ConstantProperties {
9186
9261
  user_id: this.user_id,
9187
9262
  distinct_id: this.deviceId,
9188
9263
  sdk_edition: 'web',
9189
- sdk_version: '17.1.5',
9264
+ sdk_version: '17.1.6',
9190
9265
  timezone: getUTCOffsetString(),
9191
9266
  search_engine: this.searchEngine,
9192
9267
  };
@@ -9830,7 +9905,7 @@ class API {
9830
9905
  this.signalStartIssue = (reason, missingApi) => {
9831
9906
  const doNotTrack = this.checkDoNotTrack();
9832
9907
  console.log("Tracker couldn't start due to:", JSON.stringify({
9833
- trackerVersion: '17.1.5',
9908
+ trackerVersion: '17.1.6',
9834
9909
  projectKey: this.options.projectKey,
9835
9910
  doNotTrack,
9836
9911
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,