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
@@ -29,6 +29,19 @@
29
29
  win = window;
30
30
  }
31
31
 
32
+ var Config = {
33
+ DEBUG: false,
34
+ LIB_VERSION: '2.76.0'
35
+ };
36
+
37
+ // Window global names for async modules
38
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
39
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
40
+
41
+ // Constants that are injected at build-time for the names of async modules.
42
+ var RECORDER_FILENAME = '__MP_RECORDER_FILENAME__';
43
+ var TARGETING_FILENAME = '__MP_TARGETING_FILENAME__';
44
+
32
45
  function _array_like_to_array(arr, len) {
33
46
  if (len == null || len > arr.length) len = arr.length;
34
47
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -643,14 +656,16 @@
643
656
  return this.nodeMetaMap.get(n2) || null;
644
657
  };
645
658
  // removes the node from idNodeMap
646
- // doesn't remove the node from nodeMetaMap
647
- _proto.removeNodeFromMap = function removeNodeFromMap(n2) {
659
+ // if permanent is true, also removes from nodeMetaMap
660
+ _proto.removeNodeFromMap = function removeNodeFromMap(n2, permanent) {
648
661
  var _this = this;
662
+ if (permanent === void 0) permanent = false;
649
663
  var id = this.getId(n2);
650
664
  this.idNodeMap.delete(id);
665
+ if (permanent) this.nodeMetaMap.delete(n2);
651
666
  if (n2.childNodes) {
652
667
  n2.childNodes.forEach(function(childNode) {
653
- return _this.removeNodeFromMap(childNode);
668
+ return _this.removeNodeFromMap(childNode, permanent);
654
669
  });
655
670
  }
656
671
  };
@@ -10392,6 +10407,15 @@
10392
10407
  _proto.generateId = function generateId() {
10393
10408
  return this.id++;
10394
10409
  };
10410
+ _proto.remove = function remove(stylesheet) {
10411
+ var id = this.styleIDMap.get(stylesheet);
10412
+ if (id !== void 0) {
10413
+ this.styleIDMap.delete(stylesheet);
10414
+ this.idStyleMap.delete(id);
10415
+ return true;
10416
+ }
10417
+ return false;
10418
+ };
10395
10419
  return StyleSheetMirror;
10396
10420
  }();
10397
10421
  function getShadowHost(n2) {
@@ -10714,7 +10738,15 @@
10714
10738
  }
10715
10739
  };
10716
10740
  while(_this.mapRemoves.length){
10717
- _this.mirror.removeNodeFromMap(_this.mapRemoves.shift());
10741
+ var removedNode = _this.mapRemoves.shift();
10742
+ if (removedNode.nodeName === "IFRAME") {
10743
+ try {
10744
+ _this.iframeManager.removeIframe(removedNode);
10745
+ } catch (e2) {}
10746
+ } else {
10747
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10748
+ }
10749
+ _this.mirror.removeNodeFromMap(removedNode);
10718
10750
  }
10719
10751
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
10720
10752
  var n2 = _step.value;
@@ -11088,6 +11120,9 @@
11088
11120
  this.shadowDomManager.reset();
11089
11121
  this.canvasManager.reset();
11090
11122
  };
11123
+ _proto.getDoc = function getDoc() {
11124
+ return this.doc;
11125
+ };
11091
11126
  return MutationBuffer;
11092
11127
  }();
11093
11128
  function deepDelete(addsSet, n2) {
@@ -11188,6 +11223,14 @@
11188
11223
  });
11189
11224
  return observer;
11190
11225
  }
11226
+ function removeMutationBufferForDoc(doc) {
11227
+ for(var i2 = mutationBuffers.length - 1; i2 >= 0; i2--){
11228
+ var buffer = mutationBuffers[i2];
11229
+ if (buffer.getDoc() === doc) {
11230
+ mutationBuffers.splice(i2, 1);
11231
+ }
11232
+ }
11233
+ }
11191
11234
  function initMoveObserver(param) {
11192
11235
  var mousemoveCb = param.mousemoveCb, sampling = param.sampling, doc = param.doc, mirror2 = param.mirror;
11193
11236
  if (sampling.mousemove === false) {
@@ -12203,6 +12246,8 @@
12203
12246
  __publicField$1(this, "crossOriginIframeMirror", new CrossOriginIframeMirror(genId));
12204
12247
  __publicField$1(this, "crossOriginIframeStyleMirror");
12205
12248
  __publicField$1(this, "crossOriginIframeRootIdMap", /* @__PURE__ */ new WeakMap());
12249
+ __publicField$1(this, "iframeContentDocumentMap", /* @__PURE__ */ new WeakMap());
12250
+ __publicField$1(this, "iframeObserverCleanupMap", /* @__PURE__ */ new WeakMap());
12206
12251
  __publicField$1(this, "mirror");
12207
12252
  __publicField$1(this, "mutationCb");
12208
12253
  __publicField$1(this, "wrappedEmit");
@@ -12224,6 +12269,31 @@
12224
12269
  this.iframes.set(iframeEl, true);
12225
12270
  if (iframeEl.contentWindow) this.crossOriginIframeMap.set(iframeEl.contentWindow, iframeEl);
12226
12271
  };
12272
+ _proto.getIframeContentDocument = function getIframeContentDocument(iframeEl) {
12273
+ return this.iframeContentDocumentMap.get(iframeEl);
12274
+ };
12275
+ _proto.setObserverCleanup = function setObserverCleanup(iframeEl, cleanup) {
12276
+ this.iframeObserverCleanupMap.set(iframeEl, cleanup);
12277
+ };
12278
+ _proto.getObserverCleanup = function getObserverCleanup(iframeEl) {
12279
+ return this.iframeObserverCleanupMap.get(iframeEl);
12280
+ };
12281
+ _proto.removeIframe = function removeIframe(iframeEl) {
12282
+ var storedDoc = this.iframeContentDocumentMap.get(iframeEl);
12283
+ if (storedDoc) {
12284
+ this.stylesheetManager.cleanupStylesheetsForRemovedNode(storedDoc);
12285
+ this.mirror.removeNodeFromMap(storedDoc, true);
12286
+ }
12287
+ this.iframes.delete(iframeEl);
12288
+ this.iframeContentDocumentMap.delete(iframeEl);
12289
+ var observerCleanup = this.iframeObserverCleanupMap.get(iframeEl);
12290
+ if (observerCleanup) {
12291
+ try {
12292
+ observerCleanup();
12293
+ } catch (e2) {}
12294
+ this.iframeObserverCleanupMap.delete(iframeEl);
12295
+ }
12296
+ };
12227
12297
  _proto.addLoadListener = function addLoadListener(cb) {
12228
12298
  this.loadListener = cb;
12229
12299
  };
@@ -12242,6 +12312,9 @@
12242
12312
  attributes: [],
12243
12313
  isAttachIframe: true
12244
12314
  });
12315
+ if (iframeEl.contentDocument) {
12316
+ this.iframeContentDocumentMap.set(iframeEl, iframeEl.contentDocument);
12317
+ }
12245
12318
  if (this.recordCrossOriginIframes) (_a2 = iframeEl.contentWindow) == null ? void 0 : _a2.addEventListener("message", this.handleMessage.bind(this));
12246
12319
  (_b = this.loadListener) == null ? void 0 : _b.call(this, iframeEl);
12247
12320
  if (iframeEl.contentDocument && iframeEl.contentDocument.adoptedStyleSheets && iframeEl.contentDocument.adoptedStyleSheets.length > 0) this.stylesheetManager.adoptStyleSheets(iframeEl.contentDocument.adoptedStyleSheets, this.mirror.getId(iframeEl.contentDocument));
@@ -13154,6 +13227,41 @@
13154
13227
  this.styleMirror.reset();
13155
13228
  this.trackedLinkElements = /* @__PURE__ */ new WeakSet();
13156
13229
  };
13230
+ /**
13231
+ * Cleans up stylesheets associated with a removed node.
13232
+ *
13233
+ * @param removedNode - The node that was removed from the DOM.
13234
+ */ _proto.cleanupStylesheetsForRemovedNode = function cleanupStylesheetsForRemovedNode(removedNode) {
13235
+ var _this = this;
13236
+ try {
13237
+ if (removedNode.nodeType === Node.DOCUMENT_NODE) {
13238
+ var doc = removedNode;
13239
+ if (doc.adoptedStyleSheets) {
13240
+ for(var _iterator = _create_for_of_iterator_helper_loose(doc.adoptedStyleSheets), _step; !(_step = _iterator()).done;){
13241
+ var sheet = _step.value;
13242
+ this.styleMirror.remove(sheet);
13243
+ }
13244
+ }
13245
+ }
13246
+ if (removedNode.nodeName === "STYLE") {
13247
+ var styleEl = removedNode;
13248
+ if (styleEl.sheet) {
13249
+ this.styleMirror.remove(styleEl.sheet);
13250
+ }
13251
+ }
13252
+ if (removedNode.nodeName === "LINK" && removedNode.rel === "stylesheet") {
13253
+ var linkEl = removedNode;
13254
+ if (linkEl.sheet) {
13255
+ this.styleMirror.remove(linkEl.sheet);
13256
+ }
13257
+ }
13258
+ if (removedNode.childNodes) {
13259
+ removedNode.childNodes.forEach(function(child) {
13260
+ _this.cleanupStylesheetsForRemovedNode(child);
13261
+ });
13262
+ }
13263
+ } catch (e2) {}
13264
+ };
13157
13265
  // TODO: take snapshot on stylesheet reload by applying event listener
13158
13266
  _proto.trackStylesheetInLinkElement = function trackStylesheetInLinkElement(_linkEl) {};
13159
13267
  return StylesheetManager;
@@ -13608,7 +13716,23 @@
13608
13716
  };
13609
13717
  iframeManager.addLoadListener(function(iframeEl) {
13610
13718
  try {
13611
- handlers.push(observe(iframeEl.contentDocument));
13719
+ var iframeDoc = iframeEl.contentDocument;
13720
+ var iframeHandler = observe(iframeDoc);
13721
+ handlers.push(iframeHandler);
13722
+ var existingCleanup = iframeManager.getObserverCleanup(iframeEl);
13723
+ iframeManager.setObserverCleanup(iframeEl, function() {
13724
+ if (existingCleanup) {
13725
+ try {
13726
+ existingCleanup();
13727
+ } catch (e2) {}
13728
+ }
13729
+ try {
13730
+ iframeHandler();
13731
+ var idx = handlers.indexOf(iframeHandler);
13732
+ if (idx !== -1) handlers.splice(idx, 1);
13733
+ removeMutationBufferForDoc(iframeDoc);
13734
+ } catch (e2) {}
13735
+ });
13612
13736
  } catch (error) {
13613
13737
  console.warn(error);
13614
13738
  }
@@ -13865,7 +13989,7 @@
13865
13989
  }
13866
13990
  return classMatchesRegex(index.parentNode(node2), regex);
13867
13991
  }
