mixpanel-browser 2.74.0 → 2.76.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 (61) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.github/workflows/integration-tests.yml +2 -2
  3. package/.github/workflows/unit-tests.yml +3 -3
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +2 -2
  6. package/build.sh +10 -8
  7. package/dist/async-modules/mixpanel-recorder-bIS4LMGd.js +23595 -0
  8. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +2 -0
  9. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +1 -0
  10. package/dist/async-modules/mixpanel-targeting-BcAPS-Mz.js +2520 -0
  11. package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js +2 -0
  12. package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js.map +1 -0
  13. package/dist/mixpanel-core.cjs.d.ts +68 -0
  14. package/dist/mixpanel-core.cjs.js +802 -337
  15. package/dist/mixpanel-recorder.js +828 -40
  16. package/dist/mixpanel-recorder.min.js +1 -1
  17. package/dist/mixpanel-recorder.min.js.map +1 -1
  18. package/dist/mixpanel-targeting.js +2520 -0
  19. package/dist/mixpanel-targeting.min.js +2 -0
  20. package/dist/mixpanel-targeting.min.js.map +1 -0
  21. package/dist/mixpanel-with-async-modules.cjs.d.ts +590 -0
  22. package/dist/mixpanel-with-async-modules.cjs.js +9867 -0
  23. package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
  24. package/dist/mixpanel-with-async-recorder.cjs.js +802 -337
  25. package/dist/mixpanel-with-recorder.d.ts +68 -0
  26. package/dist/mixpanel-with-recorder.js +1591 -343
  27. package/dist/mixpanel-with-recorder.min.d.ts +68 -0
  28. package/dist/mixpanel-with-recorder.min.js +1 -1
  29. package/dist/mixpanel.amd.d.ts +68 -0
  30. package/dist/mixpanel.amd.js +2124 -345
  31. package/dist/mixpanel.cjs.d.ts +68 -0
  32. package/dist/mixpanel.cjs.js +2124 -345
  33. package/dist/mixpanel.globals.js +802 -337
  34. package/dist/mixpanel.min.js +185 -175
  35. package/dist/mixpanel.module.d.ts +68 -0
  36. package/dist/mixpanel.module.js +2124 -345
  37. package/dist/mixpanel.umd.d.ts +68 -0
  38. package/dist/mixpanel.umd.js +2124 -345
  39. package/dist/rrweb-bundled.js +119 -5
  40. package/dist/rrweb-compiled.js +116 -5
  41. package/logo.svg +5 -0
  42. package/package.json +5 -3
  43. package/rollup.config.mjs +189 -40
  44. package/src/autocapture/index.js +10 -27
  45. package/src/config.js +9 -3
  46. package/src/flags/index.js +269 -9
  47. package/src/index.d.ts +68 -0
  48. package/src/loaders/loader-module.js +1 -0
  49. package/src/mixpanel-core.js +83 -109
  50. package/src/recorder/index.js +2 -1
  51. package/src/recorder/recorder.js +5 -1
  52. package/src/recorder/rrweb-network-plugin.js +649 -0
  53. package/src/recorder/session-recording.js +31 -11
  54. package/src/recorder-manager.js +216 -0
  55. package/src/request-batcher.js +1 -1
  56. package/src/targeting/event-matcher.js +42 -0
  57. package/src/targeting/index.js +11 -0
  58. package/src/targeting/loader.js +36 -0
  59. package/src/utils.js +14 -9
  60. package/testServer.js +55 -0
  61. /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +0 -0
@@ -26,6 +26,19 @@
26
26
  win = window;
27
27
  }
28
28
 
29
+ var Config = {
30
+ DEBUG: false,
31
+ LIB_VERSION: '2.76.0'
32
+ };
33
+
34
+ // Window global names for async modules
35
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
36
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
37
+
38
+ // Constants that are injected at build-time for the names of async modules.
39
+ var RECORDER_FILENAME = '__MP_RECORDER_FILENAME__';
40
+ var TARGETING_FILENAME = '__MP_TARGETING_FILENAME__';
41
+
29
42
  function _array_like_to_array(arr, len) {
30
43
  if (len == null || len > arr.length) len = arr.length;
31
44
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -640,14 +653,16 @@
640
653
  return this.nodeMetaMap.get(n2) || null;
641
654
  };
642
655
  // removes the node from idNodeMap
643
- // doesn't remove the node from nodeMetaMap
644
- _proto.removeNodeFromMap = function removeNodeFromMap(n2) {
656
+ // if permanent is true, also removes from nodeMetaMap
657
+ _proto.removeNodeFromMap = function removeNodeFromMap(n2, permanent) {
645
658
  var _this = this;
659
+ if (permanent === void 0) permanent = false;
646
660
  var id = this.getId(n2);
647
661
  this.idNodeMap.delete(id);
662
+ if (permanent) this.nodeMetaMap.delete(n2);
648
663
  if (n2.childNodes) {
649
664
  n2.childNodes.forEach(function(childNode) {
650
- return _this.removeNodeFromMap(childNode);
665
+ return _this.removeNodeFromMap(childNode, permanent);
651
666
  });
652
667
  }
653
668
  };
@@ -10389,6 +10404,15 @@
10389
10404
  _proto.generateId = function generateId() {
10390
10405
  return this.id++;
10391
10406
  };
10407
+ _proto.remove = function remove(stylesheet) {
10408
+ var id = this.styleIDMap.get(stylesheet);
10409
+ if (id !== void 0) {
10410
+ this.styleIDMap.delete(stylesheet);
10411
+ this.idStyleMap.delete(id);
10412
+ return true;
10413
+ }
10414
+ return false;
10415
+ };
10392
10416
  return StyleSheetMirror;
10393
10417
  }();
10394
10418
  function getShadowHost(n2) {
@@ -10711,7 +10735,15 @@
10711
10735
  }
10712
10736
  };
10713
10737
  while(_this.mapRemoves.length){
10714
- _this.mirror.removeNodeFromMap(_this.mapRemoves.shift());
10738
+ var removedNode = _this.mapRemoves.shift();
10739
+ if (removedNode.nodeName === "IFRAME") {
10740
+ try {
10741
+ _this.iframeManager.removeIframe(removedNode);
10742
+ } catch (e2) {}
10743
+ } else {
10744
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10745
+ }
10746
+ _this.mirror.removeNodeFromMap(removedNode);
10715
10747
  }
10716
10748
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
10717
10749
  var n2 = _step.value;
@@ -11085,6 +11117,9 @@
11085
11117
  this.shadowDomManager.reset();
11086
11118
  this.canvasManager.reset();
11087
11119
  };
11120
+ _proto.getDoc = function getDoc() {
11121
+ return this.doc;
11122
+ };
11088
11123
  return MutationBuffer;
11089
11124
  }();
11090
11125
  function deepDelete(addsSet, n2) {
@@ -11185,6 +11220,14 @@
11185
11220
  });
11186
11221
  return observer;
11187
11222
  }
11223
+ function removeMutationBufferForDoc(doc) {
11224
+ for(var i2 = mutationBuffers.length - 1; i2 >= 0; i2--){
11225
+ var buffer = mutationBuffers[i2];
11226
+ if (buffer.getDoc() === doc) {
11227
+ mutationBuffers.splice(i2, 1);
11228
+ }
11229
+ }
11230
+ }
11188
11231
  function initMoveObserver(param) {
11189
11232
  var mousemoveCb = param.mousemoveCb, sampling = param.sampling, doc = param.doc, mirror2 = param.mirror;
11190
11233
  if (sampling.mousemove === false) {
@@ -12200,6 +12243,8 @@
12200
12243
  __publicField$1(this, "crossOriginIframeMirror", new CrossOriginIframeMirror(genId));
12201
12244
  __publicField$1(this, "crossOriginIframeStyleMirror");
12202
12245
  __publicField$1(this, "crossOriginIframeRootIdMap", /* @__PURE__ */ new WeakMap());
12246
+ __publicField$1(this, "iframeContentDocumentMap", /* @__PURE__ */ new WeakMap());
12247
+ __publicField$1(this, "iframeObserverCleanupMap", /* @__PURE__ */ new WeakMap());
12203
12248
  __publicField$1(this, "mirror");
12204
12249
  __publicField$1(this, "mutationCb");
12205
12250
  __publicField$1(this, "wrappedEmit");
@@ -12221,6 +12266,31 @@
12221
12266
  this.iframes.set(iframeEl, true);
12222
12267
  if (iframeEl.contentWindow) this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
12223
12268
  };
12269
+ _proto.getIframeContentDocument = function getIframeContentDocument(iframeEl) {
12270
+ return this.iframeContentDocumentMap.get(iframeEl);
12271
+ };
12272
+ _proto.setObserverCleanup = function setObserverCleanup(iframeEl, cleanup) {
12273
+ this.iframeObserverCleanupMap.set(iframeEl, cleanup);
12274
+ };
12275
+ _proto.getObserverCleanup = function getObserverCleanup(iframeEl) {
12276
+ return this.iframeObserverCleanupMap.get(iframeEl);
12277
+ };
12278
+ _proto.removeIframe = function removeIframe(iframeEl) {
12279
+ var storedDoc = this.iframeContentDocumentMap.get(iframeEl);
12280
+ if (storedDoc) {
12281
+ this.stylesheetManager.cleanupStylesheetsForRemovedNode(storedDoc);
12282
+ this.mirror.removeNodeFromMap(storedDoc, true);
12283
+ }
12284
+ this.iframes.delete(iframeEl);
12285
+ this.iframeContentDocumentMap.delete(iframeEl);
12286
+ var observerCleanup = this.iframeObserverCleanupMap.get(iframeEl);
12287
+ if (observerCleanup) {
12288
+ try {
12289
+ observerCleanup();
12290
+ } catch (e2) {}
12291
+ this.iframeObserverCleanupMap.delete(iframeEl);
12292
+ }
12293
+ };
12224
12294
  _proto.addLoadListener = function addLoadListener(cb) {
12225
12295
  this.loadListener = cb;
12226
12296
  };
@@ -12239,6 +12309,9 @@
12239
12309
  attributes: [],
12240
12310
  isAttachIframe: true
12241
12311
  });
12312
+ if (iframeEl.contentDocument) {
12313
+ this.iframeContentDocumentMap.set(iframeEl, iframeEl.contentDocument);
12314
+ }
12242
12315
  if (this.recordCrossOriginIframes) (_a2 = iframeEl.contentWindow) == null ? void 0 : _a2.addEventListener("message", this.handleMessage.bind(this));
12243
12316
  (_b = this.loadListener) == null ? void 0 : _b.call(this, iframeEl);
12244
12317
  if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets && iframeEl.contentDocument.adoptedStyleSheets.length > 0) this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
@@ -13151,6 +13224,41 @@
13151
13224
  this.styleMirror.reset();
13152
13225
  this.trackedLinkElements = /* @__PURE__ */ new WeakSet();
13153
13226
  };
13227
+ /**
13228
+ * Cleans up stylesheets associated with a removed node.
13229
+ *
13230
+ * @param removedNode - The node that was removed from the DOM.
13231
+ */ _proto.cleanupStylesheetsForRemovedNode = function cleanupStylesheetsForRemovedNode(removedNode) {
13232
+ var _this = this;
13233
+ try {
13234
+ if (removedNode.nodeType === Node.DOCUMENT_NODE) {
13235
+ var doc = removedNode;
13236
+ if (doc.adoptedStyleSheets) {
13237
+ for(var _iterator = _create_for_of_iterator_helper_loose(doc.adoptedStyleSheets), _step; !(_step = _iterator()).done;){
13238
+ var sheet = _step.value;
13239
+ this.styleMirror.remove(sheet);
13240
+ }
13241
+ }
13242
+ }
13243
+ if (removedNode.nodeName === "STYLE") {
13244
+ var styleEl = removedNode;
13245
+ if (styleEl.sheet) {
13246
+ this.styleMirror.remove(styleEl.sheet);
13247
+ }
13248
+ }
13249
+ if (removedNode.nodeName === "LINK" && removedNode.rel === "stylesheet") {
13250
+ var linkEl = removedNode;
13251
+ if (linkEl.sheet) {
13252
+ this.styleMirror.remove(linkEl.sheet);
13253
+ }
13254
+ }
13255
+ if (removedNode.childNodes) {
13256
+ removedNode.childNodes.forEach(function(child) {
13257
+ _this.cleanupStylesheetsForRemovedNode(child);
13258
+ });
13259
+ }
13260
+ } catch (e2) {}
13261
+ };
13154
13262
  // TODO: take snapshot on stylesheet reload by applying event listener
