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