13868
- function getDefaultExportFromCjs(x2) {
13992
+ function getDefaultExportFromCjs$3(x2) {
13869
13993
  return x2 && x2.__esModule && Object.prototype.hasOwnProperty.call(x2, "default") ? x2["default"] : x2;
13870
13994
  }
13871
13995
  function getAugmentedNamespace(n) {
@@ -17980,7 +18104,7 @@
17980
18104
  LazyResult2.registerPostcss(postcss);
17981
18105
  var postcss_1 = postcss;
17982
18106
  postcss.default = postcss;
17983
- var postcss$1 = /* @__PURE__ */ getDefaultExportFromCjs(postcss_1);
18107
+ var postcss$1 = /* @__PURE__ */ getDefaultExportFromCjs$3(postcss_1);
17984
18108
  postcss$1.stringify;
17985
18109
  postcss$1.fromJSON;
17986
18110
  postcss$1.plugin;
@@ -18017,7 +18141,7 @@
18017
18141
  var __publicField = function(obj, key, value) {
18018
18142
  return __defNormalProp(obj, (typeof key === "undefined" ? "undefined" : _type_of(key)) !== "symbol" ? key + "" : key, value);
18019
18143
  };
18020
- function patch(source, name, replacement) {
18144
+ function patch$3(source, name, replacement) {
18021
18145
  try {
18022
18146
  if (!(name in source)) {
18023
18147
  return function() {};
@@ -18434,7 +18558,7 @@
18434
18558
  if (!_logger[level]) {
18435
18559
  return function() {};
18436
18560
  }
18437
- return patch(_logger, level, function(original) {
18561
+ return patch$3(_logger, level, function(original) {
18438
18562
  var _this1 = _this;
18439
18563
  return function() {
18440
18564
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
@@ -18855,11 +18979,6 @@
18855
18979
  PromisePolyfill = NpoPromise;
18856
18980
  }
18857
18981
 
18858
- var Config = {
18859
- DEBUG: false,
18860
- LIB_VERSION: '2.74.0'
18861
- };
18862
-
18863
18982
  /* eslint camelcase: "off", eqeqeq: "off" */
18864
18983
 
18865
18984
  // Maximum allowed session recording length
@@ -19063,15 +19182,8 @@
19063
19182
  return toString.call(obj) === '[object Array]';
19064
19183
  };
19065
19184
 
19066
- // from a comment on http://dbj.org/dbj/?p=286
19067
- // fails on only one very rare and deliberate custom object:
19068
- // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
19069
19185
  _.isFunction = function(f) {
19070
- try {
19071
- return /^\s*\bfunction\b/.test(f);
19072
- } catch (x) {
19073
- return false;
19074
- }
19186
+ return typeof f === 'function';
19075
19187
  };
19076
19188
 
19077
19189
  _.isArguments = function(obj) {
@@ -20598,6 +20710,17 @@
20598
20710
 
20599
20711
  var NOOP_FUNC = function () {};
20600
20712
 
20713
+ var urlMatchesRegexList = function (url, regexList) {
20714
+ var matches = false;
20715
+ for (var i = 0; i < regexList.length; i++) {
20716
+ if (url.match(regexList[i])) {
20717
+ matches = true;
20718
+ break;
20719
+ }
20720
+ }
20721
+ return matches;
20722
+ };
20723
+
20601
20724
  var JSONStringify = null, JSONParse = null;
20602
20725
  if (typeof JSON !== 'undefined') {
20603
20726
  JSONStringify = JSON.stringify;
@@ -21069,7 +21192,7 @@
21069
21192
  };
21070
21193
  }
21071
21194
 
21072
- var logger$6 = console_with_prefix('lock');
21195
+ var logger$7 = console_with_prefix('lock');
21073
21196
 
21074
21197
  /**
21075
21198
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -21121,7 +21244,7 @@
21121
21244
 
21122
21245
  var delay = function(cb) {
21123
21246
  if (new Date().getTime() - startTime > timeoutMS) {
21124
- logger$6.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21247
+ logger$7.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21125
21248
  storage.removeItem(keyZ);
21126
21249
  storage.removeItem(keyY);
21127
21250
  loop();
@@ -21268,7 +21391,7 @@
21268
21391
  }, this));
21269
21392
  };
21270
21393
 
21271
- var logger$5 = console_with_prefix('batch');
21394
+ var logger$6 = console_with_prefix('batch');
21272
21395
 
21273
21396
  /**
21274
21397
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -21297,7 +21420,7 @@
21297
21420
  timeoutMS: options.sharedLockTimeoutMS,
21298
21421
  });
21299
21422
  }
21300
- this.reportError = options.errorReporter || _.bind(logger$5.error, logger$5);
21423
+ this.reportError = options.errorReporter || _.bind(logger$6.error, logger$6);
21301
21424
 
21302
21425
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
21303
21426
 
@@ -21630,7 +21753,7 @@
21630
21753
  // maximum interval between request retries after exponential backoff
21631
21754
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
21632
21755
 
21633
- var logger$4 = console_with_prefix('batch');
21756
+ var logger$5 = console_with_prefix('batch');
21634
21757
 
21635
21758
  /**
21636
21759
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -21758,7 +21881,7 @@
21758
21881
  */
21759
21882
  RequestBatcher.prototype.flush = function(options) {
21760
21883
  if (this.requestInProgress) {
21761
- logger$4.log('Flush: Request already in progress');
21884
+ logger$5.log('Flush: Request already in progress');
21762
21885
  return PromisePolyfill.resolve();
21763
21886
  }
21764
21887
 
@@ -21935,7 +22058,7 @@
21935
22058
  if (options.unloading) {
21936
22059
  requestOptions.transport = 'sendBeacon';
21937
22060
  }
21938
- logger$4.log('MIXPANEL REQUEST:', dataForRequest);
22061
+ logger$5.log('MIXPANEL REQUEST:', dataForRequest);
21939
22062
  return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
21940
22063
  }, this))
21941
22064
  .catch(_.bind(function(err) {
@@ -21948,7 +22071,7 @@
21948
22071
  * Log error to global logger and optional user-defined logger.
21949
22072
  */
21950
22073
  RequestBatcher.prototype.reportError = function(msg, err) {
21951
- logger$4.error.apply(logger$4.error, arguments);
22074
+ logger$5.error.apply(logger$5.error, arguments);
21952
22075
  if (this.errorReporter) {
21953
22076
  try {
21954
22077
  if (!(err instanceof Error)) {
@@ -21956,7 +22079,7 @@
21956
22079
  }
21957
22080
  this.errorReporter(msg, err);
21958
22081
  } catch(err) {
21959
- logger$4.error(err);
22082
+ logger$5.error(err);
21960
22083
  }
21961
22084
  }
21962
22085
  };
@@ -22078,7 +22201,7 @@
22078
22201
 
22079
22202
  var MAX_DEPTH = 5;
22080
22203
 
22081
- var logger$3 = console_with_prefix('autocapture');
22204
+ var logger$4 = console_with_prefix('autocapture');
22082
22205
 
22083
22206
 
22084
22207
  function getClasses(el) {
@@ -22342,7 +22465,7 @@
22342
22465
  return false;
22343
22466
  }
22344
22467
  } catch (err) {
22345
- logger$3.critical('Error while checking element in allowElementCallback', err);
22468
+ logger$4.critical('Error while checking element in allowElementCallback', err);
22346
22469
  return false;
22347
22470
  }
22348
22471
  }
@@ -22359,7 +22482,7 @@
22359
22482
  return true;
22360
22483
  }
22361
22484
  } catch (err) {
22362
- logger$3.critical('Error while checking selector: ' + sel, err);
22485
+ logger$4.critical('Error while checking selector: ' + sel, err);
22363
22486
  }
22364
22487
  }
22365
22488
  return false;
@@ -22374,7 +22497,7 @@
22374
22497
  return true;
22375
22498
  }
22376
22499
  } catch (err) {
22377
- logger$3.critical('Error while checking element in blockElementCallback', err);
22500
+ logger$4.critical('Error while checking element in blockElementCallback', err);
22378
22501
  return true;
22379
22502
  }
22380
22503
  }
@@ -22388,7 +22511,7 @@
22388
22511
  return true;
22389
22512
  }
22390
22513
  } catch (err) {
22391
- logger$3.critical('Error while checking selector: ' + sel, err);
22514
+ logger$4.critical('Error while checking selector: ' + sel, err);
22392
22515
  }
22393
22516
  }
22394
22517
  }
@@ -22936,173 +23059,822 @@
22936
23059
  }
22937
23060
 
22938
23061
  /**
22939
- * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23062
+ * This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
23063
+ * the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
23064
+ *
23065
+ * This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
23066
+ * header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
23067
+ *
22940
23068
  */
22941
23069
 
23070
+ var logger$3 = console_with_prefix('network-plugin');
22942
23071
 
22943
- var logger$2 = console_with_prefix('recorder');
22944
- var CompressionStream = win['CompressionStream'];
22945
-
22946
- var RECORDER_BATCHER_LIB_CONFIG = {
22947
- 'batch_size': 1000,
22948
- 'batch_flush_interval_ms': 10 * 1000,
22949
- 'batch_request_timeout_ms': 90 * 1000,
22950
- 'batch_autostart': true
22951
- };
23072
+ /**
23073
+ * Get the time origin for converting performance timestamps to absolute timestamps.
23074
+ * Uses Date.now() - performance.now() instead of performance.timeOrigin because
23075
+ * browsers can report timeOrigin values that are skewed from actual time, and some
23076
+ * browsers (notably older Safari versions) don't implement timeOrigin at all.
23077
+ * See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
23078
+ * @param {Window} win
23079
+ * @returns {number}
23080
+ */
23081
+ function getTimeOrigin(win) {
23082
+ return Math.round(Date.now() - win.performance.now());
23083
+ }
22952
23084
 
22953
- var ACTIVE_SOURCES = new Set([
22954
- IncrementalSource.MouseMove,
22955
- IncrementalSource.MouseInteraction,
22956
- IncrementalSource.Scroll,
22957
- IncrementalSource.ViewportResize,
22958
- IncrementalSource.Input,
22959
- IncrementalSource.TouchMove,
22960
- IncrementalSource.MediaInteraction,
22961
- IncrementalSource.Drag,
22962
- IncrementalSource.Selection,
22963
- ]);
23085
+ /**
23086
+ * @typedef {import('../index.d.ts').InitiatorType} InitiatorType
23087
+ * @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
23088
+ * @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
23089
+ * @typedef {import('../index.d.ts').NetworkData} NetworkData
23090
+ */
22964
23091
 
22965
- function isUserEvent(ev) {
22966
- return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
22967
- }
23092
+ /**
23093
+ * @typedef {Record<string, string>} Headers
23094
+ */
22968
23095
 
22969
23096
  /**
22970
- * @typedef {Object} SerializedRecording
22971
- * @property {number} idleExpires
22972
- * @property {number} maxExpires
22973
- * @property {number} replayStartTime
22974
- * @property {number} lastEventTimestamp
22975
- * @property {number} seqNo
22976
- * @property {string} batchStartUrl
22977
- * @property {string} replayId
22978
- * @property {string} tabId
22979
- * @property {string} replayStartUrl
23097
+ * @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
22980
23098
  */
22981
23099
 
22982
23100
  /**
22983
- * @typedef {Object} SessionRecordingOptions
22984
- * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
22985
- * @property {String} [options.replayId] - unique uuid for a single replay
22986
- * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
22987
- * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
22988
- * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
22989
- * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
22990
- * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
22991
- * optional properties for deserialization:
22992
- * @property {number} idleExpires
22993
- * @property {number} maxExpires
22994
- * @property {number} replayStartTime
22995
- * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
22996
- * @property {number} seqNo
22997
- * @property {string} batchStartUrl
22998
- * @property {string} replayStartUrl
23101
+ * @callback networkCallback
23102
+ * @param {NetworkData} data
23103
+ * @returns {void}
22999
23104
  */
23000
23105
 
23001
23106
  /**
23002
- * @typedef {Object} UserIdInfo
23003
- * @property {string} distinct_id
23004
- * @property {string} user_id
23005
- * @property {string} device_id
23107
+ * @callback listenerHandler
23108
+ * @returns {void}
23006
23109
  */
23007
23110
 
23111
+ /**
23112
+ * @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
23113
+ */
23008
23114
 
23009
23115
  /**
23010
- * This class encapsulates a single session recording and its lifecycle.
23011
- * @param {SessionRecordingOptions} options
23116
+ * @typedef {Object} RecordPlugin
23117
+ * @property {string} name
23118
+ * @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
23119
+ * @property {NetworkRecordOptions} [options]
23012
23120
  */
23013
- var SessionRecording = function(options) {
23014
- this._mixpanel = options.mixpanelInstance;
23015
- this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23016
- this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23017
- this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23018
- this._rrwebRecord = options.rrwebRecord || null;
23019
23121
 
23020
- // internal rrweb stopRecording function
23021
- this._stopRecording = null;
23022
- this.replayId = options.replayId;
23122
+ /** @type {Required<NetworkRecordOptions>} */
23123
+ var defaultNetworkOptions = {
23124
+ initiatorTypes: [
23125
+ 'audio',
23126
+ 'beacon',
23127
+ 'body',
23128
+ 'css',
23129
+ 'early-hint',
23130
+ 'embed',
23131
+ 'fetch',
23132
+ 'frame',
23133
+ 'iframe',
23134
+ 'icon',
23135
+ 'image',
23136
+ 'img',
23137
+ 'input',
23138
+ 'link',
23139
+ 'navigation',
23140
+ 'object',
23141
+ 'ping',
23142
+ 'script',
23143
+ 'track',
23144
+ 'video',
23145
+ 'xmlhttprequest',
23146
+ ],
23147
+ ignoreRequestFn: function() { return false; },
23148
+ recordHeaders: {
23149
+ request: [],
23150
+ response: [],
23151
+ },
23152
+ recordBodyUrls: {
23153
+ request: [],
23154
+ response: [],
23155
+ },
23156
+ recordInitialRequests: false,
23157
+ };
23023
23158
 
23024
- this.batchStartUrl = options.batchStartUrl || null;
23025
- this.replayStartUrl = options.replayStartUrl || null;
23026
- this.idleExpires = options.idleExpires || null;
23027
- this.maxExpires = options.maxExpires || null;
23028
- this.replayStartTime = options.replayStartTime || null;
23029
- this.lastEventTimestamp = options.lastEventTimestamp || null;
23030
- this.seqNo = options.seqNo || 0;
23159
+ /**
23160
+ * @param {PerformanceEntry} entry
23161
+ * @returns {entry is PerformanceNavigationTiming}
23162
+ */
23163
+ function isNavigationTiming(entry) {
23164
+ return entry.entryType === 'navigation';
23165
+ }
23031
23166
 
23032
- this.idleTimeoutId = null;
23033
- this.maxTimeoutId = null;
23167
+ /**
23168
+ * @param {PerformanceEntry} entry
23169
+ * @returns {entry is PerformanceResourceTiming}
23170
+ */
23171
+ function isResourceTiming (entry) {
23172
+ return entry.entryType === 'resource';
23173
+ }
23034
23174
 
23035
- this.recordMaxMs = MAX_RECORDING_MS;
23036
- this.recordMinMs = 0;
23175
+ function findLast(array, predicate) {
23176
+ var length = array.length;
23177
+ for (var i = length - 1; i >= 0; i -= 1) {
23178
+ if (predicate(array[i])) {
23179
+ return array[i];
23180
+ }
23181
+ }
23182
+ }
23037
23183
 
23038
- // disable persistence if localStorage is not supported
23039
- // request-queue will automatically disable persistence if indexedDB fails to initialize
23040
- var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23184
+ /**
23185
+ * Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
23186
+ * Adapted from Sentry's `fill` utility:
23187
+ * https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
23188
+ *
23189
+ * @param {object} source - The object containing the method to patch
23190
+ * @param {string} name - The method name to patch
23191
+ * @param {function} replacementFactory - A function that receives the original method and returns the replacement
23192
+ * @returns {function} A function that restores the original method
23193
+ */
23194
+ function patch(source, name, replacementFactory) {
23195
+ if (!(name in source) || typeof source[name] !== 'function') {
23196
+ return function() {};
23197
+ }
23198
+ var original = source[name];
23199
+ var wrapped = replacementFactory(original);
23200
+ source[name] = wrapped;
23201
+ return function() {
23202
+ source[name] = original;
23203
+ };
23204
+ }
23041
23205
 
23042
- // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23043
- this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23044
- this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23045
- this.batcher = new RequestBatcher(this.batcherKey, {
23046
- errorReporter: this.reportError.bind(this),
23047
- flushOnlyOnInterval: true,
23048
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
23049
- sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23050
- queueStorage: this.queueStorage,
23051
- sharedLockStorage: options.sharedLockStorage,
23052
- usePersistence: usePersistence,
23053
- stopAllBatchingFunc: this.stopRecording.bind(this),
23054
23206
 
23055
- // increased throttle and shared lock timeout because recording events are very high frequency.
23056
- // this will minimize the amount of lock contention between enqueued events.
23057
- // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23058
- enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23059
- sharedLockTimeoutMS: 10 * 1000,
23060
- });
23061
- };
23207
+ /**
23208
+ * Maximum body size to record (1MB)
23209
+ */
23210
+ var MAX_BODY_SIZE = 1024 * 1024;
23062
23211
 
23063
23212
  /**
23064
- * @returns {UserIdInfo}
23213
+ * Truncate string if it exceeds max size
23214
+ * @param {string} str
23215
+ * @returns {string}
23065
23216
  */