13155
13263
  _proto.trackStylesheetInLinkElement = function trackStylesheetInLinkElement(_linkEl) {};
13156
13264
  return StylesheetManager;
@@ -13605,7 +13713,23 @@
13605
13713
  };
13606
13714
  iframeManager.addLoadListener(function(iframeEl) {
13607
13715
  try {
13608
- handlers.push(observe(iframeEl.contentDocument));
13716
+ var iframeDoc = iframeEl.contentDocument;
13717
+ var iframeHandler = observe(iframeDoc);
13718
+ handlers.push(iframeHandler);
13719
+ var existingCleanup = iframeManager.getObserverCleanup(iframeEl);
13720
+ iframeManager.setObserverCleanup(iframeEl, function() {
13721
+ if (existingCleanup) {
13722
+ try {
13723
+ existingCleanup();
13724
+ } catch (e2) {}
13725
+ }
13726
+ try {
13727
+ iframeHandler();
13728
+ var idx = handlers.indexOf(iframeHandler);
13729
+ if (idx !== -1) handlers.splice(idx, 1);
13730
+ removeMutationBufferForDoc(iframeDoc);
13731
+ } catch (e2) {}
13732
+ });
13609
13733
  } catch (error) {
13610
13734
  console.warn(error);
13611
13735
  }
@@ -18014,7 +18138,7 @@
18014
18138
  var __publicField = function(obj, key, value) {
18015
18139
  return __defNormalProp(obj, (typeof key === "undefined" ? "undefined" : _type_of(key)) !== "symbol" ? key + "" : key, value);
18016
18140
  };
18017
- function patch(source, name, replacement) {
18141
+ function patch$3(source, name, replacement) {
18018
18142
  try {
18019
18143
  if (!(name in source)) {
18020
18144
  return function() {};
@@ -18431,7 +18555,7 @@
18431
18555
  if (!_logger[level]) {
18432
18556
  return function() {};
18433
18557
  }
18434
- return patch(_logger, level, function(original) {
18558
+ return patch$3(_logger, level, function(original) {
18435
18559
  var _this1 = _this;
18436
18560
  return function() {
18437
18561
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
@@ -18852,11 +18976,6 @@
18852
18976
  PromisePolyfill = NpoPromise;
18853
18977
  }
18854
18978
 
18855
- var Config = {
18856
- DEBUG: false,
18857
- LIB_VERSION: '2.74.0'
18858
- };
18859
-
18860
18979
  /* eslint camelcase: "off", eqeqeq: "off" */
18861
18980
 
18862
18981
  // Maximum allowed session recording length
@@ -19060,15 +19179,8 @@
19060
19179
  return toString.call(obj) === '[object Array]';
19061
19180
  };
19062
19181
 
19063
- // from a comment on http://dbj.org/dbj/?p=286
19064
- // fails on only one very rare and deliberate custom object:
19065
- // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
19066
19182
  _.isFunction = function(f) {
19067
- try {
19068
- return /^\s*\bfunction\b/.test(f);
19069
- } catch (x) {
19070
- return false;
19071
- }
19183
+ return typeof f === 'function';
19072
19184
  };
19073
19185
 
19074
19186
  _.isArguments = function(obj) {
@@ -20595,6 +20707,17 @@
20595
20707
 
20596
20708
  var NOOP_FUNC = function () {};
20597
20709
 
20710
+ var urlMatchesRegexList = function (url, regexList) {
20711
+ var matches = false;
20712
+ for (var i = 0; i < regexList.length; i++) {
20713
+ if (url.match(regexList[i])) {
20714
+ matches = true;
20715
+ break;
20716
+ }
20717
+ }
20718
+ return matches;
20719
+ };
20720
+
20598
20721
  var JSONStringify = null, JSONParse = null;
20599
20722
  if (typeof JSON !== 'undefined') {
20600
20723
  JSONStringify = JSON.stringify;
@@ -21066,7 +21189,7 @@
21066
21189
  };
21067
21190
  }
21068
21191
 
21069
- var logger$6 = console_with_prefix('lock');
21192
+ var logger$7 = console_with_prefix('lock');
21070
21193
 
21071
21194
  /**
21072
21195
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -21118,7 +21241,7 @@
21118
21241
 
21119
21242
  var delay = function(cb) {
21120
21243
  if (new Date().getTime() - startTime > timeoutMS) {
21121
- logger$6.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21244
+ logger$7.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21122
21245
  storage.removeItem(keyZ);
21123
21246
  storage.removeItem(keyY);
21124
21247
  loop();
@@ -21265,7 +21388,7 @@
21265
21388
  }, this));
21266
21389
  };
21267
21390
 
21268
- var logger$5 = console_with_prefix('batch');
21391
+ var logger$6 = console_with_prefix('batch');
21269
21392
 
21270
21393
  /**
21271
21394
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -21294,7 +21417,7 @@
21294
21417
  timeoutMS: options.sharedLockTimeoutMS,
21295
21418
  });
21296
21419
  }
21297
- this.reportError = options.errorReporter || _.bind(logger$5.error, logger$5);
21420
+ this.reportError = options.errorReporter || _.bind(logger$6.error, logger$6);
21298
21421
 
21299
21422
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
21300
21423
 
@@ -21627,7 +21750,7 @@
21627
21750
  // maximum interval between request retries after exponential backoff
21628
21751
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
21629
21752
 
21630
- var logger$4 = console_with_prefix('batch');
21753
+ var logger$5 = console_with_prefix('batch');
21631
21754
 
21632
21755
  /**
21633
21756
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -21755,7 +21878,7 @@
21755
21878
  */
21756
21879
  RequestBatcher.prototype.flush = function(options) {
21757
21880
  if (this.requestInProgress) {
21758
- logger$4.log('Flush: Request already in progress');
21881
+ logger$5.log('Flush: Request already in progress');
21759
21882
  return PromisePolyfill.resolve();
21760
21883
  }
21761
21884
 
@@ -21932,7 +22055,7 @@
21932
22055
  if (options.unloading) {
21933
22056
  requestOptions.transport = 'sendBeacon';
21934
22057
  }
21935
- logger$4.log('MIXPANEL REQUEST:', dataForRequest);
22058
+ logger$5.log('MIXPANEL REQUEST:', dataForRequest);
21936
22059
  return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
21937
22060
  }, this))
21938
22061
  .catch(_.bind(function(err) {
@@ -21945,7 +22068,7 @@
21945
22068
  * Log error to global logger and optional user-defined logger.
21946
22069
  */
21947
22070
  RequestBatcher.prototype.reportError = function(msg, err) {
21948
- logger$4.error.apply(logger$4.error, arguments);
22071
+ logger$5.error.apply(logger$5.error, arguments);
21949
22072
  if (this.errorReporter) {
21950
22073
  try {
21951
22074
  if (!(err instanceof Error)) {
@@ -21953,7 +22076,7 @@
21953
22076
  }
21954
22077
  this.errorReporter(msg, err);
21955
22078
  } catch(err) {
21956
- logger$4.error(err);
22079
+ logger$5.error(err);
21957
22080
  }
21958
22081
  }
21959
22082
  };
@@ -22075,7 +22198,7 @@
22075
22198
 
22076
22199
  var MAX_DEPTH = 5;
22077
22200
 
22078
- var logger$3 = console_with_prefix('autocapture');
22201
+ var logger$4 = console_with_prefix('autocapture');
22079
22202
 
22080
22203
 
22081
22204
  function getClasses(el) {
@@ -22339,7 +22462,7 @@
22339
22462
  return false;
22340
22463
  }
22341
22464
  } catch (err) {
22342
- logger$3.critical('Error while checking element in allowElementCallback', err);
22465
+ logger$4.critical('Error while checking element in allowElementCallback', err);
22343
22466
  return false;
22344
22467
  }
22345
22468
  }
@@ -22356,7 +22479,7 @@
22356
22479
  return true;
22357
22480
  }
22358
22481
  } catch (err) {
22359
- logger$3.critical('Error while checking selector: ' + sel, err);
22482
+ logger$4.critical('Error while checking selector: ' + sel, err);
22360
22483
  }
22361
22484
  }
22362
22485
  return false;
@@ -22371,7 +22494,7 @@
22371
22494
  return true;
22372
22495
  }
22373
22496
  } catch (err) {
22374
- logger$3.critical('Error while checking element in blockElementCallback', err);
22497
+ logger$4.critical('Error while checking element in blockElementCallback', err);
22375
22498
  return true;
22376
22499
  }
22377
22500
  }
@@ -22385,7 +22508,7 @@
22385
22508
  return true;
22386
22509
  }
22387
22510
  } catch (err) {
22388
- logger$3.critical('Error while checking selector: ' + sel, err);
22511
+ logger$4.critical('Error while checking selector: ' + sel, err);
22389
22512
  }
22390
22513
  }
22391
22514
  }
@@ -22933,173 +23056,822 @@
22933
23056
  }
22934
23057
 
22935
23058
  /**
22936
- * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23059
+ * This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
23060
+ * the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
23061
+ *
23062
+ * This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
23063
+ * header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
23064
+ *
22937
23065
  */
22938
23066
 
23067
+ var logger$3 = console_with_prefix('network-plugin');
22939
23068
 
22940
- var logger$2 = console_with_prefix('recorder');
22941
- var CompressionStream = win['CompressionStream'];
22942
-
22943
- var RECORDER_BATCHER_LIB_CONFIG = {
22944
- 'batch_size': 1000,
22945
- 'batch_flush_interval_ms': 10 * 1000,
22946
- 'batch_request_timeout_ms': 90 * 1000,
22947
- 'batch_autostart': true
22948
- };
23069
+ /**
23070
+ * Get the time origin for converting performance timestamps to absolute timestamps.
23071
+ * Uses Date.now() - performance.now() instead of performance.timeOrigin because
23072
+ * browsers can report timeOrigin values that are skewed from actual time, and some
23073
+ * browsers (notably older Safari versions) don't implement timeOrigin at all.
23074
+ * See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
23075
+ * @param {Window} win
23076
+ * @returns {number}
23077
+ */
23078
+ function getTimeOrigin(win) {
23079
+ return Math.round(Date.now() - win.performance.now());
23080
+ }
22949
23081
 
22950
- var ACTIVE_SOURCES = new Set([
22951
- IncrementalSource.MouseMove,
22952
- IncrementalSource.MouseInteraction,
22953
- IncrementalSource.Scroll,
22954
- IncrementalSource.ViewportResize,
22955
- IncrementalSource.Input,
22956
- IncrementalSource.TouchMove,
22957
- IncrementalSource.MediaInteraction,
22958
- IncrementalSource.Drag,
22959
- IncrementalSource.Selection,
22960
- ]);
23082
+ /**
23083
+ * @typedef {import('../index.d.ts').InitiatorType} InitiatorType
23084
+ * @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
23085
+ * @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
23086
+ * @typedef {import('../index.d.ts').NetworkData} NetworkData
23087
+ */
22961
23088
 
22962
- function isUserEvent(ev) {
22963
- return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
22964
- }
23089
+ /**
23090
+ * @typedef {Record<string, string>} Headers
23091
+ */
22965
23092
 
22966
23093
  /**
22967
- * @typedef {Object} SerializedRecording
22968
- * @property {number} idleExpires
22969
- * @property {number} maxExpires
22970
- * @property {number} replayStartTime
22971
- * @property {number} lastEventTimestamp
22972
- * @property {number} seqNo
22973
- * @property {string} batchStartUrl
22974
- * @property {string} replayId
22975
- * @property {string} tabId
22976
- * @property {string} replayStartUrl
23094
+ * @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
22977
23095
  */
22978
23096
 
22979
23097
  /**
22980
- * @typedef {Object} SessionRecordingOptions
22981
- * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
22982
- * @property {String} [options.replayId] - unique uuid for a single replay
22983
- * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
22984
- * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
22985
- * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
22986
- * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
22987
- * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
22988
- * optional properties for deserialization:
22989
- * @property {number} idleExpires
22990
- * @property {number} maxExpires
22991
- * @property {number} replayStartTime
22992
- * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
22993
- * @property {number} seqNo
22994
- * @property {string} batchStartUrl
22995
- * @property {string} replayStartUrl
23098
+ * @callback networkCallback
23099
+ * @param {NetworkData} data
23100
+ * @returns {void}
22996
23101
  */
22997
23102
 
