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