23066
- SessionRecording.prototype.getUserIdInfo = function () {
23067
- if (this.finalFlushUserIdInfo) {
23068
- return this.finalFlushUserIdInfo;
23217
+ function truncateBody(str) {
23218
+ if (!str || typeof str !== 'string') {
23219
+ return str;
23220
+ }
23221
+ if (str.length > MAX_BODY_SIZE) {
23222
+ logger$3.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
23223
+ return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
23069
23224
  }
23225
+ return str;
23226
+ }
23070
23227
 
23071
- var userIdInfo = {
23072
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
23228
+ /**
23229
+ * @param {networkCallback} cb
23230
+ * @param {Window} win
23231
+ * @param {Required<NetworkRecordOptions>} options
23232
+ * @returns {listenerHandler}
23233
+ */
23234
+ function initPerformanceObserver(cb, win, options) {
23235
+ if (!win.PerformanceObserver) {
23236
+ logger$3.error('PerformanceObserver not supported');
23237
+ return function() {
23238
+ //
23239
+ };
23240
+ }
23241
+ if (options.recordInitialRequests) {
23242
+ var initialPerformanceEntries = win.performance
23243
+ .getEntries()
23244
+ .filter(function(entry) {
23245
+ return isNavigationTiming(entry) ||
23246
+ (isResourceTiming(entry) &&
23247
+ options.initiatorTypes.includes(entry.initiatorType));
23248
+ });
23249
+ cb({
23250
+ requests: initialPerformanceEntries.map(function(entry) {
23251
+ return {
23252
+ url: entry.name,
23253
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23254
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23255
+ startTime: Math.round(entry.startTime),
23256
+ endTime: Math.round(entry.responseEnd),
23257
+ timeOrigin: getTimeOrigin(win),
23258
+ };
23259
+ }),
23260
+ isInitial: true,
23261
+ });
23262
+ }
23263
+ var observer = new win.PerformanceObserver(function(entries) {
23264
+ var performanceEntries = entries
23265
+ .getEntries()
23266
+ .filter(function(entry) {
23267
+ return isResourceTiming(entry) &&
23268
+ options.initiatorTypes.includes(entry.initiatorType) &&
23269
+ entry.initiatorType !== 'xmlhttprequest' &&
23270
+ entry.initiatorType !== 'fetch';
23271
+ });
23272
+ cb({
23273
+ requests: performanceEntries.map(function(entry) {
23274
+ return {
23275
+ url: entry.name,
23276
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23277
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23278
+ startTime: Math.round(entry.startTime),
23279
+ endTime: Math.round(entry.responseEnd),
23280
+ timeOrigin: getTimeOrigin(win),
23281
+ };
23282
+ }),
23283
+ });
23284
+ });
23285
+ observer.observe({ entryTypes: ['navigation', 'resource'] });
23286
+ return function() {
23287
+ observer.disconnect();
23073
23288
  };
23289
+ }
23074
23290
 
23075
- // send ID management props if they exist
23076
- var deviceId = this._mixpanel.get_property('$device_id');
23077
- if (deviceId) {
23078
- userIdInfo['$device_id'] = deviceId;
23291
+ /**
23292
+ * Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
23293
+ * @param {'request' | 'response'} type
23294
+ * @param {NetworkRecordOptions['recordHeaders']} recordHeaders
23295
+ * @param {string} headerName
23296
+ * @returns {boolean}
23297
+ */
23298
+ function shouldRecordHeader(type, recordHeaders, headerName) {
23299
+ if (!recordHeaders[type] || recordHeaders[type].length === 0) {
23300
+ return false;
23079
23301
  }
23080
- var userId = this._mixpanel.get_property('$user_id');
23081
- if (userId) {
23082
- userIdInfo['$user_id'] = userId;
23302
+
23303
+ return recordHeaders[type].includes(headerName.toLowerCase());
23304
+ }
23305
+
23306
+ /**
23307
+ * Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
23308
+ * @param {'request' | 'response'} type
23309
+ * @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
23310
+ * @param {string} url
23311
+ * @returns {boolean}
23312
+ */
23313
+ function shouldRecordBody(type, recordBodyUrls, url) {
23314
+ if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
23315
+ return false;
23083
23316
  }
23084
- return userIdInfo;
23085
- };
23086
23317
 
23087
- SessionRecording.prototype.unloadPersistedData = function () {
23088
- this.batcher.stop();
23318
+ return urlMatchesRegexList(url, recordBodyUrls[type]);
23319
+ }
23089
23320
 
23090
- return this.queueStorage.init().catch(function () {
23091
- this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23092
- }.bind(this)).then(function () {
23093
- // if the recording is too short, just delete any stored events without flushing
23094
- if (this.getDurationMs() < this._getRecordMinMs()) {
23095
- return this.queueStorage.removeItem(this.batcherKey);
23321
+ function tryReadXHRBody(body) {
23322
+ if (body === null || body === undefined) {
23323
+ return null;
23324
+ }
23325
+
23326
+ var result;
23327
+ if (typeof body === 'string') {
23328
+ result = body;
23329
+ } else if (body instanceof Document) {
23330
+ result = body.textContent;
23331
+ } else if (body instanceof FormData) {
23332
+ result = _.HTTPBuildQuery(body);
23333
+ } else if (_.isObject(body)) {
23334
+ try {
23335
+ result = JSON.stringify(body);
23336
+ } catch (e) {
23337
+ return 'Failed to stringify response object';
23096
23338
  }
23339
+ } else {
23340
+ return 'Cannot read body of type ' + typeof body;
23341
+ }
23097
23342
 
23098
- return this.batcher.flush()
23099
- .then(function () {
23100
- return this.queueStorage.removeItem(this.batcherKey);
23101
- }.bind(this));
23102
- }.bind(this));
23103
- };
23343
+ return truncateBody(result);
23344
+ }
23104
23345
 
23105
- SessionRecording.prototype.getConfig = function(configVar) {
23346
+ /**
23347
+ * @param {Request | Response} r
23348
+ * @returns {Promise<string>}
23349
+ */
23350
+ function tryReadFetchBody(r) {
23351
+ return new Promise(function(resolve) {
23352
+ var timeout = setTimeout(function() {
23353
+ resolve('Timeout while trying to read body');
23354
+ }, 500);
23355
+ try {
23356
+ r.clone()
23357
+ .text()
23358
+ .then(
23359
+ function(txt) {
23360
+ clearTimeout(timeout);
23361
+ resolve(truncateBody(txt));
23362
+ },
23363
+ function(reason) {
23364
+ clearTimeout(timeout);
23365
+ resolve('Failed to read body: ' + String(reason));
23366
+ }
23367
+ );
23368
+ } catch (e) {
23369
+ clearTimeout(timeout);
23370
+ resolve('Failed to read body: ' + String(e));
23371
+ }
23372
+ });
23373
+ }
23374
+
23375
+ /**
23376
+ * @param {Window} win
23377
+ * @param {string} initiatorType
23378
+ * @param {string} url
23379
+ * @param {number} [after]
23380
+ * @param {number} [before]
23381
+ * @param {number} [attempt]
23382
+ * @returns {Promise<PerformanceResourceTiming>}
23383
+ */
23384
+ function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
23385
+ if (attempt === undefined) {
23386
+ attempt = 0;
23387
+ }
23388
+ if (attempt > 10) {
23389
+ logger$3.error('Cannot find performance entry');
23390
+ return Promise.resolve(null);
23391
+ }
23392
+ var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
23393
+ win.performance.getEntriesByName(url)
23394
+ );
23395
+ var performanceEntry = findLast(
23396
+ urlPerformanceEntries,
23397
+ function(entry) {
23398
+ return isResourceTiming(entry) &&
23399
+ entry.initiatorType === initiatorType &&
23400
+ (!after || entry.startTime >= after) &&
23401
+ (!before || entry.startTime <= before);
23402
+ }
23403
+ );
23404
+ if (!performanceEntry) {
23405
+ return new Promise(function(resolve) {
23406
+ setTimeout(resolve, 50 * attempt);
23407
+ }).then(function() {
23408
+ return getRequestPerformanceEntry(
23409
+ win,
23410
+ initiatorType,
23411
+ url,
23412
+ after,
23413
+ before,
23414
+ attempt + 1
23415
+ );
23416
+ });
23417
+ }
23418
+ return Promise.resolve(performanceEntry);
23419
+ }
23420
+
23421
+ /**
23422
+ * @param {networkCallback} cb
23423
+ * @param {Window} win
23424
+ * @param {Required<NetworkRecordOptions>} options
23425
+ * @returns {listenerHandler}
23426
+ */
23427
+ function initXhrObserver(cb, win, options) {
23428
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
23429
+ return function() {
23430
+ //
23431
+ };
23432
+ }
23433
+ var restorePatch = patch(
23434
+ win.XMLHttpRequest.prototype,
23435
+ 'open',
23436
+ function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
23437
+ return function(
23438
+ /** @type {string} */ method,
23439
+ /** @type {string | URL} */ url,
23440
+ /** @type {boolean} */ async,
23441
+ username, password
23442
+ ) {
23443
+ if (async === undefined) {
23444
+ async = true;
23445
+ }
23446
+ var xhr = /** @type {XMLHttpRequest} */ (this);
23447
+ var req = new Request(url, { method: method });
23448
+ /** @type {Partial<NetworkRequest>} */
23449
+ var networkRequest = {};
23450
+ /** @type {number | undefined} */
23451
+ var after;
23452
+ /** @type {number | undefined} */
23453
+ var before;
23454
+
23455
+ /** @type {Headers} */
23456
+ var requestHeaders = {};
23457
+ var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
23458
+ xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
23459
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23460
+ requestHeaders[header] = value;
23461
+ }
23462
+ return originalSetRequestHeader(header, value);
23463
+ };
23464
+ networkRequest.requestHeaders = requestHeaders;
23465
+
23466
+ var originalSend = xhr.send.bind(xhr);
23467
+ xhr.send = function(/** @type {Body} */ body) {
23468
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23469
+ networkRequest.requestBody = tryReadXHRBody(body);
23470
+ }
23471
+ after = win.performance.now();
23472
+ return originalSend(body);
23473
+ };
23474
+ xhr.addEventListener('readystatechange', function() {
23475
+ if (xhr.readyState !== xhr.DONE) {
23476
+ return;
23477
+ }
23478
+ before = win.performance.now();
23479
+ /** @type {Headers} */
23480
+ var responseHeaders = {};
23481
+ var rawHeaders = xhr.getAllResponseHeaders();
23482
+ if (rawHeaders) {
23483
+ var headers = rawHeaders.trim().split(/[\r\n]+/);
23484
+ headers.forEach(function(line) {
23485
+ if (!line) return;
23486
+ var colonIndex = line.indexOf(': ');
23487
+ if (colonIndex === -1) return;
23488
+ var header = line.substring(0, colonIndex);
23489
+ var value = line.substring(colonIndex + 2);
23490
+ if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
23491
+ responseHeaders[header] = value;
23492
+ }
23493
+ });
23494
+ }
23495
+ networkRequest.responseHeaders = responseHeaders;
23496
+ if (
23497
+ shouldRecordBody('response', options.recordBodyUrls, req.url)
23498
+ ) {
23499
+ networkRequest.responseBody = tryReadXHRBody(xhr.response);
23500
+ }
23501
+ getRequestPerformanceEntry(
23502
+ win,
23503
+ 'xmlhttprequest',
23504
+ req.url,
23505
+ after,
23506
+ before
23507
+ )
23508
+ .then(function(entry) {
23509
+ if (!entry) {
23510
+ logger$3.error('Failed to get performance entry for XHR request to ' + req.url);
23511
+ return;
23512
+ }
23513
+ /** @type {NetworkRequest} */
23514
+ var request = {
23515
+ url: entry.name,
23516
+ method: req.method,
23517
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23518
+ status: xhr.status,
23519
+ startTime: Math.round(entry.startTime),
23520
+ endTime: Math.round(entry.responseEnd),
23521
+ timeOrigin: getTimeOrigin(win),
23522
+ requestHeaders: networkRequest.requestHeaders,
23523
+ requestBody: networkRequest.requestBody,
23524
+ responseHeaders: networkRequest.responseHeaders,
23525
+ responseBody: networkRequest.responseBody,
23526
+ };
23527
+ cb({ requests: [request] });
23528
+ })
23529
+ .catch(function(e) {
23530
+ logger$3.error('Error recording XHR request to ' + req.url + ': ' + String(e));
23531
+ });
23532
+ });
23533
+
23534
+ originalOpen.call(xhr, method, url, async, username, password);
23535
+ };
23536
+ }
23537
+ );
23538
+ return function() {
23539
+ restorePatch();
23540
+ };
23541
+ }
23542
+
23543
+ /**
23544
+ * @param {networkCallback} cb
23545
+ * @param {Window} win
23546
+ * @param {Required<NetworkRecordOptions>} options
23547
+ * @returns {listenerHandler}
23548
+ */
23549
+ function initFetchObserver(cb, win, options) {
23550
+ if (!options.initiatorTypes.includes('fetch')) {
23551
+ return function() {
23552
+ //
23553
+ };
23554
+ }
23555
+
23556
+ var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
23557
+ return function() {
23558
+ var req = new Request(arguments[0], arguments[1]);
23559
+ /** @type {Response | undefined} */
23560
+ var res;
23561
+ /** @type {Partial<NetworkRequest>} */
23562
+ var networkRequest = {};
23563
+ /** @type {number | undefined} */
23564
+ var after;
23565
+ /** @type {number | undefined} */
23566
+ var before;
23567
+
23568
+ var originalFetchPromise;
23569
+ var requestBodyPromise = Promise.resolve(undefined);
23570
+ var responseBodyPromise = Promise.resolve(undefined);
23571
+ try {
23572
+ /** @type {Headers} */
23573
+ var requestHeaders = {};
23574
+ req.headers.forEach(function(value, header) {
23575
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23576
+ requestHeaders[header] = value;
23577
+ }
23578
+ });
23579
+ networkRequest.requestHeaders = requestHeaders;
23580
+
23581
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23582
+ requestBodyPromise = tryReadFetchBody(req)
23583
+ .then(function(body) {
23584
+ networkRequest.requestBody = body;
23585
+ });
23586
+ }
23587
+
23588
+ after = win.performance.now();
23589
+ originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
23590
+ res = response;
23591
+ before = win.performance.now();
23592
+
23593
+ /** @type {Headers} */
23594
+ var responseHeaders = {};
23595
+ res.headers.forEach(function(value, header) {
23596
+ if (shouldRecordHeader('response', options.recordHeaders, header)) {
23597
+ responseHeaders[header] = value;
23598
+ }
23599
+ });
23600
+ networkRequest.responseHeaders = responseHeaders;
23601
+
23602
+ if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
23603
+ responseBodyPromise = tryReadFetchBody(res)
23604
+ .then(function(body) {
23605
+ networkRequest.responseBody = body;
23606
+ });
23607
+ }
23608
+
23609
+ return res;
23610
+ });
23611
+ } catch (e) {
23612
+ originalFetchPromise = Promise.reject(e);
23613
+ }
23614
+
23615
+ // await concurrently so we don't delay the fetch response
23616
+ Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
23617
+ .then(function () {
23618
+ return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
23619
+ })
23620
+ .then(function(entry) {
23621
+ if (!entry) {
23622
+ logger$3.error('Failed to get performance entry for fetch request to ' + req.url);
23623
+ return;
23624
+ }
23625
+ /** @type {NetworkRequest} */
23626
+ var request = {
23627
+ url: entry.name,
23628
+ method: req.method,
23629
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23630
+ status: res ? res.status : undefined,
23631
+ startTime: Math.round(entry.startTime),
23632
+ endTime: Math.round(entry.responseEnd),
23633
+ timeOrigin: getTimeOrigin(win),
23634
+ requestHeaders: networkRequest.requestHeaders,
23635
+ requestBody: networkRequest.requestBody,
23636
+ responseHeaders: networkRequest.responseHeaders,
23637
+ responseBody: networkRequest.responseBody,
23638
+ };
23639
+ cb({ requests: [request] });
23640
+ })
23641
+ .catch(function (e) {
23642
+ logger$3.error('Error recording fetch request to ' + req.url + ': ' + String(e));
23643
+ });
23644
+
23645
+ return originalFetchPromise;
23646
+ };
23647
+ });
23648
+ return function() {
23649
+ restorePatch();
23650
+ };
23651
+ }
23652
+
23653
+ /**
23654
+ * @param {networkCallback} callback
23655
+ * @param {Window} win
23656
+ * @param {NetworkRecordOptions} options
23657
+ * @returns {listenerHandler}
23658
+ */
23659
+ function initNetworkObserver(callback, win, options) {
23660
+ if (!('performance' in win)) {
23661
+ return function() {
23662
+ //
23663
+ };
23664
+ }
23665
+
23666
+ var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
23667
+ var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
23668
+ options = Object.assign({}, options, {
23669
+ recordHeaders: recordHeaders,
23670
+ recordBodyUrls: recordBodyUrls,
23671
+ });
23672
+ var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
23673
+
23674
+ /** @type {networkCallback} */
23675
+ var cb = function(data) {
23676
+ var requests = data.requests.filter(function(request) {
23677
+ var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
23678
+ return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
23679
+ });
23680
+ if (requests.length > 0 || data.isInitial) {
23681
+ callback(Object.assign({}, data, { requests: requests }));
23682
+ }
23683
+ };
23684
+ var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
23685
+ var xhrObserver = initXhrObserver(cb, win, networkOptions);
23686
+ var fetchObserver = initFetchObserver(cb, win, networkOptions);
23687
+ return function() {
23688
+ performanceObserver();
23689
+ xhrObserver();
23690
+ fetchObserver();
23691
+ };
23692
+ }
23693
+
23694
+ // arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
23695
+ // a changed format in the mixpanel product.
23696
+ var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
23697
+
23698
+ /**
23699
+ * @param {NetworkRecordOptions} [options]
23700
+ * @returns {RecordPlugin}
23701
+ */
23702
+ var getRecordNetworkPlugin = function(options) {
23703
+ return {
23704
+ name: NETWORK_PLUGIN_NAME,
23705
+ observer: initNetworkObserver,
23706
+ options: options,
23707
+ };
23708
+ };
23709
+
23710
+ /**
23711
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23712
+ */
23713
+
23714
+
23715
+ var logger$2 = console_with_prefix('recorder');
23716
+ var CompressionStream = win['CompressionStream'];
23717
+
23718
+ var RECORDER_BATCHER_LIB_CONFIG = {
23719
+ 'batch_size': 1000,
23720
+ 'batch_flush_interval_ms': 10 * 1000,
23721
+ 'batch_request_timeout_ms': 90 * 1000,
23722
+ 'batch_autostart': true
23723
+ };
23724
+
23725
+ var ACTIVE_SOURCES = new Set([
23726
+ IncrementalSource.MouseMove,
23727
+ IncrementalSource.MouseInteraction,
23728
+ IncrementalSource.Scroll,
23729
+ IncrementalSource.ViewportResize,
23730
+ IncrementalSource.Input,
23731
+ IncrementalSource.TouchMove,
23732
+ IncrementalSource.MediaInteraction,
23733
+ IncrementalSource.Drag,
23734
+ IncrementalSource.Selection,
23735
+ ]);
23736
+
23737
+ function isUserEvent(ev) {
23738
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23739
+ }
23740
+
23741
+ /**
23742
+ * @typedef {Object} SerializedRecording
23743
+ * @property {number} idleExpires
23744
+ * @property {number} maxExpires
23745
+ * @property {number} replayStartTime
23746
+ * @property {number} lastEventTimestamp
23747
+ * @property {number} seqNo
23748
+ * @property {string} batchStartUrl
23749
+ * @property {string} replayId
23750
+ * @property {string} tabId
23751
+ * @property {string} replayStartUrl
23752
+ */
23753
+
23754
+ /**
23755
+ * @typedef {Object} SessionRecordingOptions
23756
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23757
+ * @property {String} [options.replayId] - unique uuid for a single replay
23758
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23759
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23760
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23761
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23762
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23763
+ * optional properties for deserialization:
23764
+ * @property {number} idleExpires
23765
+ * @property {number} maxExpires
23766
+ * @property {number} replayStartTime
23767
+ * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23768
+ * @property {number} seqNo
23769
+ * @property {string} batchStartUrl
23770
+ * @property {string} replayStartUrl
23771
+ */
23772
+
23773
+ /**
23774
+ * @typedef {Object} UserIdInfo
23775
+ * @property {string} distinct_id
23776
+ * @property {string} user_id
23777
+ * @property {string} device_id
23778
+ */
23779
+
23780
+
23781
+ /**
23782
+ * This class encapsulates a single session recording and its lifecycle.
23783
+ * @param {SessionRecordingOptions} options
23784
+ */
23785
+ var SessionRecording = function(options) {
23786
+ this._mixpanel = options.mixpanelInstance;
23787
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23788
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23789
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23790
+ this._rrwebRecord = options.rrwebRecord || null;
23791
+
23792
+ // internal rrweb stopRecording function
23793
+ this._stopRecording = null;
23794
+ this.replayId = options.replayId;
23795
+
23796
+ this.batchStartUrl = options.batchStartUrl || null;
23797
+ this.replayStartUrl = options.replayStartUrl || null;
23798
+ this.idleExpires = options.idleExpires || null;
23799
+ this.maxExpires = options.maxExpires || null;
23800
+ this.replayStartTime = options.replayStartTime || null;
23801
+ this.lastEventTimestamp = options.lastEventTimestamp || null;
23802
+ this.seqNo = options.seqNo || 0;
23803
+
23804
+ this.idleTimeoutId = null;
23805
+ this.maxTimeoutId = null;
23806
+
23807
+ this.recordMaxMs = MAX_RECORDING_MS;
23808
+ this.recordMinMs = 0;
23809
+
23810
+ // disable persistence if localStorage is not supported
23811
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
23812
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23813
+
23814
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23815
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23816
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23817
+ this.batcher = new RequestBatcher(this.batcherKey, {
23818
+ errorReporter: this.reportError.bind(this),
23819
+ flushOnlyOnInterval: true,
23820
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
23821
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23822
+ queueStorage: this.queueStorage,
23823
+ sharedLockStorage: options.sharedLockStorage,
23824
+ usePersistence: usePersistence,
23825
+ stopAllBatchingFunc: this.stopRecording.bind(this),
23826
+
23827
+ // increased throttle and shared lock timeout because recording events are very high frequency.
23828
+ // this will minimize the amount of lock contention between enqueued events.
23829
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23830
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23831
+ sharedLockTimeoutMS: 10 * 1000,
23832
+ });
23833
+ };
23834
+
23835
+ /**
23836
+ * @returns {UserIdInfo}
23837
+ */
23838
+ SessionRecording.prototype.getUserIdInfo = function () {
23839
+ if (this.finalFlushUserIdInfo) {
23840
+ return this.finalFlushUserIdInfo;
23841
+ }
23842
+
23843
+ var userIdInfo = {
23844
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
23845
+ };
23846
+
23847
+ // send ID management props if they exist
23848
+ var deviceId = this._mixpanel.get_property('$device_id');
23849
+ if (deviceId) {
23850
+ userIdInfo['$device_id'] = deviceId;
23851
+ }
23852
+ var userId = this._mixpanel.get_property('$user_id');
23853
+ if (userId) {
23854
+ userIdInfo['$user_id'] = userId;
23855
+ }
23856
+ return userIdInfo;
23857
+ };
23858
+
23859
+ SessionRecording.prototype.unloadPersistedData = function () {
23860
+ this.batcher.stop();
23861
+
23862
+ return this.queueStorage.init().catch(function () {
23863
+ this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23864
+ }.bind(this)).then(function () {
23865
+ // if the recording is too short, just delete any stored events without flushing
23866
+ if (this.getDurationMs() < this._getRecordMinMs()) {
23867
+ return this.queueStorage.removeItem(this.batcherKey);
23868
+ }
23869
+
23870
+ return this.batcher.flush()
23871
+ .then(function () {
23872
+ return this.queueStorage.removeItem(this.batcherKey);
23873
+ }.bind(this));
23874
+ }.bind(this));
23875
+ };
23876
+
23877
+ SessionRecording.prototype.getConfig = function(configVar) {
23106
23878
  return this._mixpanel.get_config(configVar);
23107
23879
  };