22998
23103
  /**
22999
- * @typedef {Object} UserIdInfo
23000
- * @property {string} distinct_id
23001
- * @property {string} user_id
23002
- * @property {string} device_id
23104
+ * @callback listenerHandler
23105
+ * @returns {void}
23003
23106
  */
23004
23107
 
23108
+ /**
23109
+ * @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
23110
+ */
23005
23111
 
23006
23112
  /**
23007
- * This class encapsulates a single session recording and its lifecycle.
23008
- * @param {SessionRecordingOptions} options
23113
+ * @typedef {Object} RecordPlugin
23114
+ * @property {string} name
23115
+ * @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
23116
+ * @property {NetworkRecordOptions} [options]
23009
23117
  */
23010
- var SessionRecording = function(options) {
23011
- this._mixpanel = options.mixpanelInstance;
23012
- this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23013
- this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23014
- this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23015
- this._rrwebRecord = options.rrwebRecord || null;
23016
23118
 
23017
- // internal rrweb stopRecording function
23018
- this._stopRecording = null;
23019
- this.replayId = options.replayId;
23119
+ /** @type {Required<NetworkRecordOptions>} */
23120
+ var defaultNetworkOptions = {
23121
+ initiatorTypes: [
23122
+ 'audio',
23123
+ 'beacon',
23124
+ 'body',
23125
+ 'css',
23126
+ 'early-hint',
23127
+ 'embed',
23128
+ 'fetch',
23129
+ 'frame',
23130
+ 'iframe',
23131
+ 'icon',
23132
+ 'image',
23133
+ 'img',
23134
+ 'input',
23135
+ 'link',
23136
+ 'navigation',
23137
+ 'object',
23138
+ 'ping',
23139
+ 'script',
23140
+ 'track',
23141
+ 'video',
23142
+ 'xmlhttprequest',
23143
+ ],
23144
+ ignoreRequestFn: function() { return false; },
23145
+ recordHeaders: {
23146
+ request: [],
23147
+ response: [],
23148
+ },
23149
+ recordBodyUrls: {
23150
+ request: [],
23151
+ response: [],
23152
+ },
23153
+ recordInitialRequests: false,
23154
+ };
23020
23155
 
23021
- this.batchStartUrl = options.batchStartUrl || null;
23022
- this.replayStartUrl = options.replayStartUrl || null;
23023
- this.idleExpires = options.idleExpires || null;
23024
- this.maxExpires = options.maxExpires || null;
23025
- this.replayStartTime = options.replayStartTime || null;
23026
- this.lastEventTimestamp = options.lastEventTimestamp || null;
23027
- this.seqNo = options.seqNo || 0;
23156
+ /**
23157
+ * @param {PerformanceEntry} entry
23158
+ * @returns {entry is PerformanceNavigationTiming}
23159
+ */
23160
+ function isNavigationTiming(entry) {
23161
+ return entry.entryType === 'navigation';
23162
+ }
23028
23163
 
23029
- this.idleTimeoutId = null;
23030
- this.maxTimeoutId = null;
23164
+ /**
23165
+ * @param {PerformanceEntry} entry
23166
+ * @returns {entry is PerformanceResourceTiming}
23167
+ */
23168
+ function isResourceTiming (entry) {
23169
+ return entry.entryType === 'resource';
23170
+ }
23031
23171
 
23032
- this.recordMaxMs = MAX_RECORDING_MS;
23033
- this.recordMinMs = 0;
23172
+ function findLast(array, predicate) {
23173
+ var length = array.length;
23174
+ for (var i = length - 1; i >= 0; i -= 1) {
23175
+ if (predicate(array[i])) {
23176
+ return array[i];
23177
+ }
23178
+ }
23179
+ }
23034
23180
 
23035
- // disable persistence if localStorage is not supported
23036
- // request-queue will automatically disable persistence if indexedDB fails to initialize
23037
- var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23181
+ /**
23182
+ * Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
23183
+ * Adapted from Sentry's `fill` utility:
23184
+ * https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
23185
+ *
23186
+ * @param {object} source - The object containing the method to patch
23187
+ * @param {string} name - The method name to patch
23188
+ * @param {function} replacementFactory - A function that receives the original method and returns the replacement
23189
+ * @returns {function} A function that restores the original method
23190
+ */
23191
+ function patch(source, name, replacementFactory) {
23192
+ if (!(name in source) || typeof source[name] !== 'function') {
23193
+ return function() {};
23194
+ }
23195
+ var original = source[name];
23196
+ var wrapped = replacementFactory(original);
23197
+ source[name] = wrapped;
23198
+ return function() {
23199
+ source[name] = original;
23200
+ };
23201
+ }
23038
23202
 
23039
- // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23040
- this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23041
- this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23042
- this.batcher = new RequestBatcher(this.batcherKey, {
23043
- errorReporter: this.reportError.bind(this),
23044
- flushOnlyOnInterval: true,
23045
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
23046
- sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23047
- queueStorage: this.queueStorage,
23048
- sharedLockStorage: options.sharedLockStorage,
23049
- usePersistence: usePersistence,
23050
- stopAllBatchingFunc: this.stopRecording.bind(this),
23051
23203
 
23052
- // increased throttle and shared lock timeout because recording events are very high frequency.
23053
- // this will minimize the amount of lock contention between enqueued events.
23054
- // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23055
- enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23056
- sharedLockTimeoutMS: 10 * 1000,
23057
- });
23058
- };
23204
+ /**
23205
+ * Maximum body size to record (1MB)
23206
+ */
23207
+ var MAX_BODY_SIZE = 1024 * 1024;
23059
23208
 
23060
23209
  /**
23061
- * @returns {UserIdInfo}
23210
+ * Truncate string if it exceeds max size
23211
+ * @param {string} str
23212
+ * @returns {string}
23062
23213
  */
