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