23108
23880
 
@@ -23168,6 +23940,29 @@
23168
23940
 
23169
23941
  var privacyConfig = getPrivacyConfig(this._mixpanel);
23170
23942
 
23943
+ var plugins = [];
23944
+ if (this.getConfig('record_network')) {
23945
+ var options = this.getConfig('record_network_options') || {};
23946
+ // don't track requests to Mixpanel /record API
23947
+ var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
23948
+ ignoreRequestUrls.push(this._getApiRoute());
23949
+ options.ignoreRequestUrls = ignoreRequestUrls;
23950
+
23951
+ plugins.push(getRecordNetworkPlugin(options));
23952
+ }
23953
+
23954
+ if (this.getConfig('record_console')) {
23955
+ plugins.push(
23956
+ getRecordConsolePlugin({
23957
+ stringifyOptions: {
23958
+ stringLengthLimit: 1000,
23959
+ numOfKeysLimit: 50,
23960
+ depthOfLimit: 2
23961
+ }
23962
+ })
23963
+ );
23964
+ }
23965
+
23171
23966
  try {
23172
23967
  this._stopRecording = this._rrwebRecord({
23173
23968
  'emit': function (ev) {
@@ -23206,15 +24001,7 @@
23206
24001
  'sampling': {
23207
24002
  'canvas': 15
23208
24003
  },
23209
- 'plugins': this.getConfig('record_console') ? [
23210
- getRecordConsolePlugin({
23211
- stringifyOptions: {
23212
- stringLengthLimit: 1000,
23213
- numOfKeysLimit: 50,
23214
- depthOfLimit: 2
23215
- }
23216
- })
23217
- ] : []
24004
+ 'plugins': plugins,
23218
24005
  });
23219
24006
  } catch (err) {
23220
24007
  this.reportError('Unexpected error when starting rrweb recording.', err);
@@ -23329,6 +24116,10 @@
23329
24116
  return recording;
23330
24117
  };
23331
24118
 
24119
+ SessionRecording.prototype._getApiRoute = function () {
24120
+ return this.getConfig('api_routes')['record'];
24121
+ };
24122
+
23332
24123
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
23333
24124
  var onSuccess = function (response, responseBody) {
23334
24125
  // Update batch specific props only if the request was successful to guarantee ordering.
@@ -23348,7 +24139,7 @@
23348
24139
  });
23349
24140
  }.bind(this);
23350
24141
  var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