23063
- SessionRecording.prototype.getUserIdInfo = function () {
23064
- if (this.finalFlushUserIdInfo) {
23065
- return this.finalFlushUserIdInfo;
23214
+ function truncateBody(str) {
23215
+ if (!str || typeof str !== 'string') {
23216
+ return str;
23217
+ }
23218
+ if (str.length > MAX_BODY_SIZE) {
23219
+ logger$3.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
23220
+ return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
23066
23221
  }
23222
+ return str;
23223
+ }
23067
23224
 
23068
- var userIdInfo = {
23069
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
23225
+ /**
23226
+ * @param {networkCallback} cb
23227
+ * @param {Window} win
23228
+ * @param {Required<NetworkRecordOptions>} options
23229
+ * @returns {listenerHandler}
23230
+ */
23231
+ function initPerformanceObserver(cb, win, options) {
23232
+ if (!win.PerformanceObserver) {
23233
+ logger$3.error('PerformanceObserver not supported');
23234
+ return function() {
23235
+ //
23236
+ };
23237
+ }
23238
+ if (options.recordInitialRequests) {
23239
+ var initialPerformanceEntries = win.performance
23240
+ .getEntries()
23241
+ .filter(function(entry) {
23242
+ return isNavigationTiming(entry) ||
23243
+ (isResourceTiming(entry) &&
23244
+ options.initiatorTypes.includes(entry.initiatorType));
23245
+ });
23246
+ cb({
23247
+ requests: initialPerformanceEntries.map(function(entry) {
23248
+ return {
23249
+ url: entry.name,
23250
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23251
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23252
+ startTime: Math.round(entry.startTime),
23253
+ endTime: Math.round(entry.responseEnd),
23254
+ timeOrigin: getTimeOrigin(win),
23255
+ };
23256
+ }),
23257
+ isInitial: true,
23258
+ });
23259
+ }
23260
+ var observer = new win.PerformanceObserver(function(entries) {
23261
+ var performanceEntries = entries
23262
+ .getEntries()
23263
+ .filter(function(entry) {
23264
+ return isResourceTiming(entry) &&
23265
+ options.initiatorTypes.includes(entry.initiatorType) &&
23266
+ entry.initiatorType !== 'xmlhttprequest' &&
23267
+ entry.initiatorType !== 'fetch';
23268
+ });
23269
+ cb({
23270
+ requests: performanceEntries.map(function(entry) {
23271
+ return {
23272
+ url: entry.name,
23273
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23274
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23275
+ startTime: Math.round(entry.startTime),
23276
+ endTime: Math.round(entry.responseEnd),
23277
+ timeOrigin: getTimeOrigin(win),
23278
+ };
23279
+ }),
23280
+ });
23281
+ });
23282
+ observer.observe({ entryTypes: ['navigation', 'resource'] });
23283
+ return function() {
23284
+ observer.disconnect();
23070
23285
  };
23286
+ }
23071
23287
 
23072
- // send ID management props if they exist
23073
- var deviceId = this._mixpanel.get_property('$device_id');
23074
- if (deviceId) {
23075
- userIdInfo['$device_id'] = deviceId;
23288
+ /**
23289
+ * Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
23290
+ * @param {'request' | 'response'} type
23291
+ * @param {NetworkRecordOptions['recordHeaders']} recordHeaders
23292
+ * @param {string} headerName
23293
+ * @returns {boolean}
23294
+ */
23295
+ function shouldRecordHeader(type, recordHeaders, headerName) {
23296
+ if (!recordHeaders[type] || recordHeaders[type].length === 0) {
23297
+ return false;
23076
23298
  }
23077
- var userId = this._mixpanel.get_property('$user_id');
23078
- if (userId) {
23079
- userIdInfo['$user_id'] = userId;
23299
+
23300
+ return recordHeaders[type].includes(headerName.toLowerCase());
23301
+ }
23302
+
23303
+ /**
23304
+ * Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
23305
+ * @param {'request' | 'response'} type
23306
+ * @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
23307
+ * @param {string} url
23308
+ * @returns {boolean}
23309
+ */
23310
+ function shouldRecordBody(type, recordBodyUrls, url) {
23311
+ if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
23312
+ return false;
23080
23313
  }
23081
- return userIdInfo;
23082
- };
23083
23314
 
23084
- SessionRecording.prototype.unloadPersistedData = function () {
23085
- this.batcher.stop();
23315
+ return urlMatchesRegexList(url, recordBodyUrls[type]);
23316
+ }
23086
23317
 
23087
- return this.queueStorage.init().catch(function () {
23088
- this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23089
- }.bind(this)).then(function () {
23090
- // if the recording is too short, just delete any stored events without flushing
23091
- if (this.getDurationMs() < this._getRecordMinMs()) {
23092
- return this.queueStorage.removeItem(this.batcherKey);
23318
+ function tryReadXHRBody(body) {
23319
+ if (body === null || body === undefined) {
23320
+ return null;
23321
+ }
23322
+
23323
+ var result;
23324
+ if (typeof body === 'string') {
23325
+ result = body;
23326
+ } else if (body instanceof Document) {
23327
+ result = body.textContent;
23328
+ } else if (body instanceof FormData) {
23329
+ result = _.HTTPBuildQuery(body);
23330
+ } else if (_.isObject(body)) {
23331
+ try {
23332
+ result = JSON.stringify(body);
23333
+ } catch (e) {
23334
+ return 'Failed to stringify response object';
23093
23335
  }
23336
+ } else {
23337
+ return 'Cannot read body of type ' + typeof body;
23338
+ }
23094
23339
 
23095
- return this.batcher.flush()
23096
- .then(function () {
23097
- return this.queueStorage.removeItem(this.batcherKey);
23098
- }.bind(this));
23099
- }.bind(this));
23100
- };
23340
+ return truncateBody(result);
23341
+ }
23101
23342
 
23102
- SessionRecording.prototype.getConfig = function(configVar) {
23343
+ /**
23344
+ * @param {Request | Response} r
23345
+ * @returns {Promise<string>}
23346
+ */
23347
+ function tryReadFetchBody(r) {
23348
+ return new Promise(function(resolve) {
23349
+ var timeout = setTimeout(function() {
23350
+ resolve('Timeout while trying to read body');
23351
+ }, 500);
23352
+ try {
23353
+ r.clone()
23354
+ .text()
23355
+ .then(
23356
+ function(txt) {
23357
+ clearTimeout(timeout);
23358
+ resolve(truncateBody(txt));
23359
+ },
23360
+ function(reason) {
23361
+ clearTimeout(timeout);
23362
+ resolve('Failed to read body: ' + String(reason));
23363
+ }
23364
+ );
23365
+ } catch (e) {
23366
+ clearTimeout(timeout);
23367
+ resolve('Failed to read body: ' + String(e));
23368
+ }
23369
+ });
23370
+ }
23371
+
23372
+ /**
23373
+ * @param {Window} win
23374
+ * @param {string} initiatorType
23375
+ * @param {string} url
23376
+ * @param {number} [after]
23377
+ * @param {number} [before]
23378
+ * @param {number} [attempt]
23379
+ * @returns {Promise<PerformanceResourceTiming>}
23380
+ */
23381
+ function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
23382
+ if (attempt === undefined) {
23383
+ attempt = 0;
23384
+ }
23385
+ if (attempt > 10) {
23386
+ logger$3.error('Cannot find performance entry');
23387
+ return Promise.resolve(null);
23388
+ }
23389
+ var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
23390
+ win.performance.getEntriesByName(url)
23391
+ );
23392
+ var performanceEntry = findLast(
23393
+ urlPerformanceEntries,
23394
+ function(entry) {
23395
+ return isResourceTiming(entry) &&
23396
+ entry.initiatorType === initiatorType &&
23397
+ (!after || entry.startTime >= after) &&
23398
+ (!before || entry.startTime <= before);
23399
+ }
23400
+ );
23401
+ if (!performanceEntry) {
23402
+ return new Promise(function(resolve) {
23403
+ setTimeout(resolve, 50 * attempt);
23404
+ }).then(function() {
23405
+ return getRequestPerformanceEntry(
23406
+ win,
23407
+ initiatorType,
23408
+ url,
23409
+ after,
23410
+ before,
23411
+ attempt + 1
23412
+ );
23413
+ });
23414
+ }
23415
+ return Promise.resolve(performanceEntry);
23416
+ }
23417
+
23418
+ /**
23419
+ * @param {networkCallback} cb
23420
+ * @param {Window} win
23421
+ * @param {Required<NetworkRecordOptions>} options
23422
+ * @returns {listenerHandler}
23423
+ */
23424
+ function initXhrObserver(cb, win, options) {
23425
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
23426
+ return function() {
23427
+ //
23428
+ };
23429
+ }
23430
+ var restorePatch = patch(
23431
+ win.XMLHttpRequest.prototype,
23432
+ 'open',
23433
+ function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
23434
+ return function(
23435
+ /** @type {string} */ method,
23436
+ /** @type {string | URL} */ url,
23437
+ /** @type {boolean} */ async,
23438
+ username, password
23439
+ ) {
23440
+ if (async === undefined) {
23441
+ async = true;
23442
+ }
23443
+ var xhr = /** @type {XMLHttpRequest} */ (this);
23444
+ var req = new Request(url, { method: method });
23445
+ /** @type {Partial<NetworkRequest>} */
23446
+ var networkRequest = {};
23447
+ /** @type {number | undefined} */
23448
+ var after;
23449
+ /** @type {number | undefined} */
23450
+ var before;
23451
+
23452
+ /** @type {Headers} */
23453
+ var requestHeaders = {};
23454
+ var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
23455
+ xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
23456
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23457
+ requestHeaders[header] = value;
23458
+ }
23459
+ return originalSetRequestHeader(header, value);
23460
+ };
23461
+ networkRequest.requestHeaders = requestHeaders;
23462
+
23463
+ var originalSend = xhr.send.bind(xhr);
23464
+ xhr.send = function(/** @type {Body} */ body) {
23465
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23466
+ networkRequest.requestBody = tryReadXHRBody(body);
23467
+ }
23468
+ after = win.performance.now();
23469
+ return originalSend(body);
23470
+ };
23471
+ xhr.addEventListener('readystatechange', function() {
23472
+ if (xhr.readyState !== xhr.DONE) {
23473
+ return;
23474
+ }
23475
+ before = win.performance.now();
23476
+ /** @type {Headers} */
23477
+ var responseHeaders = {};
23478
+ var rawHeaders = xhr.getAllResponseHeaders();
23479
+ if (rawHeaders) {
23480
+ var headers = rawHeaders.trim().split(/[\r\n]+/);
23481
+ headers.forEach(function(line) {
23482
+ if (!line) return;
23483
+ var colonIndex = line.indexOf(': ');
23484
+ if (colonIndex === -1) return;
23485
+ var header = line.substring(0, colonIndex);
23486
+ var value = line.substring(colonIndex + 2);
23487
+ if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
23488
+ responseHeaders[header] = value;
23489
+ }
23490
+ });
23491
+ }
23492
+ networkRequest.responseHeaders = responseHeaders;
23493
+ if (
23494
+ shouldRecordBody('response', options.recordBodyUrls, req.url)
23495
+ ) {
23496
+ networkRequest.responseBody = tryReadXHRBody(xhr.response);
23497
+ }
23498
+ getRequestPerformanceEntry(
23499
+ win,
23500
+ 'xmlhttprequest',
23501
+ req.url,
23502
+ after,
23503
+ before
23504
+ )
23505
+ .then(function(entry) {
23506
+ if (!entry) {
23507
+ logger$3.error('Failed to get performance entry for XHR request to ' + req.url);
23508
+ return;
23509
+ }
23510
+ /** @type {NetworkRequest} */
23511
+ var request = {
23512
+ url: entry.name,
23513
+ method: req.method,
23514
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23515
+ status: xhr.status,
23516
+ startTime: Math.round(entry.startTime),
23517
+ endTime: Math.round(entry.responseEnd),
23518
+ timeOrigin: getTimeOrigin(win),
23519
+ requestHeaders: networkRequest.requestHeaders,
23520
+ requestBody: networkRequest.requestBody,
23521
+ responseHeaders: networkRequest.responseHeaders,
23522
+ responseBody: networkRequest.responseBody,
23523
+ };
23524
+ cb({ requests: [request] });
23525
+ })
23526
+ .catch(function(e) {
23527
+ logger$3.error('Error recording XHR request to ' + req.url + ': ' + String(e));
23528
+ });
23529
+ });
23530
+
23531
+ originalOpen.call(xhr, method, url, async, username, password);
23532
+ };
23533
+ }
23534
+ );
23535
+ return function() {
23536
+ restorePatch();
23537
+ };
23538
+ }
23539
+
23540
+ /**
23541
+ * @param {networkCallback} cb
23542
+ * @param {Window} win
23543
+ * @param {Required<NetworkRecordOptions>} options
23544
+ * @returns {listenerHandler}
23545
+ */
23546
+ function initFetchObserver(cb, win, options) {
23547
+ if (!options.initiatorTypes.includes('fetch')) {
23548
+ return function() {
23549
+ //
23550
+ };
23551
+ }
23552
+
23553
+ var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
23554
+ return function() {
23555
+ var req = new Request(arguments[0], arguments[1]);
23556
+ /** @type {Response | undefined} */
23557
+ var res;
23558
+ /** @type {Partial<NetworkRequest>} */
23559
+ var networkRequest = {};
23560
+ /** @type {number | undefined} */
23561
+ var after;
23562
+ /** @type {number | undefined} */
23563
+ var before;
23564
+
23565
+ var originalFetchPromise;
23566
+ var requestBodyPromise = Promise.resolve(undefined);
23567
+ var responseBodyPromise = Promise.resolve(undefined);
23568
+ try {
23569
+ /** @type {Headers} */
23570
+ var requestHeaders = {};
23571
+ req.headers.forEach(function(value, header) {
23572
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23573
+ requestHeaders[header] = value;
23574
+ }
23575
+ });
23576
+ networkRequest.requestHeaders = requestHeaders;
23577
+
23578
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23579
+ requestBodyPromise = tryReadFetchBody(req)
23580
+ .then(function(body) {
23581
+ networkRequest.requestBody = body;
23582
+ });
23583
+ }
23584
+
23585
+ after = win.performance.now();
23586
+ originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
23587
+ res = response;
23588
+ before = win.performance.now();
23589
+
23590
+ /** @type {Headers} */
23591
+ var responseHeaders = {};
23592
+ res.headers.forEach(function(value, header) {
23593
+ if (shouldRecordHeader('response', options.recordHeaders, header)) {
23594
+ responseHeaders[header] = value;
23595
+ }
23596
+ });
23597
+ networkRequest.responseHeaders = responseHeaders;
23598
+
23599
+ if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
23600
+ responseBodyPromise = tryReadFetchBody(res)
23601
+ .then(function(body) {
23602
+ networkRequest.responseBody = body;
23603
+ });
23604
+ }
23605
+
23606
+ return res;
23607
+ });
23608
+ } catch (e) {
23609
+ originalFetchPromise = Promise.reject(e);
23610
+ }
23611
+
23612
+ // await concurrently so we don't delay the fetch response
23613
+ Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
23614
+ .then(function () {
23615
+ return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
23616
+ })
23617
+ .then(function(entry) {
23618
+ if (!entry) {
23619
+ logger$3.error('Failed to get performance entry for fetch request to ' + req.url);
23620
+ return;
23621
+ }
23622
+ /** @type {NetworkRequest} */
23623
+ var request = {
23624
+ url: entry.name,
23625
+ method: req.method,
23626
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23627
+ status: res ? res.status : undefined,
23628
+ startTime: Math.round(entry.startTime),
23629
+ endTime: Math.round(entry.responseEnd),
23630
+ timeOrigin: getTimeOrigin(win),
23631
+ requestHeaders: networkRequest.requestHeaders,
23632
+ requestBody: networkRequest.requestBody,
23633
+ responseHeaders: networkRequest.responseHeaders,
23634
+ responseBody: networkRequest.responseBody,
23635
+ };
23636
+ cb({ requests: [request] });
23637
+ })
23638
+ .catch(function (e) {
23639
+ logger$3.error('Error recording fetch request to ' + req.url + ': ' + String(e));
23640
+ });
23641
+
23642
+ return originalFetchPromise;
23643
+ };
23644
+ });
23645
+ return function() {
23646
+ restorePatch();
23647
+ };
23648
+ }
23649
+
23650
+ /**
23651
+ * @param {networkCallback} callback
23652
+ * @param {Window} win
23653
+ * @param {NetworkRecordOptions} options
23654
+ * @returns {listenerHandler}
23655
+ */
23656
+ function initNetworkObserver(callback, win, options) {
23657
+ if (!('performance' in win)) {
23658
+ return function() {
23659
+ //
23660
+ };
23661
+ }
23662
+
23663
+ var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
23664
+ var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
23665
+ options = Object.assign({}, options, {
23666
+ recordHeaders: recordHeaders,
23667
+ recordBodyUrls: recordBodyUrls,
23668
+ });
23669
+ var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
23670
+
23671
+ /** @type {networkCallback} */
23672
+ var cb = function(data) {
23673
+ var requests = data.requests.filter(function(request) {
23674
+ var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
23675
+ return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
23676
+ });
23677
+ if (requests.length > 0 || data.isInitial) {
23678
+ callback(Object.assign({}, data, { requests: requests }));
23679
+ }
23680
+ };
23681
+ var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
23682
+ var xhrObserver = initXhrObserver(cb, win, networkOptions);
23683
+ var fetchObserver = initFetchObserver(cb, win, networkOptions);
23684
+ return function() {
23685
+ performanceObserver();
23686
+ xhrObserver();
23687
+ fetchObserver();
23688
+ };
23689
+ }
23690
+
23691
+ // arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
23692
+ // a changed format in the mixpanel product.
23693
+ var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
23694
+
23695
+ /**
23696
+ * @param {NetworkRecordOptions} [options]
23697
+ * @returns {RecordPlugin}
23698
+ */
23699
+ var getRecordNetworkPlugin = function(options) {
23700
+ return {
23701
+ name: NETWORK_PLUGIN_NAME,
23702
+ observer: initNetworkObserver,
23703
+ options: options,
23704
+ };
23705
+ };
23706
+
23707
+ /**
23708
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23709
+ */
23710
+
23711
+
23712
+ var logger$2 = console_with_prefix('recorder');
23713
+ var CompressionStream = win['CompressionStream'];
23714
+
23715
+ var RECORDER_BATCHER_LIB_CONFIG = {
23716
+ 'batch_size': 1000,
23717
+ 'batch_flush_interval_ms': 10 * 1000,
23718
+ 'batch_request_timeout_ms': 90 * 1000,
23719
+ 'batch_autostart': true
23720
+ };
23721
+
23722
+ var ACTIVE_SOURCES = new Set([
23723
+ IncrementalSource.MouseMove,
23724
+ IncrementalSource.MouseInteraction,
23725
+ IncrementalSource.Scroll,
23726
+ IncrementalSource.ViewportResize,
23727
+ IncrementalSource.Input,
23728
+ IncrementalSource.TouchMove,
23729
+ IncrementalSource.MediaInteraction,
23730
+ IncrementalSource.Drag,
23731
+ IncrementalSource.Selection,
23732
+ ]);
23733
+
23734
+ function isUserEvent(ev) {
23735
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23736
+ }
23737
+
23738
+ /**
23739
+ * @typedef {Object} SerializedRecording
23740
+ * @property {number} idleExpires
23741
+ * @property {number} maxExpires
23742
+ * @property {number} replayStartTime
23743
+ * @property {number} lastEventTimestamp
23744
+ * @property {number} seqNo
23745
+ * @property {string} batchStartUrl
23746
+ * @property {string} replayId
23747
+ * @property {string} tabId
23748
+ * @property {string} replayStartUrl
23749
+ */
23750
+
23751
+ /**
23752
+ * @typedef {Object} SessionRecordingOptions
23753
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23754
+ * @property {String} [options.replayId] - unique uuid for a single replay
23755
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23756
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23757
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23758
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23759
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23760
+ * optional properties for deserialization:
23761
+ * @property {number} idleExpires
23762
+ * @property {number} maxExpires
23763
+ * @property {number} replayStartTime
23764
+ * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23765
+ * @property {number} seqNo
23766
+ * @property {string} batchStartUrl
23767
+ * @property {string} replayStartUrl
23768
+ */
23769
+
23770
+ /**
23771
+ * @typedef {Object} UserIdInfo
23772
+ * @property {string} distinct_id
23773
+ * @property {string} user_id
23774
+ * @property {string} device_id
23775
+ */
23776
+
23777
+
23778
+ /**
23779
+ * This class encapsulates a single session recording and its lifecycle.
23780
+ * @param {SessionRecordingOptions} options
23781
+ */
23782
+ var SessionRecording = function(options) {
23783
+ this._mixpanel = options.mixpanelInstance;
23784
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23785
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23786
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23787
+ this._rrwebRecord = options.rrwebRecord || null;
23788
+
23789
+ // internal rrweb stopRecording function
23790
+ this._stopRecording = null;
23791
+ this.replayId = options.replayId;
23792
+
23793
+ this.batchStartUrl = options.batchStartUrl || null;
23794
+ this.replayStartUrl = options.replayStartUrl || null;
23795
+ this.idleExpires = options.idleExpires || null;
23796
+ this.maxExpires = options.maxExpires || null;
23797
+ this.replayStartTime = options.replayStartTime || null;
23798
+ this.lastEventTimestamp = options.lastEventTimestamp || null;
23799
+ this.seqNo = options.seqNo || 0;
23800
+
23801
+ this.idleTimeoutId = null;
23802
+ this.maxTimeoutId = null;
23803
+
23804
+ this.recordMaxMs = MAX_RECORDING_MS;
23805
+ this.recordMinMs = 0;
23806
+
23807
+ // disable persistence if localStorage is not supported
23808
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
23809
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23810
+
23811
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23812
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23813
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23814
+ this.batcher = new RequestBatcher(this.batcherKey, {
23815
+ errorReporter: this.reportError.bind(this),
23816
+ flushOnlyOnInterval: true,
23817
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
23818
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23819
+ queueStorage: this.queueStorage,
23820
+ sharedLockStorage: options.sharedLockStorage,
23821
+ usePersistence: usePersistence,
23822
+ stopAllBatchingFunc: this.stopRecording.bind(this),
23823
+
23824
+ // increased throttle and shared lock timeout because recording events are very high frequency.
23825
+ // this will minimize the amount of lock contention between enqueued events.
23826
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23827
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23828
+ sharedLockTimeoutMS: 10 * 1000,
23829
+ });
23830
+ };
23831
+
23832
+ /**
23833
+ * @returns {UserIdInfo}
23834
+ */
23835
+ SessionRecording.prototype.getUserIdInfo = function () {
23836
+ if (this.finalFlushUserIdInfo) {
23837
+ return this.finalFlushUserIdInfo;
23838
+ }
23839
+
23840
+ var userIdInfo = {
23841
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
23842
+ };
23843
+
23844
+ // send ID management props if they exist
23845
+ var deviceId = this._mixpanel.get_property('$device_id');
23846
+ if (deviceId) {
23847
+ userIdInfo['$device_id'] = deviceId;
23848
+ }
23849
+ var userId = this._mixpanel.get_property('$user_id');
23850
+ if (userId) {
23851
+ userIdInfo['$user_id'] = userId;
23852
+ }
23853
+ return userIdInfo;
23854
+ };
23855
+
23856
+ SessionRecording.prototype.unloadPersistedData = function () {
23857
+ this.batcher.stop();
23858
+
23859
+ return this.queueStorage.init().catch(function () {
23860
+ this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23861
+ }.bind(this)).then(function () {
23862
+ // if the recording is too short, just delete any stored events without flushing
23863
+ if (this.getDurationMs() < this._getRecordMinMs()) {
23864
+ return this.queueStorage.removeItem(this.batcherKey);
23865
+ }
23866
+
23867
+ return this.batcher.flush()
23868
+ .then(function () {
23869
+ return this.queueStorage.removeItem(this.batcherKey);
23870
+ }.bind(this));
23871
+ }.bind(this));
23872
+ };
23873
+
23874
+ SessionRecording.prototype.getConfig = function(configVar) {
23103
23875
  return this._mixpanel.get_config(configVar);
23104
23876
  };
23105
23877
 
@@ -23165,6 +23937,29 @@
23165
23937
 
23166
23938
  var privacyConfig = getPrivacyConfig(this._mixpanel);
23167
23939
 
23940
+ var plugins = [];
23941
+ if (this.getConfig('record_network')) {
23942
+ var options = this.getConfig('record_network_options') || {};
23943
+ // don't track requests to Mixpanel /record API
23944
+ var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
23945
+ ignoreRequestUrls.push(this._getApiRoute());
23946
+ options.ignoreRequestUrls = ignoreRequestUrls;
23947
+
23948
+ plugins.push(getRecordNetworkPlugin(options));
23949
+ }
23950
+
23951
+ if (this.getConfig('record_console')) {
23952
+ plugins.push(
23953
+ getRecordConsolePlugin({
23954
+ stringifyOptions: {
23955
+ stringLengthLimit: 1000,
23956
+ numOfKeysLimit: 50,
23957
+ depthOfLimit: 2
23958
+ }
23959
+ })
23960
+ );
23961
+ }
23962
+
23168
23963
  try {
23169
23964
  this._stopRecording = this._rrwebRecord({
23170
23965
  'emit': function (ev) {
@@ -23203,15 +23998,7 @@
23203
23998
  'sampling': {
23204
23999
  'canvas': 15
23205
24000
  },
23206
- 'plugins': this.getConfig('record_console') ? [
23207
- getRecordConsolePlugin({
23208
- stringifyOptions: {
23209
- stringLengthLimit: 1000,
23210
- numOfKeysLimit: 50,
23211
- depthOfLimit: 2
23212
- }
23213
- })
23214
- ] : []
24001
+ 'plugins': plugins,
23215
24002
  });
23216
24003
  } catch (err) {
23217
24004
  this.reportError('Unexpected error when starting rrweb recording.', err);
@@ -23326,6 +24113,10 @@
23326
24113
  return recording;
23327
24114
  };
23328
24115
 
24116
+ SessionRecording.prototype._getApiRoute = function () {
24117
+ return this.getConfig('api_routes')['record'];
24118
+ };
24119
+
23329
24120
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
23330
24121
  var onSuccess = function (response, responseBody) {
23331
24122
  // Update batch specific props only if the request was successful to guarantee ordering.
@@ -23345,7 +24136,7 @@
23345
24136
  });
23346
24137
  }.bind(this);
23347
24138
  var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
23348
- win['fetch'](apiHost + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
24139
+ win['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
23349
24140
  'method': 'POST',
23350
24141
  'headers': {
23351
24142
  'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
@@ -23746,8 +24537,12 @@
23746
24537
  this.startRecording({shouldStopBatcher: true});
23747
24538
  };
23748
24539
 
24540
+ MixpanelRecorder.prototype.isRecording = function () {
24541
+ return this.activeRecording && !this.activeRecording.isRrwebStopped();
24542
+ };
24543
+
23749
24544
  MixpanelRecorder.prototype.getActiveReplayId = function () {
23750
- if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
24545
+ if (this.isRecording()) {
23751
24546
  return this.activeRecording.replayId;
23752
24547
  } else {
23753
24548
  return null;
@@ -23762,7 +24557,7 @@
23762
24557
  }
23763
24558
  });
23764
24559
 
23765
- win['__mp_recorder'] = MixpanelRecorder;
24560
+ win[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
23766
24561
 
23767
24562
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
23768
24563
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
@@ -23864,7 +24659,7 @@
23864
24659
  observer.observe(shadowRoot, this.observerConfig);
23865
24660
  this.shadowObservers.push(observer);
23866
24661
  } catch (e) {
23867
- logger$3.critical('Error while observing shadow root', e);
24662
+ logger$4.critical('Error while observing shadow root', e);
23868
24663
  }
23869
24664
  };
23870
24665
 
@@ -23875,7 +24670,7 @@
23875
24670
  }
23876
24671
 
23877
24672
  if (!weakSetSupported()) {
23878
- logger$3.critical('Shadow DOM observation unavailable: WeakSet not supported');
24673
+ logger$4.critical('Shadow DOM observation unavailable: WeakSet not supported');
23879
24674
  return;
23880
24675
  }
23881
24676
 
@@ -23891,7 +24686,7 @@
23891
24686
  try {
23892
24687
  this.shadowObservers[i].disconnect();
23893
24688
  } catch (e) {
23894
- logger$3.critical('Error while disconnecting shadow DOM observer', e);
24689
+ logger$4.critical('Error while disconnecting shadow DOM observer', e);
23895
24690
  }
23896
24691
  }
23897
24692
  this.shadowObservers = [];
@@ -24079,7 +24874,7 @@
24079
24874
 
24080
24875
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
24081
24876
  } catch (e) {
24082
- logger$3.critical('Error while setting up mutation observer', e);
24877
+ logger$4.critical('Error while setting up mutation observer', e);
24083
24878
  }
24084
24879
  }
24085
24880
 
@@ -24094,7 +24889,7 @@
24094
24889
  );
24095
24890
  this.shadowDOMObserver.start();
24096
24891
  } catch (e) {
24097
- logger$3.critical('Error while setting up shadow DOM observer', e);
24892
+ logger$4.critical('Error while setting up shadow DOM observer', e);
24098
24893
  this.shadowDOMObserver = null;
24099
24894
  }
24100
24895
  }
@@ -24121,7 +24916,7 @@
24121
24916
  try {
24122
24917
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
24123
24918
  } catch (e) {
24124
- logger$3.critical('Error while removing event listener', e);
24919
+ logger$4.critical('Error while removing event listener', e);
24125
24920
  }
24126
24921
  }
24127
24922
  this.eventListeners = [];
@@ -24130,7 +24925,7 @@
24130
24925
  try {
24131
24926
  this.mutationObserver.disconnect();
24132
24927
  } catch (e) {
24133
- logger$3.critical('Error while disconnecting mutation observer', e);
24928
+ logger$4.critical('Error while disconnecting mutation observer', e);
24134
24929
  }
24135
24930
  this.mutationObserver = null;
24136
24931
  }
@@ -24139,7 +24934,7 @@
24139
24934
  try {
24140
24935
  this.shadowDOMObserver.stop();
24141
24936
  } catch (e) {
24142
- logger$3.critical('Error while stopping shadow DOM observer', e);
24937
+ logger$4.critical('Error while stopping shadow DOM observer', e);
24143
24938
  }
24144
24939
  this.shadowDOMObserver = null;
24145
24940
  }
@@ -24217,7 +25012,7 @@
24217
25012
 
24218
25013
  Autocapture.prototype.init = function() {
24219
25014
  if (!minDOMApisSupported()) {
24220
- logger$3.critical('Autocapture unavailable: missing required DOM APIs');
25015
+ logger$4.critical('Autocapture unavailable: missing required DOM APIs');
24221
25016
  return;
24222
25017
  }
24223
25018
  this.initPageListeners();
@@ -24249,27 +25044,15 @@
24249
25044
  };
24250
25045
 