23351
- win['fetch'](apiHost + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
24142
+ win['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
23352
24143
  'method': 'POST',
23353
24144
  'headers': {
23354
24145
  'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
@@ -23749,8 +24540,12 @@
23749
24540
  this.startRecording({shouldStopBatcher: true});
23750
24541
  };
23751
24542
 
24543
+ MixpanelRecorder.prototype.isRecording = function () {
24544
+ return this.activeRecording && !this.activeRecording.isRrwebStopped();
24545
+ };
24546
+
23752
24547
  MixpanelRecorder.prototype.getActiveReplayId = function () {
23753
- if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
24548
+ if (this.isRecording()) {
23754
24549
  return this.activeRecording.replayId;
23755
24550
  } else {
23756
24551
  return null;
@@ -23765,7 +24560,538 @@
23765
24560
  }
23766
24561
  });
23767
24562
 
23768
- win['__mp_recorder'] = MixpanelRecorder;
24563
+ win[RECORDER_GLOBAL_NAME] = MixpanelRecorder;
24564
+
24565
+ function getDefaultExportFromCjs (x) {
24566
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
24567
+ }
24568
+
24569
+ var logic$1 = {exports: {}};
24570
+
24571
+ /* globals define,module */
24572
+ var logic = logic$1.exports;
24573
+
24574
+ var hasRequiredLogic;
24575
+
24576
+ function requireLogic () {
24577
+ if (hasRequiredLogic) return logic$1.exports;
24578
+ hasRequiredLogic = 1;
24579
+ (function (module, exports) {
24580
+ (function(root, factory) {
24581
+ {
24582
+ module.exports = factory();
24583
+ }
24584
+ }(logic, function() {
24585
+ /* globals console:false */
24586
+
24587
+ if ( ! Array.isArray) {
24588
+ Array.isArray = function(arg) {
24589
+ return Object.prototype.toString.call(arg) === "[object Array]";
24590
+ };
24591
+ }
24592
+
24593
+ /**
24594
+ * Return an array that contains no duplicates (original not modified)
24595
+ * @param {array} array Original reference array
24596
+ * @return {array} New array with no duplicates
24597
+ */
24598
+ function arrayUnique(array) {
24599
+ var a = [];
24600
+ for (var i=0, l=array.length; i<l; i++) {
24601
+ if (a.indexOf(array[i]) === -1) {
24602
+ a.push(array[i]);
24603
+ }
24604
+ }
24605
+ return a;
24606
+ }
24607
+
24608
+ var jsonLogic = {};
24609
+ var operations = {
24610
+ "==": function(a, b) {
24611
+ return a == b;
24612
+ },
24613
+ "===": function(a, b) {
24614
+ return a === b;
24615
+ },
24616
+ "!=": function(a, b) {
24617
+ return a != b;
24618
+ },
24619
+ "!==": function(a, b) {
24620
+ return a !== b;
24621
+ },
24622
+ ">": function(a, b) {
24623
+ return a > b;
24624
+ },
24625
+ ">=": function(a, b) {
24626
+ return a >= b;
24627
+ },
24628
+ "<": function(a, b, c) {
24629
+ return (c === undefined) ? a < b : (a < b) && (b < c);
24630
+ },
24631
+ "<=": function(a, b, c) {
24632
+ return (c === undefined) ? a <= b : (a <= b) && (b <= c);
24633
+ },
24634
+ "!!": function(a) {
24635
+ return jsonLogic.truthy(a);
24636
+ },
24637
+ "!": function(a) {
24638
+ return !jsonLogic.truthy(a);
24639
+ },
24640
+ "%": function(a, b) {
24641
+ return a % b;
24642
+ },
24643
+ "log": function(a) {
24644
+ console.log(a); return a;
24645
+ },
24646
+ "in": function(a, b) {
24647
+ if (!b || typeof b.indexOf === "undefined") return false;
24648
+ return (b.indexOf(a) !== -1);
24649
+ },
24650
+ "cat": function() {
24651
+ return Array.prototype.join.call(arguments, "");
24652
+ },
24653
+ "substr": function(source, start, end) {
24654
+ if (end < 0) {
24655
+ // JavaScript doesn't support negative end, this emulates PHP behavior
24656
+ var temp = String(source).substr(start);
24657
+ return temp.substr(0, temp.length + end);
24658
+ }
24659
+ return String(source).substr(start, end);
24660
+ },
24661
+ "+": function() {
24662
+ return Array.prototype.reduce.call(arguments, function(a, b) {
24663
+ return parseFloat(a, 10) + parseFloat(b, 10);
24664
+ }, 0);
24665
+ },
24666
+ "*": function() {
24667
+ return Array.prototype.reduce.call(arguments, function(a, b) {
24668
+ return parseFloat(a, 10) * parseFloat(b, 10);
24669
+ });
24670
+ },
24671
+ "-": function(a, b) {
24672
+ if (b === undefined) {
24673
+ return -a;
24674
+ } else {
24675
+ return a - b;
24676
+ }
24677
+ },
24678
+ "/": function(a, b) {
24679
+ return a / b;
24680
+ },
24681
+ "min": function() {
24682
+ return Math.min.apply(this, arguments);
24683
+ },
24684
+ "max": function() {
24685
+ return Math.max.apply(this, arguments);
24686
+ },
24687
+ "merge": function() {
24688
+ return Array.prototype.reduce.call(arguments, function(a, b) {
24689
+ return a.concat(b);
24690
+ }, []);
24691
+ },
24692
+ "var": function(a, b) {
24693
+ var not_found = (b === undefined) ? null : b;
24694
+ var data = this;
24695
+ if (typeof a === "undefined" || a==="" || a===null) {
24696
+ return data;
24697
+ }
24698
+ var sub_props = String(a).split(".");
24699
+ for (var i = 0; i < sub_props.length; i++) {
24700
+ if (data === null || data === undefined) {
24701
+ return not_found;
24702
+ }
24703
+ // Descending into data
24704
+ data = data[sub_props[i]];
24705
+ if (data === undefined) {
24706
+ return not_found;
24707
+ }
24708
+ }
24709
+ return data;
24710
+ },
24711
+ "missing": function() {
24712
+ /*
24713
+ Missing can receive many keys as many arguments, like {"missing:[1,2]}
24714
+ Missing can also receive *one* argument that is an array of keys,
24715
+ which typically happens if it's actually acting on the output of another command
24716
+ (like 'if' or 'merge')
24717
+ */
24718
+
24719
+ var missing = [];
24720
+ var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments;
24721
+
24722
+ for (var i = 0; i < keys.length; i++) {
24723
+ var key = keys[i];
24724
+ var value = jsonLogic.apply({"var": key}, this);
24725
+ if (value === null || value === "") {
24726
+ missing.push(key);
24727
+ }
24728
+ }
24729
+
24730
+ return missing;
24731
+ },
24732
+ "missing_some": function(need_count, options) {
24733
+ // missing_some takes two arguments, how many (minimum) items must be present, and an array of keys (just like 'missing') to check for presence.
24734
+ var are_missing = jsonLogic.apply({"missing": options}, this);
24735
+
24736
+ if (options.length - are_missing.length >= need_count) {
24737
+ return [];
24738
+ } else {
24739
+ return are_missing;
24740
+ }
24741
+ },
24742
+ };
24743
+
24744
+ jsonLogic.is_logic = function(logic) {
24745
+ return (
24746
+ typeof logic === "object" && // An object
24747
+ logic !== null && // but not null
24748
+ ! Array.isArray(logic) && // and not an array
24749
+ Object.keys(logic).length === 1 // with exactly one key
24750
+ );
24751
+ };
24752
+
24753
+ /*
24754
+ This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
24755
+
24756
+ Spec and rationale here: http://jsonlogic.com/truthy
24757
+ */
24758
+ jsonLogic.truthy = function(value) {
24759
+ if (Array.isArray(value) && value.length === 0) {
24760
+ return false;
24761
+ }
24762
+ return !! value;
24763
+ };
24764
+
24765
+
24766
+ jsonLogic.get_operator = function(logic) {
24767
+ return Object.keys(logic)[0];
24768
+ };
24769
+
24770
+ jsonLogic.get_values = function(logic) {
24771
+ return logic[jsonLogic.get_operator(logic)];
24772
+ };
24773
+
24774
+ jsonLogic.apply = function(logic, data) {
24775
+ // Does this array contain logic? Only one way to find out.
24776
+ if (Array.isArray(logic)) {
24777
+ return logic.map(function(l) {
24778
+ return jsonLogic.apply(l, data);
24779
+ });
24780
+ }
24781
+ // You've recursed to a primitive, stop!
24782
+ if ( ! jsonLogic.is_logic(logic) ) {
24783
+ return logic;
24784
+ }
24785
+
24786
+ var op = jsonLogic.get_operator(logic);
24787
+ var values = logic[op];
24788
+ var i;
24789
+ var current;
24790
+ var scopedLogic;
24791
+ var scopedData;
24792
+ var initial;
24793
+
24794
+ // easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
24795
+ if ( ! Array.isArray(values)) {
24796
+ values = [values];
24797
+ }
24798
+
24799
+ // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
24800
+ if (op === "if" || op == "?:") {
24801
+ /* 'if' should be called with a odd number of parameters, 3 or greater
24802
+ This works on the pattern:
24803
+ if( 0 ){ 1 }else{ 2 };
24804
+ if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
24805
+ if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
24806
+
24807
+ The implementation is:
24808
+ For pairs of values (0,1 then 2,3 then 4,5 etc)
24809
+ If the first evaluates truthy, evaluate and return the second
24810
+ If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3)
24811
+ given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false)
24812
+ given 0 parameters, return NULL (not great practice, but there was no Else)
24813
+ */
24814
+ for (i = 0; i < values.length - 1; i += 2) {
24815
+ if ( jsonLogic.truthy( jsonLogic.apply(values[i], data) ) ) {
24816
+ return jsonLogic.apply(values[i+1], data);
24817
+ }
24818
+ }
24819
+ if (values.length === i+1) {
24820
+ return jsonLogic.apply(values[i], data);
24821
+ }
24822
+ return null;
24823
+ } else if (op === "and") { // Return first falsy, or last
24824
+ for (i=0; i < values.length; i+=1) {
24825
+ current = jsonLogic.apply(values[i], data);
24826
+ if ( ! jsonLogic.truthy(current)) {
24827
+ return current;
24828
+ }
24829
+ }
24830
+ return current; // Last
24831
+ } else if (op === "or") {// Return first truthy, or last
24832
+ for (i=0; i < values.length; i+=1) {
24833
+ current = jsonLogic.apply(values[i], data);
24834
+ if ( jsonLogic.truthy(current) ) {
24835
+ return current;
24836
+ }
24837
+ }
24838
+ return current; // Last
24839
+ } else if (op === "filter") {
24840
+ scopedData = jsonLogic.apply(values[0], data);
24841
+ scopedLogic = values[1];
24842
+
24843
+ if ( ! Array.isArray(scopedData)) {
24844
+ return [];
24845
+ }
24846
+ // Return only the elements from the array in the first argument,
24847
+ // that return truthy when passed to the logic in the second argument.
24848
+ // For parity with JavaScript, reindex the returned array
24849
+ return scopedData.filter(function(datum) {
24850
+ return jsonLogic.truthy( jsonLogic.apply(scopedLogic, datum));
24851
+ });
24852
+ } else if (op === "map") {
24853
+ scopedData = jsonLogic.apply(values[0], data);
24854
+ scopedLogic = values[1];
24855
+
24856
+ if ( ! Array.isArray(scopedData)) {
24857
+ return [];
24858
+ }
24859
+
24860
+ return scopedData.map(function(datum) {
24861
+ return jsonLogic.apply(scopedLogic, datum);
24862
+ });
24863
+ } else if (op === "reduce") {
24864
+ scopedData = jsonLogic.apply(values[0], data);
24865
+ scopedLogic = values[1];
24866
+ initial = typeof values[2] !== "undefined" ? jsonLogic.apply(values[2], data) : null;
24867
+
24868
+ if ( ! Array.isArray(scopedData)) {
24869
+ return initial;
24870
+ }
24871
+
24872
+ return scopedData.reduce(
24873
+ function(accumulator, current) {
24874
+ return jsonLogic.apply(
24875
+ scopedLogic,
24876
+ {current: current, accumulator: accumulator}
24877
+ );
24878
+ },
24879
+ initial
24880
+ );
24881
+ } else if (op === "all") {
24882
+ scopedData = jsonLogic.apply(values[0], data);
24883
+ scopedLogic = values[1];
24884
+ // All of an empty set is false. Note, some and none have correct fallback after the for loop
24885
+ if ( ! Array.isArray(scopedData) || ! scopedData.length) {
24886
+ return false;
24887
+ }
24888
+ for (i=0; i < scopedData.length; i+=1) {
24889
+ if ( ! jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
24890
+ return false; // First falsy, short circuit
24891
+ }
24892
+ }
24893
+ return true; // All were truthy
24894
+ } else if (op === "none") {
24895
+ scopedData = jsonLogic.apply(values[0], data);
24896
+ scopedLogic = values[1];
24897
+
24898
+ if ( ! Array.isArray(scopedData) || ! scopedData.length) {
24899
+ return true;
24900
+ }
24901
+ for (i=0; i < scopedData.length; i+=1) {
24902
+ if ( jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
24903
+ return false; // First truthy, short circuit
24904
+ }
24905
+ }
24906
+ return true; // None were truthy
24907
+ } else if (op === "some") {
24908
+ scopedData = jsonLogic.apply(values[0], data);
24909
+ scopedLogic = values[1];
24910
+
24911
+ if ( ! Array.isArray(scopedData) || ! scopedData.length) {
24912
+ return false;
24913
+ }
24914
+ for (i=0; i < scopedData.length; i+=1) {
24915
+ if ( jsonLogic.truthy( jsonLogic.apply(scopedLogic, scopedData[i]) )) {
24916
+ return true; // First truthy, short circuit
24917
+ }
24918
+ }
24919
+ return false; // None were truthy
24920
+ }
24921
+
24922
+ // Everyone else gets immediate depth-first recursion
24923
+ values = values.map(function(val) {
24924
+ return jsonLogic.apply(val, data);
24925
+ });
24926
+
24927
+
24928
+ // The operation is called with "data" bound to its "this" and "values" passed as arguments.
24929
+ // Structured commands like % or > can name formal arguments while flexible commands (like missing or merge) can operate on the pseudo-array arguments
24930
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
24931
+ if (operations.hasOwnProperty(op) && typeof operations[op] === "function") {
24932
+ return operations[op].apply(data, values);
24933
+ } else if (op.indexOf(".") > 0) { // Contains a dot, and not in the 0th position
24934
+ var sub_ops = String(op).split(".");
24935
+ var operation = operations;
24936
+ for (i = 0; i < sub_ops.length; i++) {
24937
+ if (!operation.hasOwnProperty(sub_ops[i])) {
24938
+ throw new Error("Unrecognized operation " + op +
24939
+ " (failed at " + sub_ops.slice(0, i+1).join(".") + ")");
24940
+ }
24941
+ // Descending into operations
24942
+ operation = operation[sub_ops[i]];
24943
+ }
24944
+
24945
+ return operation.apply(data, values);
24946
+ }
24947
+
24948
+ throw new Error("Unrecognized operation " + op );
24949
+ };
24950
+
24951
+ jsonLogic.uses_data = function(logic) {
24952
+ var collection = [];
24953
+
24954
+ if (jsonLogic.is_logic(logic)) {
24955
+ var op = jsonLogic.get_operator(logic);
24956
+ var values = logic[op];
24957
+
24958
+ if ( ! Array.isArray(values)) {
24959
+ values = [values];
24960
+ }
24961
+
24962
+ if (op === "var") {
24963
+ // This doesn't cover the case where the arg to var is itself a rule.
24964
+ collection.push(values[0]);
24965
+ } else {
24966
+ // Recursion!
24967
+ values.forEach(function(val) {
24968
+ collection.push.apply(collection, jsonLogic.uses_data(val) );
24969
+ });
24970
+ }
24971
+ }
24972
+
24973
+ return arrayUnique(collection);
24974
+ };
24975
+
24976
+ jsonLogic.add_operation = function(name, code) {
24977
+ operations[name] = code;
24978
+ };
24979
+
24980
+ jsonLogic.rm_operation = function(name) {
24981
+ delete operations[name];
24982
+ };
24983
+
24984
+ jsonLogic.rule_like = function(rule, pattern) {
24985
+ // console.log("Is ". JSON.stringify(rule) . " like " . JSON.stringify(pattern) . "?");
24986
+ if (pattern === rule) {
24987
+ return true;
24988
+ } // TODO : Deep object equivalency?
24989
+ if (pattern === "@") {
24990
+ return true;
24991
+ } // Wildcard!
24992
+ if (pattern === "number") {
24993
+ return (typeof rule === "number");
24994
+ }
24995
+ if (pattern === "string") {
24996
+ return (typeof rule === "string");
24997
+ }
24998
+ if (pattern === "array") {
24999
+ // !logic test might be superfluous in JavaScript
25000
+ return Array.isArray(rule) && ! jsonLogic.is_logic(rule);
25001
+ }
25002
+
25003
+ if (jsonLogic.is_logic(pattern)) {
25004
+ if (jsonLogic.is_logic(rule)) {
25005
+ var pattern_op = jsonLogic.get_operator(pattern);
25006
+ var rule_op = jsonLogic.get_operator(rule);
25007
+
25008
+ if (pattern_op === "@" || pattern_op === rule_op) {
25009
+ // echo "\nOperators match, go deeper\n";
25010
+ return jsonLogic.rule_like(
25011
+ jsonLogic.get_values(rule, false),
25012
+ jsonLogic.get_values(pattern, false)
25013
+ );
25014
+ }
25015
+ }
25016
+ return false; // pattern is logic, rule isn't, can't be eq
25017
+ }
25018
+
25019
+ if (Array.isArray(pattern)) {
25020
+ if (Array.isArray(rule)) {
25021
+ if (pattern.length !== rule.length) {
25022
+ return false;
25023
+ }
25024
+ /*
25025
+ Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT)
25026
+ */
25027
+ for (var i = 0; i < pattern.length; i += 1) {
25028
+ // If any fail, we fail
25029
+ if ( ! jsonLogic.rule_like(rule[i], pattern[i])) {
25030
+ return false;
25031
+ }
25032
+ }
25033
+ return true; // If they *all* passed, we pass
25034
+ } else {
25035
+ return false; // Pattern is array, rule isn't
25036
+ }
25037
+ }
25038
+
25039
+ // Not logic, not array, not a === match for rule.
25040
+ return false;
25041
+ };
25042
+
25043
+ return jsonLogic;
25044
+ }));
25045
+ } (logic$1));
25046
+ return logic$1.exports;
25047
+ }
25048
+
25049
+ var logicExports = requireLogic();
25050
+ var jsonLogic = /*@__PURE__*/getDefaultExportFromCjs(logicExports);
25051
+
25052
+ /**
25053
+ * Check if an event matches the given criteria
25054
+ * @param {string} eventName - The name of the event being checked
25055
+ * @param {Object} properties - Event properties to evaluate against property filters
25056
+ * @param {Object} criteria - Criteria to match against, with:
25057
+ * - event_name: string - Required event name (case-sensitive match)
25058
+ * - property_filters: Object - Optional JsonLogic filters for properties
25059
+ * @returns {Object} Result object with:
25060
+ * - matches: boolean - Whether the event matches the criteria
25061
+ * - error: string|undefined - Error message if evaluation failed
25062
+ */
25063
+ var eventMatchesCriteria = function(eventName, properties, criteria) {
25064
+ // Check exact event name match (case-sensitive)
25065
+ if (eventName !== criteria.event_name) {
25066
+ return { matches: false };
25067
+ }
25068
+
25069
+ // Evaluate property filters using JsonLogic
25070
+ var propertyFilters = criteria.property_filters;
25071
+ var filtersMatch = true; // default to true if no filters
25072
+
25073
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
25074
+ try {
25075
+ // Use properties as-is for case-sensitive matching
25076
+ filtersMatch = jsonLogic.apply(propertyFilters, properties || {});
25077
+ } catch (error) {
25078
+ return {
25079
+ matches: false,
25080
+ error: error.toString()
25081
+ };
25082
+ }
25083
+ }
25084
+
25085
+ return { matches: filtersMatch };
25086
+ };
25087
+
25088
+ // Create targeting library object
25089
+ var targetingLibrary = {};
25090
+ targetingLibrary['eventMatchesCriteria'] = eventMatchesCriteria;
25091
+
25092
+ // Set global Promise (use bracket notation to prevent minification)
25093
+ // This is the ONE AND ONLY global - matches recorder pattern
25094
+ win[TARGETING_GLOBAL_NAME] = Promise.resolve(targetingLibrary);
23769
25095
 