24251
25046
  Autocapture.prototype.currentUrlBlocked = function() {
24252
- var i;
24253
25047
  var currentUrl = _.info.currentUrl();
24254
25048
 
24255
25049
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
24256
25050
  if (allowUrlRegexes.length) {
24257
25051
  // we're using an allowlist, only track if current URL matches
24258
- var allowed = false;
24259
- for (i = 0; i < allowUrlRegexes.length; i++) {
24260
- var allowRegex = allowUrlRegexes[i];
24261
- try {
24262
- if (currentUrl.match(allowRegex)) {
24263
- allowed = true;
24264
- break;
24265
- }
24266
- } catch (err) {
24267
- logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
24268
- return true;
24269
- }
24270
- }
24271
- if (!allowed) {
24272
- // wasn't allowed by any regex
25052
+ try {
25053
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
25054
+ } catch (err) {
25055
+ logger$4.critical('Error while checking block URL regexes: ', err);
24273
25056
  return true;
24274
25057
  }
24275
25058
  }
@@ -24279,17 +25062,12 @@
24279
25062
  return false;
24280
25063
  }
24281
25064
 
24282
- for (i = 0; i < blockUrlRegexes.length; i++) {
24283
- try {
24284
- if (currentUrl.match(blockUrlRegexes[i])) {
24285
- return true;
24286
- }
24287
- } catch (err) {
24288
- logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
24289
- return true;
24290
- }
25065
+ try {
25066
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
25067
+ } catch (err) {
25068
+ logger$4.critical('Error while checking block URL regexes: ', err);
25069
+ return true;
24291
25070
  }
24292
- return false;
24293
25071
  };
24294
25072
 
24295
25073
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -24425,7 +25203,7 @@
24425
25203
  return;
24426
25204
  }
24427
25205
 
24428
- logger$3.log('Initializing scroll depth tracking');
25206
+ logger$4.log('Initializing scroll depth tracking');
24429
25207
 
24430
25208
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
24431
25209
 
@@ -24451,7 +25229,7 @@
24451
25229
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
24452
25230
  return;
24453
25231
  }
24454
- logger$3.log('Initializing click tracking');
25232
+ logger$4.log('Initializing click tracking');
24455
25233
 
24456
25234
  this.listenerClick = function(ev) {
24457
25235
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -24470,7 +25248,7 @@
24470
25248
  return;
24471
25249
  }
24472
25250
 
24473
- logger$3.log('Initializing dead click tracking');
25251
+ logger$4.log('Initializing dead click tracking');
24474
25252
  if (!this._deadClickTracker) {
24475
25253
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
24476
25254
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -24504,7 +25282,7 @@
24504
25282
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
24505
25283
  return;
24506
25284
  }
24507
- logger$3.log('Initializing input tracking');
25285
+ logger$4.log('Initializing input tracking');
24508
25286
 
24509
25287
  this.listenerChange = function(ev) {
24510
25288
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -24521,7 +25299,7 @@
24521
25299
  if (!this.pageviewTrackingConfig()) {
24522
25300
  return;
24523
25301
  }
24524
- logger$3.log('Initializing pageview tracking');
25302
+ logger$4.log('Initializing pageview tracking');
24525
25303
 
24526
25304
  var previousTrackedUrl = '';
24527
25305
  var tracked = false;
@@ -24556,7 +25334,7 @@
24556
25334
  }
24557
25335
  if (didPathChange) {
24558
25336
  this.lastScrollCheckpoint = 0;
24559
- logger$3.log('Path change: re-initializing scroll depth checkpoints');
25337
+ logger$4.log('Path change: re-initializing scroll depth checkpoints');
24560
25338
  }
24561
25339
  }
24562
25340
  }.bind(this));
@@ -24571,7 +25349,7 @@
24571
25349
  return;
24572
25350
  }
24573
25351
 
24574
- logger$3.log('Initializing rage click tracking');
25352
+ logger$4.log('Initializing rage click tracking');
24575
25353
  if (!this._rageClickTracker) {
24576
25354
  this._rageClickTracker = new RageClickTracker();
24577
25355
  }
@@ -24601,7 +25379,7 @@
24601
25379
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
24602
25380
  return;
24603
25381
  }
24604
- logger$3.log('Initializing scroll tracking');
25382
+ logger$4.log('Initializing scroll tracking');
24605
25383
  this.lastScrollCheckpoint = 0;
24606
25384
 
24607
25385
  var scrollTrackFunction = function() {
@@ -24638,7 +25416,7 @@
24638
25416
  }
24639
25417
  }
24640
25418
  } catch (err) {
24641
- logger$3.critical('Error while calculating scroll percentage', err);
25419
+ logger$4.critical('Error while calculating scroll percentage', err);
24642
25420
  }
24643
25421
  if (shouldTrack) {
24644
25422
  this.mp.track(MP_EV_SCROLL, props);
@@ -24656,7 +25434,7 @@
24656
25434
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
24657
25435
  return;
24658
25436
  }
24659
- logger$3.log('Initializing submit tracking');
25437
+ logger$4.log('Initializing submit tracking');
24660
25438
 
24661
25439
  this.listenerSubmit = function(ev) {
24662
25440
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -24678,7 +25456,7 @@
24678
25456
  return;
24679
25457
  }
24680
25458
 
24681
- logger$3.log('Initializing page visibility tracking.');
25459
+ logger$4.log('Initializing page visibility tracking.');
24682
25460
  this._initScrollDepthTracking();
24683
25461
  var previousTrackedUrl = _.info.currentUrl();
24684
25462
 
@@ -24733,14 +25511,62 @@
24733
25511
  // TODO integrate error_reporter from mixpanel instance
24734
25512
  safewrapClass(Autocapture);
24735
25513
 
24736
- var logger = console_with_prefix('flags');
25514
+ /**
25515
+ * Get the promise-based targeting loader
25516
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
25517
+ * @param {string} targetingSrc - URL to targeting bundle
25518
+ * @returns {Promise} Promise that resolves with targeting library
25519
+ */
25520
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
25521
+ // Return existing promise if already initialized or loading
25522
+ if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
25523
+ return win[TARGETING_GLOBAL_NAME];
25524
+ }
25525
+
25526
+ // Create loading promise and set it as the global immediately
25527
+ // This makes minified build behavior consistent with dev/CJS builds
25528
+ win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
25529
+ loadExtraBundle(targetingSrc, resolve);
25530
+ }).then(function () {
25531
+ var p = win[TARGETING_GLOBAL_NAME];
25532
+ if (p && typeof p.then === 'function') {
25533
+ return p;
25534
+ }
25535
+ throw new Error('targeting failed to load');
25536
+ }).catch(function (err) {
25537
+ delete win[TARGETING_GLOBAL_NAME];
25538
+ throw err;
25539
+ });
25540
+
25541
+ return win[TARGETING_GLOBAL_NAME];
25542
+ };
24737
25543
 
25544
+ var logger = console_with_prefix('flags');
24738
25545
  var FLAGS_CONFIG_KEY = 'flags';
24739
25546
 
24740
25547
  var CONFIG_CONTEXT = 'context';
24741
25548
  var CONFIG_DEFAULTS = {};
24742
25549
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
24743
25550
 