23770
25096
  /** @const */ var DEFAULT_RAGE_CLICK_THRESHOLD_PX = 30;
23771
25097
  /** @const */ var DEFAULT_RAGE_CLICK_TIMEOUT_MS = 1000;
@@ -23867,7 +25193,7 @@
23867
25193
  observer.observe(shadowRoot, this.observerConfig);
23868
25194
  this.shadowObservers.push(observer);
23869
25195
  } catch (e) {
23870
- logger$3.critical('Error while observing shadow root', e);
25196
+ logger$4.critical('Error while observing shadow root', e);
23871
25197
  }
23872
25198
  };
23873
25199
 
@@ -23878,7 +25204,7 @@
23878
25204
  }
23879
25205
 
23880
25206
  if (!weakSetSupported()) {
23881
- logger$3.critical('Shadow DOM observation unavailable: WeakSet not supported');
25207
+ logger$4.critical('Shadow DOM observation unavailable: WeakSet not supported');
23882
25208
  return;
23883
25209
  }
23884
25210
 
@@ -23894,7 +25220,7 @@
23894
25220
  try {
23895
25221
  this.shadowObservers[i].disconnect();
23896
25222
  } catch (e) {
23897
- logger$3.critical('Error while disconnecting shadow DOM observer', e);
25223
+ logger$4.critical('Error while disconnecting shadow DOM observer', e);
23898
25224
  }
23899
25225
  }
23900
25226
  this.shadowObservers = [];
@@ -24082,7 +25408,7 @@
24082
25408
 
24083
25409
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
24084
25410
  } catch (e) {
24085
- logger$3.critical('Error while setting up mutation observer', e);
25411
+ logger$4.critical('Error while setting up mutation observer', e);
24086
25412
  }
24087
25413
  }
24088
25414
 
@@ -24097,7 +25423,7 @@
24097
25423
  );
24098
25424
  this.shadowDOMObserver.start();
24099
25425
  } catch (e) {
24100
- logger$3.critical('Error while setting up shadow DOM observer', e);
25426
+ logger$4.critical('Error while setting up shadow DOM observer', e);
24101
25427
  this.shadowDOMObserver = null;
24102
25428
  }
24103
25429
  }
@@ -24124,7 +25450,7 @@
24124
25450
  try {
24125
25451
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
24126
25452
  } catch (e) {
24127
- logger$3.critical('Error while removing event listener', e);
25453
+ logger$4.critical('Error while removing event listener', e);
24128
25454
  }
24129
25455
  }
24130
25456
  this.eventListeners = [];
@@ -24133,7 +25459,7 @@
24133
25459
  try {
24134
25460
  this.mutationObserver.disconnect();
24135
25461
  } catch (e) {
24136
- logger$3.critical('Error while disconnecting mutation observer', e);
25462
+ logger$4.critical('Error while disconnecting mutation observer', e);
24137
25463
  }
24138
25464
  this.mutationObserver = null;
24139
25465
  }
@@ -24142,7 +25468,7 @@
24142
25468
  try {
24143
25469
  this.shadowDOMObserver.stop();
24144
25470
  } catch (e) {
24145
- logger$3.critical('Error while stopping shadow DOM observer', e);
25471
+ logger$4.critical('Error while stopping shadow DOM observer', e);
24146
25472
  }
24147
25473
  this.shadowDOMObserver = null;
24148
25474
  }
@@ -24220,7 +25546,7 @@
24220
25546
 
24221
25547
  Autocapture.prototype.init = function() {
24222
25548
  if (!minDOMApisSupported()) {
24223
- logger$3.critical('Autocapture unavailable: missing required DOM APIs');
25549
+ logger$4.critical('Autocapture unavailable: missing required DOM APIs');
24224
25550
  return;
24225
25551
  }
24226
25552
  this.initPageListeners();
@@ -24252,27 +25578,15 @@
24252
25578
  };
24253
25579
 
24254
25580
  Autocapture.prototype.currentUrlBlocked = function() {
24255
- var i;
24256
25581
  var currentUrl = _.info.currentUrl();
24257
25582
 
24258
25583
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
24259
25584
  if (allowUrlRegexes.length) {
24260
25585
  // we're using an allowlist, only track if current URL matches
24261
- var allowed = false;
24262
- for (i = 0; i < allowUrlRegexes.length; i++) {
24263
- var allowRegex = allowUrlRegexes[i];
24264
- try {
24265
- if (currentUrl.match(allowRegex)) {
24266
- allowed = true;
24267
- break;
24268
- }
24269
- } catch (err) {
24270
- logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
24271
- return true;
24272
- }
24273
- }
24274
- if (!allowed) {
24275
- // wasn't allowed by any regex
25586
+ try {
25587
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
25588
+ } catch (err) {
25589
+ logger$4.critical('Error while checking block URL regexes: ', err);
24276
25590
  return true;
24277
25591
  }
24278
25592
  }
@@ -24282,17 +25596,12 @@
24282
25596
  return false;
24283
25597
  }
24284
25598
 
24285
- for (i = 0; i < blockUrlRegexes.length; i++) {
24286
- try {
24287
- if (currentUrl.match(blockUrlRegexes[i])) {
24288
- return true;
24289
- }
24290
- } catch (err) {
24291
- logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
24292
- return true;
24293
- }
25599
+ try {
25600
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
25601
+ } catch (err) {
25602
+ logger$4.critical('Error while checking block URL regexes: ', err);
25603
+ return true;
24294
25604
  }
24295
- return false;
24296
25605
  };
24297
25606
 
24298
25607
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -24428,7 +25737,7 @@
24428
25737
  return;
24429
25738
  }
24430
25739
 
24431
- logger$3.log('Initializing scroll depth tracking');
25740
+ logger$4.log('Initializing scroll depth tracking');
24432
25741
 
24433
25742
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
24434
25743
 
@@ -24454,7 +25763,7 @@
24454
25763
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
24455
25764
  return;
24456
25765
  }
24457
- logger$3.log('Initializing click tracking');
25766
+ logger$4.log('Initializing click tracking');
24458
25767
 
24459
25768
  this.listenerClick = function(ev) {
24460
25769
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -24473,7 +25782,7 @@
24473
25782
  return;
24474
25783
  }
24475
25784
 
24476
- logger$3.log('Initializing dead click tracking');
25785
+ logger$4.log('Initializing dead click tracking');
24477
25786
  if (!this._deadClickTracker) {
24478
25787
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
24479
25788
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -24507,7 +25816,7 @@
24507
25816
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
24508
25817
  return;
24509
25818
  }
24510
- logger$3.log('Initializing input tracking');
25819
+ logger$4.log('Initializing input tracking');
24511
25820
 
24512
25821
  this.listenerChange = function(ev) {
24513
25822
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -24524,7 +25833,7 @@
24524
25833
  if (!this.pageviewTrackingConfig()) {
24525
25834
  return;
24526
25835
  }
24527
- logger$3.log('Initializing pageview tracking');
25836
+ logger$4.log('Initializing pageview tracking');
24528
25837
 
24529
25838
  var previousTrackedUrl = '';
24530
25839
  var tracked = false;
@@ -24559,7 +25868,7 @@
24559
25868
  }
24560
25869
  if (didPathChange) {
24561
25870
  this.lastScrollCheckpoint = 0;
24562
- logger$3.log('Path change: re-initializing scroll depth checkpoints');
25871
+ logger$4.log('Path change: re-initializing scroll depth checkpoints');
24563
25872
  }
24564
25873
  }
24565
25874
  }.bind(this));
@@ -24574,7 +25883,7 @@
24574
25883
  return;
24575
25884
  }
24576
25885
 
24577
- logger$3.log('Initializing rage click tracking');
25886
+ logger$4.log('Initializing rage click tracking');
24578
25887
  if (!this._rageClickTracker) {
24579
25888
  this._rageClickTracker = new RageClickTracker();
24580
25889
  }
@@ -24604,7 +25913,7 @@
24604
25913
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
24605
25914
  return;
24606
25915
  }
24607
- logger$3.log('Initializing scroll tracking');
25916
+ logger$4.log('Initializing scroll tracking');
24608
25917
  this.lastScrollCheckpoint = 0;
24609
25918
 
24610
25919
  var scrollTrackFunction = function() {
@@ -24641,7 +25950,7 @@
24641
25950
  }
24642
25951
  }
24643
25952
  } catch (err) {
24644
- logger$3.critical('Error while calculating scroll percentage', err);
25953
+ logger$4.critical('Error while calculating scroll percentage', err);
24645
25954
  }
24646
25955
  if (shouldTrack) {
24647
25956
  this.mp.track(MP_EV_SCROLL, props);
@@ -24659,7 +25968,7 @@
24659
25968
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
24660
25969
  return;
24661
25970
  }
24662
- logger$3.log('Initializing submit tracking');
25971
+ logger$4.log('Initializing submit tracking');
24663
25972
 
24664
25973
  this.listenerSubmit = function(ev) {
24665
25974
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -24681,7 +25990,7 @@
24681
25990
  return;
24682
25991
  }
24683
25992
 
24684
- logger$3.log('Initializing page visibility tracking.');
25993
+ logger$4.log('Initializing page visibility tracking.');
24685
25994
  this._initScrollDepthTracking();
24686
25995
  var previousTrackedUrl = _.info.currentUrl();
24687
25996
 
@@ -24736,14 +26045,62 @@
24736
26045
  // TODO integrate error_reporter from mixpanel instance
24737
26046
  safewrapClass(Autocapture);
24738
26047
 
24739
- var logger = console_with_prefix('flags');
26048
+ /**
26049
+ * Get the promise-based targeting loader
26050
+ * @param {Function} loadExtraBundle - Function to load external bundle (callback-based)
26051
+ * @param {string} targetingSrc - URL to targeting bundle
26052
+ * @returns {Promise} Promise that resolves with targeting library
26053
+ */
26054
+ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
26055
+ // Return existing promise if already initialized or loading
26056
+ if (win[TARGETING_GLOBAL_NAME] && typeof win[TARGETING_GLOBAL_NAME].then === 'function') {
26057
+ return win[TARGETING_GLOBAL_NAME];
26058
+ }
26059
+
26060
+ // Create loading promise and set it as the global immediately
26061
+ // This makes minified build behavior consistent with dev/CJS builds
26062
+ win[TARGETING_GLOBAL_NAME] = new Promise(function (resolve) {
26063
+ loadExtraBundle(targetingSrc, resolve);
26064
+ }).then(function () {
26065
+ var p = win[TARGETING_GLOBAL_NAME];
26066
+ if (p && typeof p.then === 'function') {
26067
+ return p;
26068
+ }
26069
+ throw new Error('targeting failed to load');
26070
+ }).catch(function (err) {
26071
+ delete win[TARGETING_GLOBAL_NAME];
26072
+ throw err;
26073
+ });
24740
26074
 
26075
+ return win[TARGETING_GLOBAL_NAME];
26076
+ };
26077
+
26078
+ var logger = console_with_prefix('flags');
24741
26079
  var FLAGS_CONFIG_KEY = 'flags';
24742
26080
 
24743
26081
  var CONFIG_CONTEXT = 'context';
24744
26082
  var CONFIG_DEFAULTS = {};
24745
26083
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
24746
26084
 
26085
+ /**
26086
+ * Generate a unique key for a pending first-time event
26087
+ * @param {string} flagKey - The flag key
26088
+ * @param {string} firstTimeEventHash - The first_time_event_hash from the pending event definition
26089
+ * @returns {string} Composite key in format "flagKey:firstTimeEventHash"
26090
+ */
26091
+ var getPendingEventKey = function(flagKey, firstTimeEventHash) {
26092
+ return flagKey + ':' + firstTimeEventHash;
26093
+ };
26094
+
26095
+ /**
26096
+ * Extract the flag key from a pending event key
26097
+ * @param {string} eventKey - The composite event key in format "flagKey:firstTimeEventHash"
26098
+ * @returns {string} The flag key portion
26099
+ */
26100
+ var getFlagKeyFromPendingEventKey = function(eventKey) {
26101
+ return eventKey.split(':')[0];
26102
+ };
26103
+
24747
26104
  /**
24748
26105
  * FeatureFlagManager: support for Mixpanel's feature flagging product
24749
26106
  * @constructor
@@ -24755,6 +26112,8 @@
24755
26112
  this.setMpConfig = initOptions.setConfigFunc;
24756
26113
  this.getMpProperty = initOptions.getPropertyFunc;
24757
26114
  this.track = initOptions.trackingFunc;
26115
+ this.loadExtraBundle = initOptions.loadExtraBundle || function() {};
26116
+ this.targetingSrc = initOptions.targetingSrc || '';
24758
26117
  };
24759
26118
 
24760
26119
  FeatureFlagManager.prototype.init = function() {
@@ -24767,6 +26126,8 @@
24767
26126
  this.fetchFlags();
24768
26127
 
24769
26128
  this.trackedFeatures = new Set();
26129
+ this.pendingFirstTimeEvents = {};
26130
+ this.activatedFirstTimeEvents = {};
24770
26131
  };
24771
26132
 
24772
26133
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -24847,17 +26208,78 @@
24847
26208
  throw new Error('No flags in API response');
24848
26209
  }
24849
26210
  var flags = new Map();
26211
+ var pendingFirstTimeEvents = {};
26212
+
26213
+ // Process flags from response
24850
26214
  _.each(responseFlags, function(data, key) {
24851
- flags.set(key, {
24852
- 'key': data['variant_key'],
24853
- 'value': data['variant_value'],
24854
- 'experiment_id': data['experiment_id'],
24855
- 'is_experiment_active': data['is_experiment_active'],
24856
- 'is_qa_tester': data['is_qa_tester']
26215
+ // Check if this flag has any activated first-time events this session
26216
+ var hasActivatedEvent = false;
26217
+ var prefix = key + ':';
26218
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
26219
+ if (eventKey.startsWith(prefix)) {
26220
+ hasActivatedEvent = true;
26221
+ }
24857
26222
  });
24858
- });
26223
+
26224
+ if (hasActivatedEvent) {
26225
+ // Preserve the activated variant, don't overwrite with server's current variant
26226
+ var currentFlag = this.flags && this.flags.get(key);
26227
+ if (currentFlag) {
26228
+ flags.set(key, currentFlag);
26229
+ }
26230
+ } else {
26231
+ // Use server's current variant
26232
+ flags.set(key, {
26233
+ 'key': data['variant_key'],
26234
+ 'value': data['variant_value'],
26235
+ 'experiment_id': data['experiment_id'],
26236
+ 'is_experiment_active': data['is_experiment_active'],
26237
+ 'is_qa_tester': data['is_qa_tester']
26238
+ });
26239
+ }
26240
+ }, this);
26241
+
26242
+ // Process top-level pending_first_time_events array
26243
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
26244
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
26245
+ _.each(topLevelDefinitions, function(def) {
26246
+ var flagKey = def['flag_key'];
26247
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
26248
+
26249
+ // Skip if this specific event has already been activated this session
26250
+ if (this.activatedFirstTimeEvents[eventKey]) {
26251
+ return;
26252
+ }
26253
+
26254
+ // Store pending event definition using composite key
26255
+ pendingFirstTimeEvents[eventKey] = {
26256
+ 'flag_key': flagKey,
26257
+ 'flag_id': def['flag_id'],
26258
+ 'project_id': def['project_id'],
26259
+ 'first_time_event_hash': def['first_time_event_hash'],
26260
+ 'event_name': def['event_name'],
26261
+ 'property_filters': def['property_filters'],
26262
+ 'pending_variant': def['pending_variant']
26263
+ };
26264
+ }, this);
26265
+ }
26266
+
26267
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
26268
+ if (this.activatedFirstTimeEvents) {
26269
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
26270
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
26271
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
26272
+ // Keep the activated flag even though it's not in the new response
26273
+ flags.set(flagKey, this.flags.get(flagKey));
26274
+ }
26275
+ }, this);
26276
+ }
26277
+
24859
26278
  this.flags = flags;
26279
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
24860
26280
  this._traceparent = traceparent;
26281
+
26282
+ this._loadTargetingIfNeeded();
24861
26283
  }.bind(this)).catch(function(error) {
24862
26284
  this.markFetchComplete();
24863
26285
  logger.error(error);
@@ -24870,15 +26292,186 @@
24870
26292
  return this.fetchPromise;
24871
26293
  };
24872
26294
 
24873
- FeatureFlagManager.prototype.markFetchComplete = function() {
24874
- if (!this._fetchInProgressStartTime) {
24875
- logger.error('Fetch in progress started time not set, cannot mark fetch complete');
24876
- return;
24877
- }
24878
- this._fetchStartTime = this._fetchInProgressStartTime;
24879
- this._fetchCompleteTime = Date.now();
24880
- this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
24881
- this._fetchInProgressStartTime = null;
26295
+ FeatureFlagManager.prototype.markFetchComplete = function() {
26296
+ if (!this._fetchInProgressStartTime) {
26297
+ logger.error('Fetch in progress started time not set, cannot mark fetch complete');
26298
+ return;
26299
+ }
26300
+ this._fetchStartTime = this._fetchInProgressStartTime;
26301
+ this._fetchCompleteTime = Date.now();
26302
+ this._fetchLatency = this._fetchCompleteTime - this._fetchStartTime;
26303
+ this._fetchInProgressStartTime = null;
26304
+ };
26305
+
26306
+ /**
26307
+ * Proactively load targeting bundle if any pending events have property filters
26308
+ */
26309
+ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
26310
+ var hasPropertyFilters = false;
26311
+ _.each(this.pendingFirstTimeEvents, function(evt) {
26312
+ if (evt['property_filters'] && !_.isEmptyObject(evt['property_filters'])) {
26313
+ hasPropertyFilters = true;
26314
+ }
26315
+ });
26316
+
26317
+ if (hasPropertyFilters) {
26318
+ this.getTargeting().then(function() {
26319
+ logger.log('targeting loaded for property filter evaluation');
26320
+ });
26321
+ }
26322
+ };
26323
+
26324
+ /**
26325
+ * Get the targeting library (initializes if not already loaded)
26326
+ * This method is primarily for testing - production code should rely on automatic loading
26327
+ * @returns {Promise} Promise that resolves with targeting library
26328
+ */
26329
+ FeatureFlagManager.prototype.getTargeting = function() {
26330
+ return getTargetingPromise(
26331
+ this.loadExtraBundle.bind(this),
26332
+ this.targetingSrc
26333
+ ).catch(function(error) {
26334
+ logger.error('Failed to load targeting: ' + error);
26335
+ }.bind(this));
26336
+ };
26337
+
26338
+ /**
26339
+ * Check if a tracked event matches any pending first-time events and activate the corresponding flag variant
26340
+ * @param {string} eventName - The name of the event being tracked
26341
+ * @param {Object} properties - Event properties to evaluate against property filters
26342
+ *
26343
+ * When a match is found (event name matches and property filters pass), this method:
26344
+ * - Switches the flag to the pending variant
26345
+ * - Marks the event as activated for this session
26346
+ * - Records the activation via the API (fire-and-forget)
26347
+ */
26348
+ FeatureFlagManager.prototype.checkFirstTimeEvents = function(eventName, properties) {
26349
+ if (!this.pendingFirstTimeEvents || _.isEmptyObject(this.pendingFirstTimeEvents)) {
26350
+ return;
26351
+ }
26352
+
26353
+ // Check if targeting promise exists (either bundled or async loaded)
26354
+ if (win[TARGETING_GLOBAL_NAME] && _.isFunction(win[TARGETING_GLOBAL_NAME].then)) {
26355
+ win[TARGETING_GLOBAL_NAME].then(function(library) {
26356
+ this._processFirstTimeEventCheck(eventName, properties, library);
26357
+ }.bind(this)).catch(function() {
26358
+ // If targeting failed to load, process with null
26359
+ // Events without property filters will still match
26360
+ this._processFirstTimeEventCheck(eventName, properties, null);
26361
+ }.bind(this));
26362
+ } else {
26363
+ // No targeting available, process with null
26364
+ // Events without property filters will still match
26365
+ this._processFirstTimeEventCheck(eventName, properties, null);
26366
+ }
26367
+ };
26368
+
26369
+ /**
26370
+ * Internal method to process first-time event checks with loaded targeting library
26371
+ * @param {string} eventName - The name of the event being tracked
26372
+ * @param {Object} properties - Event properties to evaluate against property filters
26373
+ * @param {Object} targeting - The loaded targeting library
26374
+ */
26375
+ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, properties, targeting) {
26376
+ _.each(this.pendingFirstTimeEvents, function(pendingEvent, eventKey) {
26377
+ if (this.activatedFirstTimeEvents[eventKey]) {
26378
+ return;
26379
+ }
26380
+
26381
+ var flagKey = pendingEvent['flag_key'];
26382
+
26383
+ // Use targeting module to check if event matches
26384
+ var matchResult;
26385
+
26386
+ // If no targeting library and event has property filters, skip it
26387
+ if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
26388
+ logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
26389
+ return;
26390
+ }
26391
+
26392
+ // For simple events (no property filters), just check event name
26393
+ if (!targeting) {
26394
+ matchResult = {
26395
+ matches: eventName === pendingEvent['event_name'],
26396
+ error: null
26397
+ };
26398
+ } else {
26399
+ var criteria = {
26400
+ 'event_name': pendingEvent['event_name'],
26401
+ 'property_filters': pendingEvent['property_filters']
26402
+ };
26403
+ matchResult = targeting['eventMatchesCriteria'](
26404
+ eventName,
26405
+ properties,
26406
+ criteria
26407
+ );
26408
+ }
26409
+
26410
+ if (matchResult.error) {
26411
+ logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
26412
+ return;
26413
+ }
26414
+
26415
+ if (!matchResult.matches) {
26416
+ return;
26417
+ }
26418
+
26419
+ logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
26420
+
26421
+ var newVariant = {
26422
+ 'key': pendingEvent['pending_variant']['variant_key'],
26423
+ 'value': pendingEvent['pending_variant']['variant_value'],
26424
+ 'experiment_id': pendingEvent['pending_variant']['experiment_id'],
26425
+ 'is_experiment_active': pendingEvent['pending_variant']['is_experiment_active']
26426
+ };
26427
+
26428
+ this.flags.set(flagKey, newVariant);
26429
+ this.activatedFirstTimeEvents[eventKey] = true;
26430
+
26431
+ this.recordFirstTimeEvent(
26432
+ pendingEvent['flag_id'],
26433
+ pendingEvent['project_id'],
26434
+ pendingEvent['first_time_event_hash']
26435
+ );
26436
+ }, this);
26437
+ };
26438
+
26439
+ FeatureFlagManager.prototype.getFirstTimeEventApiRoute = function(flagId) {
26440
+ // Construct URL: {api_host}/flags/{flagId}/first-time-events
26441
+ return this.getFullApiRoute() + '/' + flagId + '/first-time-events';
26442
+ };
26443
+
26444
+ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId, firstTimeEventHash) {
26445
+ var distinctId = this.getMpProperty('distinct_id');
26446
+ var traceparent = generateTraceparent();
26447
+
26448
+ // Build URL with query string parameters
26449
+ var searchParams = new URLSearchParams();
26450
+ searchParams.set('mp_lib', 'web');
26451
+ searchParams.set('$lib_version', Config.LIB_VERSION);
26452
+ var url = this.getFirstTimeEventApiRoute(flagId) + '?' + searchParams.toString();
26453
+
26454
+ var payload = {
26455
+ 'distinct_id': distinctId,
26456
+ 'project_id': projectId,
26457
+ 'first_time_event_hash': firstTimeEventHash
26458
+ };
26459
+
26460
+ logger.log('Recording first-time event for flag: ' + flagId);
26461
+
26462
+ // Fire-and-forget POST request
26463
+ this.fetch.call(win, url, {
26464
+ 'method': 'POST',
26465
+ 'headers': {
26466
+ 'Content-Type': 'application/json',
26467
+ 'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
26468
+ 'traceparent': traceparent
26469
+ },
26470
+ 'body': JSON.stringify(payload)
26471
+ }).catch(function(error) {
26472
+ // Silent failure - cohort sync will catch up
26473
+ logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26474
+ });
24882
26475
  };
24883
26476
 
24884
26477
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
@@ -24999,6 +26592,217 @@
24999
26592
  // Deprecated method
25000
26593
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
25001
26594
 