25551
+ /**
25552
+ * Generate a unique key for a pending first-time event
25553
+ * @param {string} flagKey - The flag key
25554
+ * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
25555
+ * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
25556
+ */
25557
+ var getPendingEventKey = function(flagKey, firstTimeEventHash) {
25558
+ return flagKey + ':' + firstTimeEventHash;
25559
+ };
25560
+
25561
+ /**
25562
+ * Extract the flag key from a pending event key
25563
+ * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
25564
+ * @returns {string} The flag key portion
25565
+ */
25566
+ var getFlagKeyFromPendingEventKey = function(eventKey) {
25567
+ return eventKey.split(':')[0];
25568
+ };
25569
+
24744
25570
  /**
24745
25571
  * FeatureFlagManager: support for Mixpanel's feature flagging product
24746
25572
  * @constructor
@@ -24752,6 +25578,8 @@
24752
25578
  this.setMpConfig = initOptions.setConfigFunc;
24753
25579
  this.getMpProperty = initOptions.getPropertyFunc;
24754
25580
  this.track = initOptions.trackingFunc;
25581
+ this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
25582
+ this.targetingSrc = initOptions.targetingSrc || '';
24755
25583
  };
24756
25584
 
24757
25585
  FeatureFlagManager.prototype.init = function() {
@@ -24764,6 +25592,8 @@
24764
25592
  this.fetchFlags();
24765
25593
 
24766
25594
  this.trackedFeatures = new Set();
25595
+ this.pendingFirstTimeEvents = {};
25596
+ this.activatedFirstTimeEvents = {};
24767
25597
  };
24768
25598
 
24769
25599
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -24844,17 +25674,78 @@
24844
25674
  throw new Error('No flags in API response');
24845
25675
  }
24846
25676
  var flags = new Map();
25677
+ var pendingFirstTimeEvents = {};
25678
+
25679
+ // Process flags from response
24847
25680
  _.each(responseFlags, function(data, key) {
24848
- flags.set(key, {
24849
- 'key': data['variant_key'],
24850
- 'value': data['variant_value'],
24851
- 'experiment_id': data['experiment_id'],
24852
- 'is_experiment_active': data['is_experiment_active'],
24853
- 'is_qa_tester': data['is_qa_tester']
25681
+ // Check if this flag has any activated first-time events this session
25682
+ var hasActivatedEvent = false;
25683
+ var prefix = key + ':';
25684
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
25685
+ if (eventKey.startsWith(prefix)) {
25686
+ hasActivatedEvent = true;
25687
+ }
24854
25688
  });
24855
- });
25689
+
25690
+ if (hasActivatedEvent) {
25691
+ // Preserve the activated variant, don't overwrite with server's current variant
25692
+ var currentFlag = this.flags && this.flags.get(key);
25693
+ if (currentFlag) {
25694
+ flags.set(key, currentFlag);
25695
+ }
25696
+ } else {
25697
+ // Use server's current variant
25698
+ flags.set(key, {
25699
+ 'key': data['variant_key'],
25700
+ 'value': data['variant_value'],
25701
+ 'experiment_id': data['experiment_id'],
25702
+ 'is_experiment_active': data['is_experiment_active'],
25703
+ 'is_qa_tester': data['is_qa_tester']
25704
+ });
25705
+ }
25706
+ }, this);
25707
+
25708
+ // Process top-level pending_first_time_events array
25709
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
25710
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
25711
+ _.each(topLevelDefinitions, function(def) {
25712
+ var flagKey = def['flag_key'];
25713
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
25714
+
25715
+ // Skip if this specific event has already been activated this session
25716
+ if (this.activatedFirstTimeEvents[eventKey]) {
25717
+ return;
25718
+ }
25719
+
25720
+ // Store pending event definition using composite key
25721
+ pendingFirstTimeEvents[eventKey] = {
25722
+ 'flag_key': flagKey,
25723
+ 'flag_id': def['flag_id'],
25724
+ 'project_id': def['project_id'],
25725
+ 'first_time_event_hash': def['first_time_event_hash'],
25726
+ 'event_name': def['event_name'],
25727
+ 'property_filters': def['property_filters'],
25728
+ 'pending_variant': def['pending_variant']
25729
+ };
25730
+ }, this);
25731
+ }
25732
+
25733
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
25734
+ if (this.activatedFirstTimeEvents) {
25735
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
25736
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
25737
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
25738
+ // Keep the activated flag even though it's not in the new response
25739
+ flags.set(flagKey, this.flags.get(flagKey));
25740
+ }
25741
+ }, this);
25742
+ }
25743
+
24856
25744
  this.flags = flags;
25745
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
24857
25746
  this._traceparent = traceparent;
25747
+
25748
+ this._loadTargetingIfNeeded();
24858
25749
  }.bind(this)).catch(function(error) {
24859
25750
  this.markFetchComplete();
24860
25751
  logger.error(error);
@@ -24867,15 +25758,186 @@
24867
25758
  return this.fetchPromise;
24868
25759
  };
24869
25760
 
24870
- FeatureFlagManager.prototype.markFetchComplete = function() {
24871
- if (!this._fetchInProgressStartTime) {
24872
- logger.error('Fetch in progress started time not set, cannot mark fetch complete');
24873
- return;
24874
- }
24875
- this._fetchStartTime = this._fetchInProgressStartTime;
24876
- this._fetchCompleteTime = Date.now();
24877
- this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
24878
- this._fetchInProgressStartTime = null;
25761
+ FeatureFlagManager.prototype.markFetchComplete = function() {
25762
+ if (!this._fetchInProgressStartTime) {
25763
+ logger.error('Fetch in progress started time not set, cannot mark fetch complete');
25764
+ return;
25765
+ }
25766
+ this._fetchStartTime = this._fetchInProgressStartTime;
25767
+ this._fetchCompleteTime = Date.now();
25768
+ this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
25769
+ this._fetchInProgressStartTime = null;
25770
+ };
25771
+
25772
+ /**
25773
+ * Proactively load targeting bundle if any pending events have property filters
25774
+ */
25775
+ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
25776
+ var hasPropertyFilters = false;
25777
+ _.each(this.pendingFirstTimeEvents, function(evt) {
25778
+ if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
25779
+ hasPropertyFilters = true;
25780
+ }
25781
+ });
25782
+
25783
+ if (hasPropertyFilters) {
25784
+ this.getTargeting().then(function() {
25785
+ logger.log('targeting loaded for property filter evaluation');
25786
+ });
25787
+ }
25788
+ };
25789
+
25790
+ /**
25791
+ * Get the targeting library (initializes if not already loaded)
25792
+ * This method is primarily for testing - production code should rely on automatic loading
25793
+ * @returns {Promise} Promise that resolves with targeting library
25794
+ */
25795
+ FeatureFlagManager.prototype.getTargeting = function() {
25796
+ return getTargetingPromise(
25797
+ this.loadExtraBundle.bind(this),
25798
+ this.targetingSrc
25799
+ ).catch(function(error) {
25800
+ logger.error('Failed to load targeting: ' + error);
25801
+ }.bind(this));
25802
+ };
25803
+
25804
+ /**
25805
+ * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
25806
+ * @param {string} eventName - The name of the event being tracked
25807
+ * @param {Object} properties - Event properties to evaluate against property filters
25808
+ *
25809
+ * When a match is found (event name matches and property filters pass), this method:
25810
+ * - Switches the flag to the pending variant
25811
+ * - Marks the event as activated for this session
25812
+ * - Records the activation via the API (fire-and-forget)
25813
+ */
25814
+ FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
25815
+ if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
25816
+ return;
25817
+ }
25818
+
25819
+ // Check if targeting promise exists (either bundled or async loaded)
25820
+ if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
25821
+ win[TARGETING_GLOBAL_NAME].then(function(library) {
25822
+ this._processFirstTimeEventCheck(eventName, properties, library);
25823
+ }.bind(this)).catch(function() {
25824
+ // If targeting failed to load, process with null
25825
+ // Events without property filters will still match
25826
+ this._processFirstTimeEventCheck(eventName, properties, null);
25827
+ }.bind(this));
25828
+ } else {
25829
+ // No targeting available, process with null
25830
+ // Events without property filters will still match
25831
+ this._processFirstTimeEventCheck(eventName, properties, null);
25832
+ }
25833
+ };
25834
+
25835
+ /**
25836
+ * Internal method to process first-time event checks with loaded targeting library
25837
+ * @param {string} eventName - The name of the event being tracked
25838
+ * @param {Object} properties - Event properties to evaluate against property filters
25839
+ * @param {Object} targeting - The loaded targeting library
25840
+ */
25841
+ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
25842
+ _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
25843
+ if (this.activatedFirstTimeEvents[eventKey]) {
25844
+ return;
25845
+ }
25846
+
25847
+ var flagKey = pendingEvent['flag_key'];
25848
+
25849
+ // Use targeting module to check if event matches
25850
+ var matchResult;
25851
+
25852
+ // If no targeting library and event has property filters, skip it
25853
+ if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
25854
+ logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
25855
+ return;
25856
+ }
25857
+
25858
+ // For simple events (no property filters), just check event name
25859
+ if (!targeting) {
25860
+ matchResult = {
25861
+ matches: eventName === pendingEvent['event_name'],
25862
+ error: null
25863
+ };
25864
+ } else {
25865
+ var criteria = {
25866
+ 'event_name': pendingEvent['event_name'],
25867
+ 'property_filters': pendingEvent['property_filters']
25868
+ };
25869
+ matchResult = targeting['eventMatchesCriteria'](
25870
+ eventName,
25871
+ properties,
25872
+ criteria
25873
+ );
25874
+ }
25875
+
25876
+ if (matchResult.error) {
25877
+ logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
25878
+ return;
25879
+ }
25880
+
25881
+ if (!matchResult.matches) {
25882
+ return;
25883
+ }
25884
+
25885
+ logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
25886
+
25887
+ var newVariant = {
25888
+ 'key': pendingEvent['pending_variant']['variant_key'],
25889
+ 'value': pendingEvent['pending_variant']['variant_value'],
25890
+ 'experiment_id': pendingEvent['pending_variant']['experiment_id'],
25891
+ 'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
25892
+ };
25893
+
25894
+ this.flags.set(flagKey, newVariant);
25895
+ this.activatedFirstTimeEvents[eventKey] = true;
25896
+
25897
+ this.recordFirstTimeEvent(
25898
+ pendingEvent['flag_id'],
25899
+ pendingEvent['project_id'],
25900
+ pendingEvent['first_time_event_hash']
25901
+ );
25902
+ }, this);
25903
+ };
25904
+
25905
+ FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
25906
+ // Construct URL: {api_host}/flags/{flagId}/first-time-events
25907
+ return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
25908
+ };
25909
+
25910
+ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
25911
+ var distinctId = this.getMpProperty('distinct_id');
25912
+ var traceparent = generateTraceparent();
25913
+
25914
+ // Build URL with query string parameters
25915
+ var searchParams = new URLSearchParams();
25916
+ searchParams.set('mp_lib', 'web');
25917
+ searchParams.set('$lib_version', Config.LIB_VERSION);
25918
+ var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
25919
+
25920
+ var payload = {
25921
+ 'distinct_id': distinctId,
25922
+ 'project_id': projectId,
25923
+ 'first_time_event_hash': firstTimeEventHash
25924
+ };
25925
+
25926
+ logger.log('Recording first-time event for flag: ' + flagId);
25927
+
25928
+ // Fire-and-forget POST request
25929
+ this.fetch.call(win, url, {
25930
+ 'method': 'POST',
25931
+ 'headers': {
25932
+ 'Content-Type': 'application/json',
25933
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
25934
+ 'traceparent': traceparent
25935
+ },
25936
+ 'body': JSON.stringify(payload)
25937
+ }).catch(function(error) {
25938
+ // Silent failure - cohort sync will catch up
25939
+ logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
25940
+ });
24879
25941
  };
24880
25942
 
24881
25943
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
@@ -24996,6 +26058,217 @@
24996
26058
  // Deprecated method
24997
26059
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
24998
26060
 
26061
+ // Exports intended only for testing
26062
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26063
+
26064
+ /* eslint camelcase: "off" */
26065
+
26066
+
26067
+ /**
26068
+ * RecorderManager: manages session recording initialization, lifecycle and state
26069
+ * @constructor
26070
+ */
26071
+ var RecorderManager = function(initOptions) {
26072
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
26073
+ // but ideally we should be able to remove this dependency.
26074
+ this.mixpanelInstance = initOptions.mixpanelInstance;
26075
+
26076
+ this.getMpConfig = initOptions.getConfigFunc;
26077
+ this.getTabId = initOptions.getTabIdFunc;
26078
+ this.reportError = initOptions.reportErrorFunc;
26079
+ this.getDistinctId = initOptions.getDistinctIdFunc;
26080
+ this.loadExtraBundle = initOptions.loadExtraBundle;
26081
+ this.recorderSrc = initOptions.recorderSrc;
26082
+ this.targetingSrc = initOptions.targetingSrc;
26083
+ this.libBasePath = initOptions.libBasePath;
26084
+
26085
+ this._recorder = null;
26086
+ };
26087
+
26088
+ RecorderManager.prototype.shouldLoadRecorder = function() {
26089
+ if (this.getMpConfig('disable_persistence')) {
26090
+ console$1.log('Load recorder check skipped due to disable_persistence config');
26091
+ return PromisePolyfill.resolve(false);
26092
+ }
26093
+
26094
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26095
+ var tab_id = this.getTabId();
26096
+ return recording_registry_idb.init()
26097
+ .then(function () {
26098
+ return recording_registry_idb.getAll();
26099
+ })
26100
+ .then(function (recordings) {
26101
+ for (var i = 0; i < recordings.length; i++) {
26102
+ // if there are expired recordings in the registry, we should load the recorder to flush them
26103
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
26104
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26105
+ return true;
26106
+ }
26107
+ }
26108
+ return false;
26109
+ })
26110
+ .catch(_.bind(function (err) {
26111
+ this.reportError('Error checking recording registry', err);
26112
+ return false;
26113
+ }, this));
26114
+ };
26115
+
26116
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
26117
+ if (!win['MutationObserver']) {
26118
+ console$1.critical('Browser does not support MutationObserver; skipping session recording');
26119
+ return PromisePolyfill.resolve();
26120
+ }
26121
+
26122
+ var loadRecorder = _.bind(function(startNewIfInactive) {
26123
+ return new PromisePolyfill(_.bind(function(resolve) {
26124
+ var handleLoadedRecorder = safewrap(_.bind(function() {
26125
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
26126
+ this._recorder['resumeRecording'](startNewIfInactive);
26127
+ resolve();
26128
+ }, this));
26129
+
26130
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
26131
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
26132
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
26133
+ } else {
26134
+ handleLoadedRecorder();
26135
+ }
26136
+ }, this));
26137
+ }, this);
26138
+
26139
+ /**
26140
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26141
+ * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
26142
+ */
26143
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
26144
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
26145
+ if (force_start || is_sampled) {
26146
+ return loadRecorder(true);
26147
+ } else {
26148
+ return this.shouldLoadRecorder()
26149
+ .then(_.bind(function (shouldLoad) {
26150
+ if (shouldLoad) {
26151
+ return loadRecorder(false);
26152
+ }
26153
+ return PromisePolyfill.resolve();
26154
+ }, this));
26155
+ }
26156
+ };
26157
+
26158
+ RecorderManager.prototype.isRecording = function() {
26159
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
26160
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
26161
+ return false;
26162
+ }
26163
+ try {
26164
+ return this._recorder['isRecording']();
26165
+ } catch (e) {
26166
+ this.reportError('Error checking if recording is active', e);
26167
+ return false;
26168
+ }
26169
+ };
26170
+
26171
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
26172
+ var isRecording = this.isRecording();
26173
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
26174
+
26175
+ if (!isRecording && recordingTriggerEvents) {
26176
+ var trigger = recordingTriggerEvents[event_name];
26177
+ if (trigger && typeof trigger['percentage'] === 'number') {
26178
+ var newRate = trigger['percentage'];
26179
+ var propertyFilters = trigger['property_filters'];
26180
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
26181
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
26182
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
26183
+ .then(function(targeting) {
26184
+ try {
26185
+ var result = targeting['eventMatchesCriteria'](
26186
+ event_name,
26187
+ properties,
26188
+ {
26189
+ 'event_name': event_name,
26190
+ 'property_filters': propertyFilters
26191
+ }
26192
+ );
26193
+ if (result['matches']) {
26194
+ this.checkAndStartSessionRecording(false, newRate);
26195
+ }
26196
+ } catch (err) {
26197
+ console$1.critical('Could not parse recording event trigger properties logic:', err);
26198
+ }
26199
+ }.bind(this)).catch(function(err) {
26200
+ console$1.critical('Failed to load targeting library:', err);
26201
+ });
26202
+ } else {
26203
+ this.checkAndStartSessionRecording(false, newRate);
26204
+ }
26205
+ }
26206
+ }
26207
+ };
26208
+
26209
+ RecorderManager.prototype.stopSessionRecording = function() {
26210
+ if (this._recorder) {
26211
+ return this._recorder['stopRecording']();
26212
+ }
26213
+ return PromisePolyfill.resolve();
26214
+ };
26215
+
26216
+ RecorderManager.prototype.pauseSessionRecording = function() {
26217
+ if (this._recorder) {
26218
+ return this._recorder['pauseRecording']();
26219
+ }
26220
+ return PromisePolyfill.resolve();
26221
+ };
26222
+
26223
+ RecorderManager.prototype.resumeSessionRecording = function() {
26224
+ if (this._recorder) {
26225
+ return this._recorder['resumeRecording']();
26226
+ }
26227
+ return PromisePolyfill.resolve();
26228
+ };
26229
+
26230
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
26231
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
26232
+ };
26233
+
26234
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
26235
+ var props = {};
26236
+ var replay_id = this.getSessionReplayId();
26237
+ if (replay_id) {
26238
+ props['$mp_replay_id'] = replay_id;
26239
+ }
26240
+ return props;
26241
+ };
26242
+
26243
+ RecorderManager.prototype.getSessionReplayUrl = function() {
26244
+ var replay_url = null;
26245
+ var replay_id = this.getSessionReplayId();
26246
+ if (replay_id) {
26247
+ var query_params = _.HTTPBuildQuery({
26248
+ 'replay_id': replay_id,
26249
+ 'distinct_id': this.getDistinctId(),
26250
+ 'token': this.getMpConfig('token')
26251
+ });
26252
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26253
+ }
26254
+ return replay_url;
26255
+ };
26256
+
26257
+ RecorderManager.prototype.getSessionReplayId = function() {
26258
+ var replay_id = null;
26259
+ if (this._recorder) {
26260
+ replay_id = this._recorder['replayId'];
26261
+ }
26262
+ return replay_id || null;
26263
+ };
26264
+
26265
+ // "private" public method to reach into the recorder in test cases
26266
+ RecorderManager.prototype.getRecorder = function() {
26267
+ return this._recorder;
26268
+ };
26269
+
26270
+ safewrapClass(RecorderManager);
26271
+
24999
26272
  /* eslint camelcase: "off" */