26595
+ // Exports intended only for testing
26596
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26597
+
26598
+ /* eslint camelcase: "off" */
26599
+
26600
+
26601
+ /**
26602
+ * RecorderManager: manages session recording initialization, lifecycle and state
26603
+ * @constructor
26604
+ */
26605
+ var RecorderManager = function(initOptions) {
26606
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
26607
+ // but ideally we should be able to remove this dependency.
26608
+ this.mixpanelInstance = initOptions.mixpanelInstance;
26609
+
26610
+ this.getMpConfig = initOptions.getConfigFunc;
26611
+ this.getTabId = initOptions.getTabIdFunc;
26612
+ this.reportError = initOptions.reportErrorFunc;
26613
+ this.getDistinctId = initOptions.getDistinctIdFunc;
26614
+ this.loadExtraBundle = initOptions.loadExtraBundle;
26615
+ this.recorderSrc = initOptions.recorderSrc;
26616
+ this.targetingSrc = initOptions.targetingSrc;
26617
+ this.libBasePath = initOptions.libBasePath;
26618
+
26619
+ this._recorder = null;
26620
+ };
26621
+
26622
+ RecorderManager.prototype.shouldLoadRecorder = function() {
26623
+ if (this.getMpConfig('disable_persistence')) {
26624
+ console$1.log('Load recorder check skipped due to disable_persistence config');
26625
+ return PromisePolyfill.resolve(false);
26626
+ }
26627
+
26628
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26629
+ var tab_id = this.getTabId();
26630
+ return recording_registry_idb.init()
26631
+ .then(function () {
26632
+ return recording_registry_idb.getAll();
26633
+ })
26634
+ .then(function (recordings) {
26635
+ for (var i = 0; i < recordings.length; i++) {
26636
+ // if there are expired recordings in the registry, we should load the recorder to flush them
26637
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
26638
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26639
+ return true;
26640
+ }
26641
+ }
26642
+ return false;
26643
+ })
26644
+ .catch(_.bind(function (err) {
26645
+ this.reportError('Error checking recording registry', err);
26646
+ return false;
26647
+ }, this));
26648
+ };
26649
+
26650
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
26651
+ if (!win['MutationObserver']) {
26652
+ console$1.critical('Browser does not support MutationObserver; skipping session recording');
26653
+ return PromisePolyfill.resolve();
26654
+ }
26655
+
26656
+ var loadRecorder = _.bind(function(startNewIfInactive) {
26657
+ return new PromisePolyfill(_.bind(function(resolve) {
26658
+ var handleLoadedRecorder = safewrap(_.bind(function() {
26659
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
26660
+ this._recorder['resumeRecording'](startNewIfInactive);
26661
+ resolve();
26662
+ }, this));
26663
+
26664
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
26665
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
26666
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
26667
+ } else {
26668
+ handleLoadedRecorder();
26669
+ }
26670
+ }, this));
26671
+ }, this);
26672
+
26673
+ /**
26674
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26675
+ * 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.
26676
+ */
26677
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
26678
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
26679
+ if (force_start || is_sampled) {
26680
+ return loadRecorder(true);
26681
+ } else {
26682
+ return this.shouldLoadRecorder()
26683
+ .then(_.bind(function (shouldLoad) {
26684
+ if (shouldLoad) {
26685
+ return loadRecorder(false);
26686
+ }
26687
+ return PromisePolyfill.resolve();
26688
+ }, this));
26689
+ }
26690
+ };
26691
+
26692
+ RecorderManager.prototype.isRecording = function() {
26693
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
26694
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
26695
+ return false;
26696
+ }
26697
+ try {
26698
+ return this._recorder['isRecording']();
26699
+ } catch (e) {
26700
+ this.reportError('Error checking if recording is active', e);
26701
+ return false;
26702
+ }
26703
+ };
26704
+
26705
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
26706
+ var isRecording = this.isRecording();
26707
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
26708
+
26709
+ if (!isRecording && recordingTriggerEvents) {
26710
+ var trigger = recordingTriggerEvents[event_name];
26711
+ if (trigger && typeof trigger['percentage'] === 'number') {
26712
+ var newRate = trigger['percentage'];
26713
+ var propertyFilters = trigger['property_filters'];
26714
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
26715
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
26716
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
26717
+ .then(function(targeting) {
26718
+ try {
26719
+ var result = targeting['eventMatchesCriteria'](
26720
+ event_name,
26721
+ properties,
26722
+ {
26723
+ 'event_name': event_name,
26724
+ 'property_filters': propertyFilters
26725
+ }
26726
+ );
26727
+ if (result['matches']) {
26728
+ this.checkAndStartSessionRecording(false, newRate);
26729
+ }
26730
+ } catch (err) {
26731
+ console$1.critical('Could not parse recording event trigger properties logic:', err);
26732
+ }
26733
+ }.bind(this)).catch(function(err) {
26734
+ console$1.critical('Failed to load targeting library:', err);
26735
+ });
26736
+ } else {
26737
+ this.checkAndStartSessionRecording(false, newRate);
26738
+ }
26739
+ }
26740
+ }
26741
+ };
26742
+
26743
+ RecorderManager.prototype.stopSessionRecording = function() {
26744
+ if (this._recorder) {
26745
+ return this._recorder['stopRecording']();
26746
+ }
26747
+ return PromisePolyfill.resolve();
26748
+ };
26749
+
26750
+ RecorderManager.prototype.pauseSessionRecording = function() {
26751
+ if (this._recorder) {
26752
+ return this._recorder['pauseRecording']();
26753
+ }
26754
+ return PromisePolyfill.resolve();
26755
+ };
26756
+
26757
+ RecorderManager.prototype.resumeSessionRecording = function() {
26758
+ if (this._recorder) {
26759
+ return this._recorder['resumeRecording']();
26760
+ }
26761
+ return PromisePolyfill.resolve();
26762
+ };
26763
+
26764
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
26765
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
26766
+ };
26767
+
26768
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
26769
+ var props = {};
26770
+ var replay_id = this.getSessionReplayId();
26771
+ if (replay_id) {
26772
+ props['$mp_replay_id'] = replay_id;
26773
+ }
26774
+ return props;
26775
+ };
26776
+
26777
+ RecorderManager.prototype.getSessionReplayUrl = function() {
26778
+ var replay_url = null;
26779
+ var replay_id = this.getSessionReplayId();
26780
+ if (replay_id) {
26781
+ var query_params = _.HTTPBuildQuery({
26782
+ 'replay_id': replay_id,
26783
+ 'distinct_id': this.getDistinctId(),
26784
+ 'token': this.getMpConfig('token')
26785
+ });
26786
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26787
+ }
26788
+ return replay_url;
26789
+ };
26790
+
26791
+ RecorderManager.prototype.getSessionReplayId = function() {
26792
+ var replay_id = null;
26793
+ if (this._recorder) {
26794
+ replay_id = this._recorder['replayId'];
26795
+ }
26796
+ return replay_id || null;
26797
+ };
26798
+
26799
+ // "private" public method to reach into the recorder in test cases
26800
+ RecorderManager.prototype.getRecorder = function() {
26801
+ return this._recorder;
26802
+ };
26803
+
26804
+ safewrapClass(RecorderManager);
26805
+
25002
26806
  /* eslint camelcase: "off" */
25003
26807
 
25004
26808
 
@@ -26462,12 +28266,17 @@
26462
28266
  'record_collect_fonts': false,
26463
28267
  'record_console': true,
26464
28268
  'record_heatmap_data': false,
28269
+ 'recording_event_triggers': {},
26465
28270
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
26466
28271
  'record_mask_inputs': true,
26467
28272
  'record_max_ms': MAX_RECORDING_MS,
26468
28273
  'record_min_ms': 0,
28274
+ 'record_network': false,
28275
+ 'record_network_options': {},
26469
28276
  'record_sessions_percent': 0,
26470
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
28277
+ 'recorder_src': null,
28278
+ 'targeting_src': null,
28279
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
26471
28280
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
26472
28281
  };
26473
28282
 
@@ -26621,6 +28430,19 @@
26621
28430
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
26622
28431
  }));
26623
28432
 
28433
+ this.recorderManager = new RecorderManager({
28434
+ mixpanelInstance: this,
28435
+ getConfigFunc: _.bind(this.get_config, this),
28436
+ setConfigFunc: _.bind(this.set_config, this),
28437
+ getTabIdFunc: _.bind(this.get_tab_id, this),
28438
+ reportErrorFunc: _.bind(this.report_error, this),
28439
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
28440
+ recorderSrc: this.get_config('recorder_src'),
28441
+ targetingSrc: this.get_config('targeting_src'),
28442
+ libBasePath: this.get_config('lib_base_path'),
28443
+ loadExtraBundle: load_extra_bundle
28444
+ });
28445
+
26624
28446
  this['_jsc'] = NOOP_FUNC;
26625
28447
 
26626
28448
  this.__dom_loaded_queue = [];
@@ -26697,7 +28519,9 @@
26697
28519
  getConfigFunc: _.bind(this.get_config, this),
26698
28520
  setConfigFunc: _.bind(this.set_config, this),
26699
28521
  getPropertyFunc: _.bind(this.get_property, this),
26700
- trackingFunc: _.bind(this.track, this)
28522
+ trackingFunc: _.bind(this.track, this),
28523
+ loadExtraBundle: load_extra_bundle,
28524
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
26701
28525
  });
26702
28526
  this.flags.init();
26703
28527
  this['flags'] = this.flags;
@@ -26710,11 +28534,11 @@
26710
28534
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
26711
28535
  var mode = this.get_config('remote_settings_mode');
26712
28536
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
26713
- this._fetch_remote_settings(mode).then(_.bind(function() {
26714
- this._check_and_start_session_recording();
28537
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
28538
+ return this._check_and_start_session_recording();
26715
28539
  }, this));
26716
28540
  } else {
26717
- this._check_and_start_session_recording();
28541
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
26718
28542
  }
26719
28543
  };
26720
28544
 
@@ -26758,132 +28582,50 @@
26758
28582
  return this.tab_id || null;
26759
28583
  };
26760
28584
 
26761
- MixpanelLib.prototype._should_load_recorder = function () {
26762
- if (this.get_config('disable_persistence')) {
26763
- console$1.log('Load recorder check skipped due to disable_persistence config');
26764
- return Promise.resolve(false);
26765
- }
26766
-
26767
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26768
- var tab_id = this.get_tab_id();
26769
- return recording_registry_idb.init()
26770
- .then(function () {
26771
- return recording_registry_idb.getAll();
26772
- })
26773
- .then(function (recordings) {
26774
- for (var i = 0; i < recordings.length; i++) {
26775
- // if there are expired recordings in the registry, we should load the recorder to flush them
26776
- // if there's a recording for this tab id, we should load the recorder to continue the recording
26777
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26778
- return true;
26779
- }
26780
- }
26781
- return false;
26782
- })
26783
- .catch(_.bind(function (err) {
26784
- this.report_error('Error checking recording registry', err);
26785
- }, this));
26786
- };
26787
-
26788
28585
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
26789
- if (!win['MutationObserver']) {
26790
- console$1.critical('Browser does not support MutationObserver; skipping session recording');
26791
- return;
26792
- }
26793
-
26794
- var loadRecorder = _.bind(function(startNewIfInactive) {
26795
- var handleLoadedRecorder = _.bind(function() {
26796
- this._recorder = this._recorder || new win['__mp_recorder'](this);
26797
- this._recorder['resumeRecording'](startNewIfInactive);
26798
- }, this);
26799
-
26800
- if (_.isUndefined(win['__mp_recorder'])) {
26801
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
26802
- } else {
26803
- handleLoadedRecorder();
26804
- }
26805
- }, this);
26806
-
26807
- /**
26808
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26809
- * 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.
26810
- */
26811
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
26812
- if (force_start || is_sampled) {
26813
- loadRecorder(true);
26814
- } else {
26815
- this._should_load_recorder()
26816
- .then(function (shouldLoad) {
26817
- if (shouldLoad) {
26818
- loadRecorder(false);
26819
- }
26820
- });
26821
- }
28586
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
26822
28587
  });
26823
28588
 
28589
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
28590
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
28591
+ };
28592
+
26824
28593
  MixpanelLib.prototype.start_session_recording = function () {
26825
- this._check_and_start_session_recording(true);
28594
+ return this._check_and_start_session_recording(true);
26826
28595
  };
26827
28596
 
26828
28597
  MixpanelLib.prototype.stop_session_recording = function () {
26829
- if (this._recorder) {
26830
- return this._recorder['stopRecording']();
26831
- }
26832
- return Promise.resolve();
28598
+ return this.recorderManager.stopSessionRecording();
26833
28599
  };
26834
28600
 
26835
28601
  MixpanelLib.prototype.pause_session_recording = function () {
26836
- if (this._recorder) {
26837
- return this._recorder['pauseRecording']();
26838
- }
26839
- return Promise.resolve();
28602
+ return this.recorderManager.pauseSessionRecording();
26840
28603
  };
26841
28604
 
26842
28605
  MixpanelLib.prototype.resume_session_recording = function () {
26843
- if (this._recorder) {
26844
- return this._recorder['resumeRecording']();
26845
- }
26846
- return Promise.resolve();
28606
+ return this.recorderManager.resumeSessionRecording();
26847
28607
  };
26848
28608
 
26849
28609
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
26850
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
28610
+ return this.recorderManager.isRecordingHeatmapData();
26851
28611
  };
26852
28612
 
26853
28613
  MixpanelLib.prototype.get_session_recording_properties = function () {
26854
- var props = {};
26855
- var replay_id = this._get_session_replay_id();
26856
- if (replay_id) {
26857
- props['$mp_replay_id'] = replay_id;
26858
- }
26859
- return props;
28614
+ return this.recorderManager.getSessionRecordingProperties();
26860
28615
  };
26861
28616
 
26862
28617
  MixpanelLib.prototype.get_session_replay_url = function () {
26863
- var replay_url = null;
26864
- var replay_id = this._get_session_replay_id();
26865
- if (replay_id) {
26866
- var query_params = _.HTTPBuildQuery({
26867
- 'replay_id': replay_id,
26868
- 'distinct_id': this.get_distinct_id(),
26869
- 'token': this.get_config('token')
26870
- });
26871
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26872
- }
26873
- return replay_url;
26874
- };
26875
-
26876
- MixpanelLib.prototype._get_session_replay_id = function () {
26877
- var replay_id = null;
26878
- if (this._recorder) {
26879
- replay_id = this._recorder['replayId'];
26880
- }
26881
- return replay_id || null;
28618
+ return this.recorderManager.getSessionReplayUrl();
26882
28619
  };
26883
28620
 
26884
28621
  // "private" public method to reach into the recorder in test cases
26885
28622
  MixpanelLib.prototype.__get_recorder = function () {
26886
- return this._recorder;
28623
+ return this.recorderManager.getRecorder();
28624
+ };
28625
+
28626
+ // "private" public method to get session recording init promise in test cases
28627
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
28628
+ return this.__session_recording_init_promise;
26887
28629
  };
26888
28630
 
26889
28631
  // Private methods
@@ -27141,6 +28883,7 @@
27141
28883
  };
27142
28884
 
27143
28885
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
28886
+ var self = this;
27144
28887
  var disableRecordingIfStrict = function() {
27145
28888
  if (mode === 'strict') {
27146
28889
  self.set_config({'record_sessions_percent': 0});
@@ -27161,7 +28904,6 @@
27161
28904
  };
27162
28905
  var query_string = _.HTTPBuildQuery(request_params);
27163
28906
  var full_url = settings_endpoint + '?' + query_string;
27164
- var self = this;
27165
28907
 
27166
28908
  var abortController = new AbortController();
27167
28909
  var timeout_id = setTimeout(function() {
@@ -27353,6 +29095,34 @@
27353
29095
  this._execute_array([item]);
27354
29096
  };
27355
29097
 
29098
+ /**
29099
+ * Enables events on the Mixpanel object. If passed no arguments,
29100
+ * this function enable tracking of all events. If passed an
29101
+ * array of event names, those events will be enabled, but other
29102
+ * existing disabled events will continue to be not tracked.
29103
+ *
29104
+ * @param {Array} [events] An array of event names to enable
29105
+ */
29106
+ MixpanelLib.prototype.enable = function(events) {
29107
+ var keys, new_disabled_events, i, j;
29108
+
29109
+ if (typeof(events) === 'undefined') {
29110
+ this._flags.disable_all_events = false;
29111
+ } else {
29112
+ keys = {};
29113
+ new_disabled_events = [];
29114
+ for (i = 0; i < events.length; i++) {
29115
+ keys[events[i]] = true;
29116
+ }
29117
+ for (j = 0; j < this.__disabled_events.length; j++) {
29118
+ if (!keys[this.__disabled_events[j]]) {
29119
+ new_disabled_events.push(this.__disabled_events[j]);
29120
+ }
29121
+ }
29122
+ this.__disabled_events = new_disabled_events;
29123
+ }
29124
+ };
29125
+
27356
29126
  /**
27357
29127
  * Disable events on the Mixpanel object. If passed no arguments,
27358
29128
  * this function disables tracking of any event. If passed an
@@ -27526,6 +29296,8 @@
27526
29296
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
27527
29297
  }
27528
29298
 
29299
+ this._start_recording_on_event(event_name, properties);
29300
+
27529
29301
  var data = {
27530
29302
  'event': event_name,
27531
29303
  'properties': properties
@@ -27539,6 +29311,11 @@
27539
29311
  send_request_options: options
27540
29312
  }, callback);
27541
29313
 
29314
+ // Check for first-time event matches
29315
+ if (this.flags && this.flags.checkFirstTimeEvents) {
29316
+ this.flags.checkFirstTimeEvents(event_name, properties);
29317
+ }
29318
+
27542
29319
  return ret;
27543
29320
  });
27544
29321
 
@@ -28729,6 +30506,7 @@
28729
30506
  // MixpanelLib Exports
28730
30507
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
28731
30508
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
30509
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
28732
30510
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
28733
30511
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
28734
30512
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -28772,6 +30550,7 @@
28772
30550
 
28773
30551
  // Exports intended only for testing
28774
30552
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
30553
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
28775
30554
 
28776
30555
  // MixpanelPersistence Exports
28777
30556
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;