25000
26273
 
25001
26274
 
@@ -26459,12 +27732,17 @@
26459
27732
  'record_collect_fonts': false,
26460
27733
  'record_console': true,
26461
27734
  'record_heatmap_data': false,
27735
+ 'recording_event_triggers': {},
26462
27736
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
26463
27737
  'record_mask_inputs': true,
26464
27738
  'record_max_ms': MAX_RECORDING_MS,
26465
27739
  'record_min_ms': 0,
27740
+ 'record_network': false,
27741
+ 'record_network_options': {},
26466
27742
  'record_sessions_percent': 0,
26467
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
27743
+ 'recorder_src': null,
27744
+ 'targeting_src': null,
27745
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
26468
27746
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
26469
27747
  };
26470
27748
 
@@ -26618,6 +27896,19 @@
26618
27896
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
26619
27897
  }));
26620
27898
 
27899
+ this.recorderManager = new RecorderManager({
27900
+ mixpanelInstance: this,
27901
+ getConfigFunc: _.bind(this.get_config, this),
27902
+ setConfigFunc: _.bind(this.set_config, this),
27903
+ getTabIdFunc: _.bind(this.get_tab_id, this),
27904
+ reportErrorFunc: _.bind(this.report_error, this),
27905
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
27906
+ recorderSrc: this.get_config('recorder_src'),
27907
+ targetingSrc: this.get_config('targeting_src'),
27908
+ libBasePath: this.get_config('lib_base_path'),
27909
+ loadExtraBundle: load_extra_bundle
27910
+ });
27911
+
26621
27912
  this['_jsc'] = NOOP_FUNC;
26622
27913
 
26623
27914
  this.__dom_loaded_queue = [];
@@ -26694,7 +27985,9 @@
26694
27985
  getConfigFunc: _.bind(this.get_config, this),
26695
27986
  setConfigFunc: _.bind(this.set_config, this),
26696
27987
  getPropertyFunc: _.bind(this.get_property, this),
26697
- trackingFunc: _.bind(this.track, this)
27988
+ trackingFunc: _.bind(this.track, this),
27989
+ loadExtraBundle: load_extra_bundle,
27990
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
26698
27991
  });
26699
27992
  this.flags.init();
26700
27993
  this['flags'] = this.flags;
@@ -26707,11 +28000,11 @@
26707
28000
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
26708
28001
  var mode = this.get_config('remote_settings_mode');
26709
28002
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
26710
- this._fetch_remote_settings(mode).then(_.bind(function() {
26711
- this._check_and_start_session_recording();
28003
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
28004
+ return this._check_and_start_session_recording();
26712
28005
  }, this));
26713
28006
  } else {
26714
- this._check_and_start_session_recording();
28007
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
26715
28008
  }
26716
28009
  };
26717
28010
 
@@ -26755,132 +28048,50 @@
26755
28048
  return this.tab_id || null;
26756
28049
  };
26757
28050
 
26758
- MixpanelLib.prototype._should_load_recorder = function () {
26759
- if (this.get_config('disable_persistence')) {
26760
- console$1.log('Load recorder check skipped due to disable_persistence config');
26761
- return Promise.resolve(false);
26762
- }
26763
-
26764
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26765
- var tab_id = this.get_tab_id();
26766
- return recording_registry_idb.init()
26767
- .then(function () {
26768
- return recording_registry_idb.getAll();
26769
- })
26770
- .then(function (recordings) {
26771
- for (var i = 0; i < recordings.length; i++) {
26772
- // if there are expired recordings in the registry, we should load the recorder to flush them
26773
- // if there's a recording for this tab id, we should load the recorder to continue the recording
26774
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26775
- return true;
26776
- }
26777
- }
26778
- return false;
26779
- })
26780
- .catch(_.bind(function (err) {
26781
- this.report_error('Error checking recording registry', err);
26782
- }, this));
26783
- };
26784
-
26785
28051
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
26786
- if (!win['MutationObserver']) {
26787
- console$1.critical('Browser does not support MutationObserver; skipping session recording');
26788
- return;
26789
- }
26790
-
26791
- var loadRecorder = _.bind(function(startNewIfInactive) {
26792
- var handleLoadedRecorder = _.bind(function() {
26793
- this._recorder = this._recorder || new win['__mp_recorder'](this);
26794
- this._recorder['resumeRecording'](startNewIfInactive);
26795
- }, this);
26796
-
26797
- if (_.isUndefined(win['__mp_recorder'])) {
26798
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
26799
- } else {
26800
- handleLoadedRecorder();
26801
- }
26802
- }, this);
26803
-
26804
- /**
26805
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26806
- * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
26807
- */
26808
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
26809
- if (force_start || is_sampled) {
26810
- loadRecorder(true);
26811
- } else {
26812
- this._should_load_recorder()
26813
- .then(function (shouldLoad) {
26814
- if (shouldLoad) {
26815
- loadRecorder(false);
26816
- }
26817
- });
26818
- }
28052
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
26819
28053
  });
26820
28054
 
28055
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
28056
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
28057
+ };
28058
+
26821
28059
  MixpanelLib.prototype.start_session_recording = function () {
26822
- this._check_and_start_session_recording(true);
28060
+ return this._check_and_start_session_recording(true);
26823
28061
  };
26824
28062
 
26825
28063
  MixpanelLib.prototype.stop_session_recording = function () {
26826
- if (this._recorder) {
26827
- return this._recorder['stopRecording']();
26828
- }
26829
- return Promise.resolve();
28064
+ return this.recorderManager.stopSessionRecording();
26830
28065
  };
26831
28066
 
26832
28067
  MixpanelLib.prototype.pause_session_recording = function () {
26833
- if (this._recorder) {
26834
- return this._recorder['pauseRecording']();
26835
- }
26836
- return Promise.resolve();
28068
+ return this.recorderManager.pauseSessionRecording();
26837
28069
  };
26838
28070
 
26839
28071
  MixpanelLib.prototype.resume_session_recording = function () {
26840
- if (this._recorder) {
26841
- return this._recorder['resumeRecording']();
26842
- }
26843
- return Promise.resolve();
28072
+ return this.recorderManager.resumeSessionRecording();
26844
28073
  };
26845
28074
 
26846
28075
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
26847
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
28076
+ return this.recorderManager.isRecordingHeatmapData();
26848
28077
  };
26849
28078
 
26850
28079
  MixpanelLib.prototype.get_session_recording_properties = function () {
26851
- var props = {};
26852
- var replay_id = this._get_session_replay_id();
26853
- if (replay_id) {
26854
- props['$mp_replay_id'] = replay_id;
26855
- }
26856
- return props;
28080
+ return this.recorderManager.getSessionRecordingProperties();
26857
28081
  };
26858
28082
 
26859
28083
  MixpanelLib.prototype.get_session_replay_url = function () {
26860
- var replay_url = null;
26861
- var replay_id = this._get_session_replay_id();
26862
- if (replay_id) {
26863
- var query_params = _.HTTPBuildQuery({
26864
- 'replay_id': replay_id,
26865
- 'distinct_id': this.get_distinct_id(),
26866
- 'token': this.get_config('token')
26867
- });
26868
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26869
- }
26870
- return replay_url;
26871
- };
26872
-
26873
- MixpanelLib.prototype._get_session_replay_id = function () {
26874
- var replay_id = null;
26875
- if (this._recorder) {
26876
- replay_id = this._recorder['replayId'];
26877
- }
26878
- return replay_id || null;
28084
+ return this.recorderManager.getSessionReplayUrl();
26879
28085
  };
26880
28086
 
26881
28087
  // "private" public method to reach into the recorder in test cases
26882
28088
  MixpanelLib.prototype.__get_recorder = function () {
26883
- return this._recorder;
28089
+ return this.recorderManager.getRecorder();
28090
+ };
28091
+
28092
+ // "private" public method to get session recording init promise in test cases
28093
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
28094
+ return this.__session_recording_init_promise;
26884
28095
  };
26885
28096
 
26886
28097
  // Private methods
@@ -27138,6 +28349,7 @@
27138
28349
  };
27139
28350
 
27140
28351
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
28352
+ var self = this;
27141
28353
  var disableRecordingIfStrict = function() {
27142
28354
  if (mode === 'strict') {
27143
28355
  self.set_config({'record_sessions_percent': 0});
@@ -27158,7 +28370,6 @@
27158
28370
  };
27159
28371
  var query_string = _.HTTPBuildQuery(request_params);
27160
28372
  var full_url = settings_endpoint + '?' + query_string;
27161
- var self = this;
27162
28373
 
27163
28374
  var abortController = new AbortController();
27164
28375
  var timeout_id = setTimeout(function() {
@@ -27350,6 +28561,34 @@
27350
28561
  this._execute_array([item]);
27351
28562
  };
27352
28563
 
28564
+ /**
28565
+ * Enables events on the Mixpanel object. If passed no arguments,
28566
+ * this function enable tracking of all events. If passed an
28567
+ * array of event names, those events will be enabled, but other
28568
+ * existing disabled events will continue to be not tracked.
28569
+ *
28570
+ * @param {Array} [events] An array of event names to enable
28571
+ */
28572
+ MixpanelLib.prototype.enable = function(events) {
28573
+ var keys, new_disabled_events, i, j;
28574
+
28575
+ if (typeof(events) === 'undefined') {
28576
+ this._flags.disable_all_events = false;
28577
+ } else {
28578
+ keys = {};
28579
+ new_disabled_events = [];
28580
+ for (i = 0; i < events.length; i++) {
28581
+ keys[events[i]] = true;
28582
+ }
28583
+ for (j = 0; j < this.__disabled_events.length; j++) {
28584
+ if (!keys[this.__disabled_events[j]]) {
28585
+ new_disabled_events.push(this.__disabled_events[j]);
28586
+ }
28587
+ }
28588
+ this.__disabled_events = new_disabled_events;
28589
+ }
28590
+ };
28591
+
27353
28592
  /**
27354
28593
  * Disable events on the Mixpanel object. If passed no arguments,
27355
28594
  * this function disables tracking of any event. If passed an
@@ -27523,6 +28762,8 @@
27523
28762
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
27524
28763
  }
27525
28764
 
28765
+ this._start_recording_on_event(event_name, properties);
28766
+
27526
28767
  var data = {
27527
28768
  'event': event_name,
27528
28769
  'properties': properties
@@ -27536,6 +28777,11 @@
27536
28777
  send_request_options: options
27537
28778
  }, callback);
27538
28779
 
28780
+ // Check for first-time event matches
28781
+ if (this.flags && this.flags.checkFirstTimeEvents) {
28782
+ this.flags.checkFirstTimeEvents(event_name, properties);
28783
+ }
28784
+
27539
28785
  return ret;
27540
28786
  });
27541
28787
 
@@ -28726,6 +29972,7 @@
28726
29972
  // MixpanelLib Exports
28727
29973
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
28728
29974
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
29975
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
28729
29976
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
28730
29977
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
28731
29978
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -28769,6 +30016,7 @@
28769
30016
 
28770
30017
  // Exports intended only for testing
28771
30018
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
30019
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
28772
30020
 
28773
30021
  // MixpanelPersistence Exports
28774
30022
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;