mixpanel-browser 2.75.0 → 2.77.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 (62) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/dependabot.yml +8 -0
  3. package/.github/workflows/integration-tests.yml +4 -4
  4. package/.github/workflows/unit-tests.yml +4 -4
  5. package/CHANGELOG.md +14 -0
  6. package/build.sh +10 -8
  7. package/dist/async-modules/mixpanel-recorder-DLKbUIEE.js +23669 -0
  8. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js +2 -0
  9. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +1 -0
  10. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +2 -0
  11. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +1 -0
  12. package/dist/async-modules/mixpanel-targeting-CmVvUyFM.js +2520 -0
  13. package/dist/mixpanel-core.cjs.d.ts +70 -1
  14. package/dist/mixpanel-core.cjs.js +724 -426
  15. package/dist/mixpanel-recorder.js +791 -41
  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 +6 -62
  19. package/dist/mixpanel-targeting.min.js +1 -1
  20. package/dist/mixpanel-targeting.min.js.map +1 -1
  21. package/dist/mixpanel-with-async-modules.cjs.d.ts +70 -1
  22. package/dist/mixpanel-with-async-modules.cjs.js +724 -426
  23. package/dist/mixpanel-with-async-recorder.cjs.d.ts +70 -1
  24. package/dist/mixpanel-with-async-recorder.cjs.js +724 -426
  25. package/dist/mixpanel-with-recorder.d.ts +70 -1
  26. package/dist/mixpanel-with-recorder.js +1471 -450
  27. package/dist/mixpanel-with-recorder.min.d.ts +70 -1
  28. package/dist/mixpanel-with-recorder.min.js +1 -1
  29. package/dist/mixpanel.amd.d.ts +70 -1
  30. package/dist/mixpanel.amd.js +1473 -504
  31. package/dist/mixpanel.cjs.d.ts +70 -1
  32. package/dist/mixpanel.cjs.js +1473 -504
  33. package/dist/mixpanel.globals.js +724 -426
  34. package/dist/mixpanel.min.js +189 -182
  35. package/dist/mixpanel.module.d.ts +70 -1
  36. package/dist/mixpanel.module.js +1473 -504
  37. package/dist/mixpanel.umd.d.ts +70 -1
  38. package/dist/mixpanel.umd.js +1473 -504
  39. package/dist/rrweb-bundled.js +61 -9
  40. package/dist/rrweb-compiled.js +56 -9
  41. package/logo.svg +5 -0
  42. package/package.json +6 -4
  43. package/rollup.config.mjs +163 -46
  44. package/src/autocapture/index.js +10 -27
  45. package/src/config.js +9 -3
  46. package/src/flags/index.js +1 -2
  47. package/src/index.d.ts +70 -1
  48. package/src/mixpanel-core.js +77 -112
  49. package/src/recorder/index.js +1 -1
  50. package/src/recorder/recorder.js +5 -1
  51. package/src/recorder/rrweb-network-plugin.js +649 -0
  52. package/src/recorder/session-recording.js +36 -12
  53. package/src/recorder/utils.js +27 -1
  54. package/src/recorder-manager.js +324 -0
  55. package/src/request-batcher.js +1 -1
  56. package/src/targeting/event-matcher.js +2 -57
  57. package/src/targeting/index.js +1 -1
  58. package/src/targeting/loader.js +1 -1
  59. package/src/utils.js +13 -1
  60. package/testServer.js +69 -1
  61. package/src/globals.js +0 -14
  62. /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
@@ -23,16 +23,19 @@ if (typeof(window) === 'undefined') {
23
23
  win = window;
24
24
  }
25
25
 
26
- /**
27
- * Shared global window property names used across modules
28
- */
26
+ var Config = {
27
+ DEBUG: false,
28
+ LIB_VERSION: '2.77.0'
29
+ };
29
30
 
30
- // Targeting library global (used by flags and targeting modules)
31
+ // Window global names for async modules
31
32
  var TARGETING_GLOBAL_NAME = '__mp_targeting';
32
-
33
- // Recorder library global (used by recorder and mixpanel-core)
34
33
  var RECORDER_GLOBAL_NAME = '__mp_recorder';
35
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
+
36
39
  function _array_like_to_array(arr, len) {
37
40
  if (len == null || len > arr.length) len = arr.length;
38
41
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -10730,13 +10733,7 @@ var MutationBuffer = /*#__PURE__*/ function() {
10730
10733
  };
10731
10734
  while(_this.mapRemoves.length){
10732
10735
  var removedNode = _this.mapRemoves.shift();
10733
- if (removedNode.nodeName === "IFRAME") {
10734
- try {
10735
- _this.iframeManager.removeIframe(removedNode);
10736
- } catch (e2) {}
10737
- } else {
10738
- _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10739
- }
10736
+ _this.cleanupRemovedNode(removedNode);
10740
10737
  _this.mirror.removeNodeFromMap(removedNode);
10741
10738
  }
10742
10739
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
@@ -11056,6 +11053,20 @@ var MutationBuffer = /*#__PURE__*/ function() {
11056
11053
  }
11057
11054
  }
11058
11055
  });
11056
+ __publicField$1(this, "cleanupRemovedNode", function(node2) {
11057
+ if (node2.nodeName === "IFRAME") {
11058
+ try {
11059
+ _this.iframeManager.removeIframe(node2);
11060
+ } catch (e2) {}
11061
+ } else {
11062
+ try {
11063
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
11064
+ } catch (e2) {}
11065
+ }
11066
+ node2.childNodes.forEach(function(child) {
11067
+ _this.cleanupRemovedNode(child);
11068
+ });
11069
+ });
11059
11070
  }
11060
11071
  var _proto = MutationBuffer.prototype;
11061
11072
  _proto.init = function init(options) {
@@ -13283,6 +13294,31 @@ var ProcessedNodeManager = /*#__PURE__*/ function() {
13283
13294
  _proto.destroy = function destroy() {};
13284
13295
  return ProcessedNodeManager;
13285
13296
  }();
13297
+ function toOrigin(url) {
13298
+ try {
13299
+ var origin = new URL(url).origin;
13300
+ return origin !== "null" ? origin : null;
13301
+ } catch (e) {
13302
+ return null;
13303
+ }
13304
+ }
13305
+ function buildAllowedOriginSet(origins) {
13306
+ if (!Array.isArray(origins) || origins.length === 0) {
13307
+ throw new Error("[rrweb] allowedIframeOrigins must be a non-empty array of origin strings.");
13308
+ }
13309
+ var set = /* @__PURE__ */ new Set();
13310
+ for(var i2 = 0; i2 < origins.length; i2++){
13311
+ var entry = origins[i2];
13312
+ if (typeof entry !== "string") {
13313
+ throw new Error("[rrweb] allowedIframeOrigins[" + i2 + "] must be a string, got " + (typeof entry === "undefined" ? "undefined" : _type_of(entry)) + ".");
13314
+ }
13315
+ var origin = toOrigin(entry);
13316
+ if (origin) {
13317
+ set.add(origin);
13318
+ }
13319
+ }
13320
+ return Object.freeze(set);
13321
+ }
13286
13322
  var wrappedEmit;
13287
13323
  var takeFullSnapshot$1;
13288
13324
  var canvasManager;
@@ -13304,10 +13340,17 @@ try {
13304
13340
  var mirror = createMirror$2();
13305
13341
  function record(options) {
13306
13342
  if (options === void 0) options = {};
13307
- var emit = options.emit, checkoutEveryNms = options.checkoutEveryNms, checkoutEveryNth = options.checkoutEveryNth, _options_blockClass = options.blockClass, blockClass = _options_blockClass === void 0 ? "rr-block" : _options_blockClass, _options_blockSelector = options.blockSelector, blockSelector = _options_blockSelector === void 0 ? null : _options_blockSelector, _options_ignoreClass = options.ignoreClass, ignoreClass = _options_ignoreClass === void 0 ? "rr-ignore" : _options_ignoreClass, _options_ignoreSelector = options.ignoreSelector, ignoreSelector = _options_ignoreSelector === void 0 ? null : _options_ignoreSelector, _options_maskTextClass = options.maskTextClass, maskTextClass = _options_maskTextClass === void 0 ? "rr-mask" : _options_maskTextClass, _options_maskTextSelector = options.maskTextSelector, maskTextSelector = _options_maskTextSelector === void 0 ? null : _options_maskTextSelector, _options_inlineStylesheet = options.inlineStylesheet, inlineStylesheet = _options_inlineStylesheet === void 0 ? true : _options_inlineStylesheet, maskAllInputs = options.maskAllInputs, _maskInputOptions = options.maskInputOptions, _slimDOMOptions = options.slimDOMOptions, maskInputFn = options.maskInputFn, maskTextFn = options.maskTextFn, hooks = options.hooks, packFn = options.packFn, _options_sampling = options.sampling, sampling = _options_sampling === void 0 ? {} : _options_sampling, _options_dataURLOptions = options.dataURLOptions, dataURLOptions = _options_dataURLOptions === void 0 ? {} : _options_dataURLOptions, mousemoveWait = options.mousemoveWait, _options_recordDOM = options.recordDOM, recordDOM = _options_recordDOM === void 0 ? true : _options_recordDOM, _options_recordCanvas = options.recordCanvas, recordCanvas = _options_recordCanvas === void 0 ? false : _options_recordCanvas, _options_recordCrossOriginIframes = options.recordCrossOriginIframes, recordCrossOriginIframes = _options_recordCrossOriginIframes === void 0 ? false : _options_recordCrossOriginIframes, _options_recordAfter = options.recordAfter, recordAfter = _options_recordAfter === void 0 ? options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load" : _options_recordAfter, _options_userTriggeredOnInput = options.userTriggeredOnInput, userTriggeredOnInput = _options_userTriggeredOnInput === void 0 ? false : _options_userTriggeredOnInput, _options_collectFonts = options.collectFonts, collectFonts = _options_collectFonts === void 0 ? false : _options_collectFonts, _options_inlineImages = options.inlineImages, inlineImages = _options_inlineImages === void 0 ? false : _options_inlineImages, plugins = options.plugins, _options_keepIframeSrcFn = options.keepIframeSrcFn, keepIframeSrcFn = _options_keepIframeSrcFn === void 0 ? function() {
13343
+ var emit = options.emit, checkoutEveryNms = options.checkoutEveryNms, checkoutEveryNth = options.checkoutEveryNth, _options_blockClass = options.blockClass, blockClass = _options_blockClass === void 0 ? "rr-block" : _options_blockClass, _options_blockSelector = options.blockSelector, blockSelector = _options_blockSelector === void 0 ? null : _options_blockSelector, _options_ignoreClass = options.ignoreClass, ignoreClass = _options_ignoreClass === void 0 ? "rr-ignore" : _options_ignoreClass, _options_ignoreSelector = options.ignoreSelector, ignoreSelector = _options_ignoreSelector === void 0 ? null : _options_ignoreSelector, _options_maskTextClass = options.maskTextClass, maskTextClass = _options_maskTextClass === void 0 ? "rr-mask" : _options_maskTextClass, _options_maskTextSelector = options.maskTextSelector, maskTextSelector = _options_maskTextSelector === void 0 ? null : _options_maskTextSelector, _options_inlineStylesheet = options.inlineStylesheet, inlineStylesheet = _options_inlineStylesheet === void 0 ? true : _options_inlineStylesheet, maskAllInputs = options.maskAllInputs, _maskInputOptions = options.maskInputOptions, _slimDOMOptions = options.slimDOMOptions, maskInputFn = options.maskInputFn, maskTextFn = options.maskTextFn, hooks = options.hooks, packFn = options.packFn, _options_sampling = options.sampling, sampling = _options_sampling === void 0 ? {} : _options_sampling, _options_dataURLOptions = options.dataURLOptions, dataURLOptions = _options_dataURLOptions === void 0 ? {} : _options_dataURLOptions, mousemoveWait = options.mousemoveWait, _options_recordDOM = options.recordDOM, recordDOM = _options_recordDOM === void 0 ? true : _options_recordDOM, _options_recordCanvas = options.recordCanvas, recordCanvas = _options_recordCanvas === void 0 ? false : _options_recordCanvas, _options_recordCrossOriginIframes = options.recordCrossOriginIframes, recordCrossOriginIframes = _options_recordCrossOriginIframes === void 0 ? false : _options_recordCrossOriginIframes, allowedIframeOrigins = options.allowedIframeOrigins, _options_recordAfter = options.recordAfter, recordAfter = _options_recordAfter === void 0 ? options.recordAfter === "DOMContentLoaded" ? options.recordAfter : "load" : _options_recordAfter, _options_userTriggeredOnInput = options.userTriggeredOnInput, userTriggeredOnInput = _options_userTriggeredOnInput === void 0 ? false : _options_userTriggeredOnInput, _options_collectFonts = options.collectFonts, collectFonts = _options_collectFonts === void 0 ? false : _options_collectFonts, _options_inlineImages = options.inlineImages, inlineImages = _options_inlineImages === void 0 ? false : _options_inlineImages, plugins = options.plugins, _options_keepIframeSrcFn = options.keepIframeSrcFn, keepIframeSrcFn = _options_keepIframeSrcFn === void 0 ? function() {
13308
13344
  return false;
13309
13345
  } : _options_keepIframeSrcFn, _options_ignoreCSSAttributes = options.ignoreCSSAttributes, ignoreCSSAttributes = _options_ignoreCSSAttributes === void 0 ? /* @__PURE__ */ new Set([]) : _options_ignoreCSSAttributes, errorHandler2 = options.errorHandler;
13310
13346
  registerErrorHandler(errorHandler2);
13347
+ var validatedOrigins;
13348
+ if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
13349
+ validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
13350
+ if (validatedOrigins.size === 0) {
13351
+ validatedOrigins = void 0;
13352
+ }
13353
+ }
13311
13354
  var inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
13312
13355
  var passEmitsToParent = false;
13313
13356
  if (!inEmittingFrame) {
@@ -13399,7 +13442,14 @@ function record(options) {
13399
13442
  origin: window.location.origin,
13400
13443
  isCheckout: isCheckout
13401
13444
  };
13402
- window.parent.postMessage(message, "*");
13445
+ if (validatedOrigins) {
13446
+ for(var _iterator = _create_for_of_iterator_helper_loose(validatedOrigins), _step; !(_step = _iterator()).done;){
13447
+ var targetOrigin = _step.value;
13448
+ window.parent.postMessage(message, targetOrigin);
13449
+ }
13450
+ } else {
13451
+ window.parent.postMessage(message, "*");
13452
+ }
13403
13453
  }
13404
13454
  if (e2.type === EventType.FullSnapshot) {
13405
13455
  lastFullSnapshotEvent = e2;
@@ -18132,7 +18182,7 @@ var __defNormalProp = function(obj, key, value) {
18132
18182
  var __publicField = function(obj, key, value) {
18133
18183
  return __defNormalProp(obj, (typeof key === "undefined" ? "undefined" : _type_of(key)) !== "symbol" ? key + "" : key, value);
18134
18184
  };
18135
- function patch(source, name, replacement) {
18185
+ function patch$3(source, name, replacement) {
18136
18186
  try {
18137
18187
  if (!(name in source)) {
18138
18188
  return function() {};
@@ -18549,7 +18599,7 @@ function initLogObserver(cb, win, options) {
18549
18599
  if (!_logger[level]) {
18550
18600
  return function() {};
18551
18601
  }
18552
- return patch(_logger, level, function(original) {
18602
+ return patch$3(_logger, level, function(original) {
18553
18603
  var _this1 = _this;
18554
18604
  return function() {
18555
18605
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
@@ -18970,11 +19020,6 @@ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]'
18970
19020
  PromisePolyfill = NpoPromise;
18971
19021
  }
18972
19022
 
18973
- var Config = {
18974
- DEBUG: false,
18975
- LIB_VERSION: '2.75.0'
18976
- };
18977
-
18978
19023
  /* eslint camelcase: "off", eqeqeq: "off" */
18979
19024
 
18980
19025
  // Maximum allowed session recording length
@@ -20706,6 +20751,17 @@ var isOnline = function() {
20706
20751
 
20707
20752
  var NOOP_FUNC = function () {};
20708
20753
 
20754
+ var urlMatchesRegexList = function (url, regexList) {
20755
+ var matches = false;
20756
+ for (var i = 0; i < regexList.length; i++) {
20757
+ if (url.match(regexList[i])) {
20758
+ matches = true;
20759
+ break;
20760
+ }
20761
+ }
20762
+ return matches;
20763
+ };
20764
+
20709
20765
  var JSONStringify = null, JSONParse = null;
20710
20766
  if (typeof JSON !== 'undefined') {
20711
20767
  JSONStringify = JSON.stringify;
@@ -21177,7 +21233,7 @@ function _addOptOutCheck(method, getConfigValue) {
21177
21233
  };
21178
21234
  }
21179
21235
 
21180
- var logger$6 = console_with_prefix('lock');
21236
+ var logger$8 = console_with_prefix('lock');
21181
21237
 
21182
21238
  /**
21183
21239
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -21229,7 +21285,7 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
21229
21285
 
21230
21286
  var delay = function(cb) {
21231
21287
  if (new Date().getTime() - startTime > timeoutMS) {
21232
- logger$6.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21288
+ logger$8.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21233
21289
  storage.removeItem(keyZ);
21234
21290
  storage.removeItem(keyY);
21235
21291
  loop();
@@ -21376,7 +21432,7 @@ LocalStorageWrapper.prototype.removeItem = function (key) {
21376
21432
  }, this));
21377
21433
  };
21378
21434
 
21379
- var logger$5 = console_with_prefix('batch');
21435
+ var logger$7 = console_with_prefix('batch');
21380
21436
 
21381
21437
  /**
21382
21438
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -21405,7 +21461,7 @@ var RequestQueue = function (storageKey, options) {
21405
21461
  timeoutMS: options.sharedLockTimeoutMS,
21406
21462
  });
21407
21463
  }
21408
- this.reportError = options.errorReporter || _.bind(logger$5.error, logger$5);
21464
+ this.reportError = options.errorReporter || _.bind(logger$7.error, logger$7);
21409
21465
 
21410
21466
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
21411
21467
 
@@ -21738,7 +21794,7 @@ RequestQueue.prototype.clear = function () {
21738
21794
  // maximum interval between request retries after exponential backoff
21739
21795
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
21740
21796
 
21741
- var logger$4 = console_with_prefix('batch');
21797
+ var logger$6 = console_with_prefix('batch');
21742
21798
 
21743
21799
  /**
21744
21800
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -21866,7 +21922,7 @@ RequestBatcher.prototype.sendRequestPromise = function(data, options) {
21866
21922
  */
21867
21923
  RequestBatcher.prototype.flush = function(options) {
21868
21924
  if (this.requestInProgress) {
21869
- logger$4.log('Flush: Request already in progress');
21925
+ logger$6.log('Flush: Request already in progress');
21870
21926
  return PromisePolyfill.resolve();
21871
21927
  }
21872
21928
 
@@ -22043,7 +22099,7 @@ RequestBatcher.prototype.flush = function(options) {
22043
22099
  if (options.unloading) {
22044
22100
  requestOptions.transport = 'sendBeacon';
22045
22101
  }
22046
- logger$4.log('MIXPANEL REQUEST:', dataForRequest);
22102
+ logger$6.log('MIXPANEL REQUEST:', dataForRequest);
22047
22103
  return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
22048
22104
  }, this))
22049
22105
  .catch(_.bind(function(err) {
@@ -22056,7 +22112,7 @@ RequestBatcher.prototype.flush = function(options) {
22056
22112
  * Log error to global logger and optional user-defined logger.
22057
22113
  */
22058
22114
  RequestBatcher.prototype.reportError = function(msg, err) {
22059
- logger$4.error.apply(logger$4.error, arguments);
22115
+ logger$6.error.apply(logger$6.error, arguments);
22060
22116
  if (this.errorReporter) {
22061
22117
  try {
22062
22118
  if (!(err instanceof Error)) {
@@ -22064,7 +22120,7 @@ RequestBatcher.prototype.reportError = function(msg, err) {
22064
22120
  }
22065
22121
  this.errorReporter(msg, err);
22066
22122
  } catch(err) {
22067
- logger$4.error(err);
22123
+ logger$6.error(err);
22068
22124
  }
22069
22125
  }
22070
22126
  };
@@ -22081,6 +22137,29 @@ var isRecordingExpired = function(serializedRecording) {
22081
22137
 
22082
22138
  var RECORD_ENQUEUE_THROTTLE_MS = 250;
22083
22139
 
22140
+ var validateAllowedOrigins = function(origins, logger) {
22141
+ if (!_.isArray(origins)) {
22142
+ if (origins) {
22143
+ logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
22144
+ }
22145
+ return [];
22146
+ }
22147
+ var valid = [];
22148
+ for (var i = 0; i < origins.length; i++) {
22149
+ try {
22150
+ var origin = new URL(origins[i]).origin;
22151
+ if (origin === 'null') {
22152
+ logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
22153
+ continue;
22154
+ }
22155
+ valid.push(origin);
22156
+ } catch (e) {
22157
+ logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
22158
+ }
22159
+ }
22160
+ return valid;
22161
+ };
22162
+
22084
22163
  // stateless utils
22085
22164
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
22086
22165
 
@@ -22186,7 +22265,7 @@ var EVENT_HANDLER_ATTRIBUTES = [
22186
22265
 
22187
22266
  var MAX_DEPTH = 5;
22188
22267
 
22189
- var logger$3 = console_with_prefix('autocapture');
22268
+ var logger$5 = console_with_prefix('autocapture');
22190
22269
 
22191
22270
 
22192
22271
  function getClasses(el) {
@@ -22450,7 +22529,7 @@ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
22450
22529
  return false;
22451
22530
  }
22452
22531
  } catch (err) {
22453
- logger$3.critical('Error while checking element in allowElementCallback', err);
22532
+ logger$5.critical('Error while checking element in allowElementCallback', err);
22454
22533
  return false;
22455
22534
  }
22456
22535
  }
@@ -22467,7 +22546,7 @@ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
22467
22546
  return true;
22468
22547
  }
22469
22548
  } catch (err) {
22470
- logger$3.critical('Error while checking selector: ' + sel, err);
22549
+ logger$5.critical('Error while checking selector: ' + sel, err);
22471
22550
  }
22472
22551
  }
22473
22552
  return false;
@@ -22482,7 +22561,7 @@ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
22482
22561
  return true;
22483
22562
  }
22484
22563
  } catch (err) {
22485
- logger$3.critical('Error while checking element in blockElementCallback', err);
22564
+ logger$5.critical('Error while checking element in blockElementCallback', err);
22486
22565
  return true;
22487
22566
  }
22488
22567
  }
@@ -22496,7 +22575,7 @@ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
22496
22575
  return true;
22497
22576
  }
22498
22577
  } catch (err) {
22499
- logger$3.critical('Error while checking selector: ' + sel, err);
22578
+ logger$5.critical('Error while checking selector: ' + sel, err);
22500
22579
  }
22501
22580
  }
22502
22581
  }
@@ -23044,177 +23123,826 @@ function shouldMaskText(element, privacyConfig) {
23044
23123
  }
23045
23124
 
23046
23125
  /**
23047
- * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23126
+ * This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
23127
+ * the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
23128
+ *
23129
+ * This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
23130
+ * header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
23131
+ *
23048
23132
  */
23049
23133
 
23134
+ var logger$4 = console_with_prefix('network-plugin');
23050
23135
 
23051
- var logger$2 = console_with_prefix('recorder');
23052
- var CompressionStream = win['CompressionStream'];
23053
-
23054
- var RECORDER_BATCHER_LIB_CONFIG = {
23055
- 'batch_size': 1000,
23056
- 'batch_flush_interval_ms': 10 * 1000,
23057
- 'batch_request_timeout_ms': 90 * 1000,
23058
- 'batch_autostart': true
23059
- };
23136
+ /**
23137
+ * Get the time origin for converting performance timestamps to absolute timestamps.
23138
+ * Uses Date.now() - performance.now() instead of performance.timeOrigin because
23139
+ * browsers can report timeOrigin values that are skewed from actual time, and some
23140
+ * browsers (notably older Safari versions) don't implement timeOrigin at all.
23141
+ * See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
23142
+ * @param {Window} win
23143
+ * @returns {number}
23144
+ */
23145
+ function getTimeOrigin(win) {
23146
+ return Math.round(Date.now() - win.performance.now());
23147
+ }
23060
23148
 
23061
- var ACTIVE_SOURCES = new Set([
23062
- IncrementalSource.MouseMove,
23063
- IncrementalSource.MouseInteraction,
23064
- IncrementalSource.Scroll,
23065
- IncrementalSource.ViewportResize,
23066
- IncrementalSource.Input,
23067
- IncrementalSource.TouchMove,
23068
- IncrementalSource.MediaInteraction,
23069
- IncrementalSource.Drag,
23070
- IncrementalSource.Selection,
23071
- ]);
23149
+ /**
23150
+ * @typedef {import('../index.d.ts').InitiatorType} InitiatorType
23151
+ * @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
23152
+ * @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
23153
+ * @typedef {import('../index.d.ts').NetworkData} NetworkData
23154
+ */
23072
23155
 
23073
- function isUserEvent(ev) {
23074
- return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23075
- }
23156
+ /**
23157
+ * @typedef {Record<string, string>} Headers
23158
+ */
23076
23159
 
23077
23160
  /**
23078
- * @typedef {Object} SerializedRecording
23079
- * @property {number} idleExpires
23080
- * @property {number} maxExpires
23081
- * @property {number} replayStartTime
23082
- * @property {number} lastEventTimestamp
23083
- * @property {number} seqNo
23084
- * @property {string} batchStartUrl
23085
- * @property {string} replayId
23086
- * @property {string} tabId
23087
- * @property {string} replayStartUrl
23161
+ * @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
23088
23162
  */
23089
23163
 
23090
23164
  /**
23091
- * @typedef {Object} SessionRecordingOptions
23092
- * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23093
- * @property {String} [options.replayId] - unique uuid for a single replay
23094
- * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23095
- * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23096
- * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23097
- * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23098
- * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23099
- * optional properties for deserialization:
23100
- * @property {number} idleExpires
23101
- * @property {number} maxExpires
23102
- * @property {number} replayStartTime
23103
- * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23104
- * @property {number} seqNo
23105
- * @property {string} batchStartUrl
23106
- * @property {string} replayStartUrl
23165
+ * @callback networkCallback
23166
+ * @param {NetworkData} data
23167
+ * @returns {void}
23107
23168
  */
23108
23169
 
23109
23170
  /**
23110
- * @typedef {Object} UserIdInfo
23111
- * @property {string} distinct_id
23112
- * @property {string} user_id
23113
- * @property {string} device_id
23171
+ * @callback listenerHandler
23172
+ * @returns {void}
23114
23173
  */
23115
23174
 
23175
+ /**
23176
+ * @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
23177
+ */
23116
23178
 
23117
23179
  /**
23118
- * This class encapsulates a single session recording and its lifecycle.
23119
- * @param {SessionRecordingOptions} options
23180
+ * @typedef {Object} RecordPlugin
23181
+ * @property {string} name
23182
+ * @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
23183
+ * @property {NetworkRecordOptions} [options]
23120
23184
  */
23121
- var SessionRecording = function(options) {
23122
- this._mixpanel = options.mixpanelInstance;
23123
- this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23124
- this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23125
- this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23126
- this._rrwebRecord = options.rrwebRecord || null;
23127
23185
 
23128
- // internal rrweb stopRecording function
23129
- this._stopRecording = null;
23130
- this.replayId = options.replayId;
23186
+ /** @type {Required<NetworkRecordOptions>} */
23187
+ var defaultNetworkOptions = {
23188
+ initiatorTypes: [
23189
+ 'audio',
23190
+ 'beacon',
23191
+ 'body',
23192
+ 'css',
23193
+ 'early-hint',
23194
+ 'embed',
23195
+ 'fetch',
23196
+ 'frame',
23197
+ 'iframe',
23198
+ 'icon',
23199
+ 'image',
23200
+ 'img',
23201
+ 'input',
23202
+ 'link',
23203
+ 'navigation',
23204
+ 'object',
23205
+ 'ping',
23206
+ 'script',
23207
+ 'track',
23208
+ 'video',
23209
+ 'xmlhttprequest',
23210
+ ],
23211
+ ignoreRequestFn: function() { return false; },
23212
+ recordHeaders: {
23213
+ request: [],
23214
+ response: [],
23215
+ },
23216
+ recordBodyUrls: {
23217
+ request: [],
23218
+ response: [],
23219
+ },
23220
+ recordInitialRequests: false,
23221
+ };
23131
23222
 
23132
- this.batchStartUrl = options.batchStartUrl || null;
23133
- this.replayStartUrl = options.replayStartUrl || null;
23134
- this.idleExpires = options.idleExpires || null;
23135
- this.maxExpires = options.maxExpires || null;
23136
- this.replayStartTime = options.replayStartTime || null;
23137
- this.lastEventTimestamp = options.lastEventTimestamp || null;
23138
- this.seqNo = options.seqNo || 0;
23223
+ /**
23224
+ * @param {PerformanceEntry} entry
23225
+ * @returns {entry is PerformanceNavigationTiming}
23226
+ */
23227
+ function isNavigationTiming(entry) {
23228
+ return entry.entryType === 'navigation';
23229
+ }
23139
23230
 
23140
- this.idleTimeoutId = null;
23141
- this.maxTimeoutId = null;
23231
+ /**
23232
+ * @param {PerformanceEntry} entry
23233
+ * @returns {entry is PerformanceResourceTiming}
23234
+ */
23235
+ function isResourceTiming (entry) {
23236
+ return entry.entryType === 'resource';
23237
+ }
23142
23238
 
23143
- this.recordMaxMs = MAX_RECORDING_MS;
23144
- this.recordMinMs = 0;
23239
+ function findLast(array, predicate) {
23240
+ var length = array.length;
23241
+ for (var i = length - 1; i >= 0; i -= 1) {
23242
+ if (predicate(array[i])) {
23243
+ return array[i];
23244
+ }
23245
+ }
23246
+ }
23145
23247
 
23146
- // disable persistence if localStorage is not supported
23147
- // request-queue will automatically disable persistence if indexedDB fails to initialize
23148
- var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23248
+ /**
23249
+ * Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
23250
+ * Adapted from Sentry's `fill` utility:
23251
+ * https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
23252
+ *
23253
+ * @param {object} source - The object containing the method to patch
23254
+ * @param {string} name - The method name to patch
23255
+ * @param {function} replacementFactory - A function that receives the original method and returns the replacement
23256
+ * @returns {function} A function that restores the original method
23257
+ */
23258
+ function patch(source, name, replacementFactory) {
23259
+ if (!(name in source) || typeof source[name] !== 'function') {
23260
+ return function() {};
23261
+ }
23262
+ var original = source[name];
23263
+ var wrapped = replacementFactory(original);
23264
+ source[name] = wrapped;
23265
+ return function() {
23266
+ source[name] = original;
23267
+ };
23268
+ }
23149
23269
 
23150
- // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23151
- this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23152
- this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23153
- this.batcher = new RequestBatcher(this.batcherKey, {
23154
- errorReporter: this.reportError.bind(this),
23155
- flushOnlyOnInterval: true,
23156
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
23157
- sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23158
- queueStorage: this.queueStorage,
23159
- sharedLockStorage: options.sharedLockStorage,
23160
- usePersistence: usePersistence,
23161
- stopAllBatchingFunc: this.stopRecording.bind(this),
23162
23270
 
23163
- // increased throttle and shared lock timeout because recording events are very high frequency.
23164
- // this will minimize the amount of lock contention between enqueued events.
23165
- // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23166
- enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23167
- sharedLockTimeoutMS: 10 * 1000,
23168
- });
23169
- };
23271
+ /**
23272
+ * Maximum body size to record (1MB)
23273
+ */
23274
+ var MAX_BODY_SIZE = 1024 * 1024;
23170
23275
 
23171
23276
  /**
23172
- * @returns {UserIdInfo}
23277
+ * Truncate string if it exceeds max size
23278
+ * @param {string} str
23279
+ * @returns {string}
23173
23280
  */
23174
- SessionRecording.prototype.getUserIdInfo = function () {
23175
- if (this.finalFlushUserIdInfo) {
23176
- return this.finalFlushUserIdInfo;
23281
+ function truncateBody(str) {
23282
+ if (!str || typeof str !== 'string') {
23283
+ return str;
23284
+ }
23285
+ if (str.length > MAX_BODY_SIZE) {
23286
+ logger$4.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
23287
+ return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
23177
23288
  }
23289
+ return str;
23290
+ }
23178
23291
 
23179
- var userIdInfo = {
23180
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
23292
+ /**
23293
+ * @param {networkCallback} cb
23294
+ * @param {Window} win
23295
+ * @param {Required<NetworkRecordOptions>} options
23296
+ * @returns {listenerHandler}
23297
+ */
23298
+ function initPerformanceObserver(cb, win, options) {
23299
+ if (!win.PerformanceObserver) {
23300
+ logger$4.error('PerformanceObserver not supported');
23301
+ return function() {
23302
+ //
23303
+ };
23304
+ }
23305
+ if (options.recordInitialRequests) {
23306
+ var initialPerformanceEntries = win.performance
23307
+ .getEntries()
23308
+ .filter(function(entry) {
23309
+ return isNavigationTiming(entry) ||
23310
+ (isResourceTiming(entry) &&
23311
+ options.initiatorTypes.includes(entry.initiatorType));
23312
+ });
23313
+ cb({
23314
+ requests: initialPerformanceEntries.map(function(entry) {
23315
+ return {
23316
+ url: entry.name,
23317
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23318
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23319
+ startTime: Math.round(entry.startTime),
23320
+ endTime: Math.round(entry.responseEnd),
23321
+ timeOrigin: getTimeOrigin(win),
23322
+ };
23323
+ }),
23324
+ isInitial: true,
23325
+ });
23326
+ }
23327
+ var observer = new win.PerformanceObserver(function(entries) {
23328
+ var performanceEntries = entries
23329
+ .getEntries()
23330
+ .filter(function(entry) {
23331
+ return isResourceTiming(entry) &&
23332
+ options.initiatorTypes.includes(entry.initiatorType) &&
23333
+ entry.initiatorType !== 'xmlhttprequest' &&
23334
+ entry.initiatorType !== 'fetch';
23335
+ });
23336
+ cb({
23337
+ requests: performanceEntries.map(function(entry) {
23338
+ return {
23339
+ url: entry.name,
23340
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23341
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23342
+ startTime: Math.round(entry.startTime),
23343
+ endTime: Math.round(entry.responseEnd),
23344
+ timeOrigin: getTimeOrigin(win),
23345
+ };
23346
+ }),
23347
+ });
23348
+ });
23349
+ observer.observe({ entryTypes: ['navigation', 'resource'] });
23350
+ return function() {
23351
+ observer.disconnect();
23181
23352
  };
23353
+ }
23182
23354
 
23183
- // send ID management props if they exist
23184
- var deviceId = this._mixpanel.get_property('$device_id');
23185
- if (deviceId) {
23186
- userIdInfo['$device_id'] = deviceId;
23187
- }
23188
- var userId = this._mixpanel.get_property('$user_id');
23189
- if (userId) {
23190
- userIdInfo['$user_id'] = userId;
23355
+ /**
23356
+ * Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
23357
+ * @param {'request' | 'response'} type
23358
+ * @param {NetworkRecordOptions['recordHeaders']} recordHeaders
23359
+ * @param {string} headerName
23360
+ * @returns {boolean}
23361
+ */
23362
+ function shouldRecordHeader(type, recordHeaders, headerName) {
23363
+ if (!recordHeaders[type] || recordHeaders[type].length === 0) {
23364
+ return false;
23191
23365
  }
23192
- return userIdInfo;
23193
- };
23194
23366
 
23195
- SessionRecording.prototype.unloadPersistedData = function () {
23196
- this.batcher.stop();
23367
+ return recordHeaders[type].includes(headerName.toLowerCase());
23368
+ }
23197
23369
 
23198
- return this.queueStorage.init().catch(function () {
23199
- this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23200
- }.bind(this)).then(function () {
23201
- // if the recording is too short, just delete any stored events without flushing
23202
- if (this.getDurationMs() < this._getRecordMinMs()) {
23203
- return this.queueStorage.removeItem(this.batcherKey);
23204
- }
23370
+ /**
23371
+ * Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
23372
+ * @param {'request' | 'response'} type
23373
+ * @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
23374
+ * @param {string} url
23375
+ * @returns {boolean}
23376
+ */
23377
+ function shouldRecordBody(type, recordBodyUrls, url) {
23378
+ if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
23379
+ return false;
23380
+ }
23205
23381
 
23206
- return this.batcher.flush()
23207
- .then(function () {
23208
- return this.queueStorage.removeItem(this.batcherKey);
23209
- }.bind(this));
23210
- }.bind(this));
23211
- };
23382
+ return urlMatchesRegexList(url, recordBodyUrls[type]);
23383
+ }
23212
23384
 
23213
- SessionRecording.prototype.getConfig = function(configVar) {
23214
- return this._mixpanel.get_config(configVar);
23215
- };
23385
+ function tryReadXHRBody(body) {
23386
+ if (body === null || body === undefined) {
23387
+ return null;
23388
+ }
23216
23389
 
23217
- // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
23390
+ var result;
23391
+ if (typeof body === 'string') {
23392
+ result = body;
23393
+ } else if (body instanceof Document) {
23394
+ result = body.textContent;
23395
+ } else if (body instanceof FormData) {
23396
+ result = _.HTTPBuildQuery(body);
23397
+ } else if (_.isObject(body)) {
23398
+ try {
23399
+ result = JSON.stringify(body);
23400
+ } catch (e) {
23401
+ return 'Failed to stringify response object';
23402
+ }
23403
+ } else {
23404
+ return 'Cannot read body of type ' + typeof body;
23405
+ }
23406
+
23407
+ return truncateBody(result);
23408
+ }
23409
+
23410
+ /**
23411
+ * @param {Request | Response} r
23412
+ * @returns {Promise<string>}
23413
+ */
23414
+ function tryReadFetchBody(r) {
23415
+ return new Promise(function(resolve) {
23416
+ var timeout = setTimeout(function() {
23417
+ resolve('Timeout while trying to read body');
23418
+ }, 500);
23419
+ try {
23420
+ r.clone()
23421
+ .text()
23422
+ .then(
23423
+ function(txt) {
23424
+ clearTimeout(timeout);
23425
+ resolve(truncateBody(txt));
23426
+ },
23427
+ function(reason) {
23428
+ clearTimeout(timeout);
23429
+ resolve('Failed to read body: ' + String(reason));
23430
+ }
23431
+ );
23432
+ } catch (e) {
23433
+ clearTimeout(timeout);
23434
+ resolve('Failed to read body: ' + String(e));
23435
+ }
23436
+ });
23437
+ }
23438
+
23439
+ /**
23440
+ * @param {Window} win
23441
+ * @param {string} initiatorType
23442
+ * @param {string} url
23443
+ * @param {number} [after]
23444
+ * @param {number} [before]
23445
+ * @param {number} [attempt]
23446
+ * @returns {Promise<PerformanceResourceTiming>}
23447
+ */
23448
+ function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
23449
+ if (attempt === undefined) {
23450
+ attempt = 0;
23451
+ }
23452
+ if (attempt > 10) {
23453
+ logger$4.error('Cannot find performance entry');
23454
+ return Promise.resolve(null);
23455
+ }
23456
+ var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
23457
+ win.performance.getEntriesByName(url)
23458
+ );
23459
+ var performanceEntry = findLast(
23460
+ urlPerformanceEntries,
23461
+ function(entry) {
23462
+ return isResourceTiming(entry) &&
23463
+ entry.initiatorType === initiatorType &&
23464
+ (!after || entry.startTime >= after) &&
23465
+ (!before || entry.startTime <= before);
23466
+ }
23467
+ );
23468
+ if (!performanceEntry) {
23469
+ return new Promise(function(resolve) {
23470
+ setTimeout(resolve, 50 * attempt);
23471
+ }).then(function() {
23472
+ return getRequestPerformanceEntry(
23473
+ win,
23474
+ initiatorType,
23475
+ url,
23476
+ after,
23477
+ before,
23478
+ attempt + 1
23479
+ );
23480
+ });
23481
+ }
23482
+ return Promise.resolve(performanceEntry);
23483
+ }
23484
+
23485
+ /**
23486
+ * @param {networkCallback} cb
23487
+ * @param {Window} win
23488
+ * @param {Required<NetworkRecordOptions>} options
23489
+ * @returns {listenerHandler}
23490
+ */
23491
+ function initXhrObserver(cb, win, options) {
23492
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
23493
+ return function() {
23494
+ //
23495
+ };
23496
+ }
23497
+ var restorePatch = patch(
23498
+ win.XMLHttpRequest.prototype,
23499
+ 'open',
23500
+ function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
23501
+ return function(
23502
+ /** @type {string} */ method,
23503
+ /** @type {string | URL} */ url,
23504
+ /** @type {boolean} */ async,
23505
+ username, password
23506
+ ) {
23507
+ if (async === undefined) {
23508
+ async = true;
23509
+ }
23510
+ var xhr = /** @type {XMLHttpRequest} */ (this);
23511
+ var req = new Request(url, { method: method });
23512
+ /** @type {Partial<NetworkRequest>} */
23513
+ var networkRequest = {};
23514
+ /** @type {number | undefined} */
23515
+ var after;
23516
+ /** @type {number | undefined} */
23517
+ var before;
23518
+
23519
+ /** @type {Headers} */
23520
+ var requestHeaders = {};
23521
+ var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
23522
+ xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
23523
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23524
+ requestHeaders[header] = value;
23525
+ }
23526
+ return originalSetRequestHeader(header, value);
23527
+ };
23528
+ networkRequest.requestHeaders = requestHeaders;
23529
+
23530
+ var originalSend = xhr.send.bind(xhr);
23531
+ xhr.send = function(/** @type {Body} */ body) {
23532
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23533
+ networkRequest.requestBody = tryReadXHRBody(body);
23534
+ }
23535
+ after = win.performance.now();
23536
+ return originalSend(body);
23537
+ };
23538
+ xhr.addEventListener('readystatechange', function() {
23539
+ if (xhr.readyState !== xhr.DONE) {
23540
+ return;
23541
+ }
23542
+ before = win.performance.now();
23543
+ /** @type {Headers} */
23544
+ var responseHeaders = {};
23545
+ var rawHeaders = xhr.getAllResponseHeaders();
23546
+ if (rawHeaders) {
23547
+ var headers = rawHeaders.trim().split(/[\r\n]+/);
23548
+ headers.forEach(function(line) {
23549
+ if (!line) return;
23550
+ var colonIndex = line.indexOf(': ');
23551
+ if (colonIndex === -1) return;
23552
+ var header = line.substring(0, colonIndex);
23553
+ var value = line.substring(colonIndex + 2);
23554
+ if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
23555
+ responseHeaders[header] = value;
23556
+ }
23557
+ });
23558
+ }
23559
+ networkRequest.responseHeaders = responseHeaders;
23560
+ if (
23561
+ shouldRecordBody('response', options.recordBodyUrls, req.url)
23562
+ ) {
23563
+ networkRequest.responseBody = tryReadXHRBody(xhr.response);
23564
+ }
23565
+ getRequestPerformanceEntry(
23566
+ win,
23567
+ 'xmlhttprequest',
23568
+ req.url,
23569
+ after,
23570
+ before
23571
+ )
23572
+ .then(function(entry) {
23573
+ if (!entry) {
23574
+ logger$4.error('Failed to get performance entry for XHR request to ' + req.url);
23575
+ return;
23576
+ }
23577
+ /** @type {NetworkRequest} */
23578
+ var request = {
23579
+ url: entry.name,
23580
+ method: req.method,
23581
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23582
+ status: xhr.status,
23583
+ startTime: Math.round(entry.startTime),
23584
+ endTime: Math.round(entry.responseEnd),
23585
+ timeOrigin: getTimeOrigin(win),
23586
+ requestHeaders: networkRequest.requestHeaders,
23587
+ requestBody: networkRequest.requestBody,
23588
+ responseHeaders: networkRequest.responseHeaders,
23589
+ responseBody: networkRequest.responseBody,
23590
+ };
23591
+ cb({ requests: [request] });
23592
+ })
23593
+ .catch(function(e) {
23594
+ logger$4.error('Error recording XHR request to ' + req.url + ': ' + String(e));
23595
+ });
23596
+ });
23597
+
23598
+ originalOpen.call(xhr, method, url, async, username, password);
23599
+ };
23600
+ }
23601
+ );
23602
+ return function() {
23603
+ restorePatch();
23604
+ };
23605
+ }
23606
+
23607
+ /**
23608
+ * @param {networkCallback} cb
23609
+ * @param {Window} win
23610
+ * @param {Required<NetworkRecordOptions>} options
23611
+ * @returns {listenerHandler}
23612
+ */
23613
+ function initFetchObserver(cb, win, options) {
23614
+ if (!options.initiatorTypes.includes('fetch')) {
23615
+ return function() {
23616
+ //
23617
+ };
23618
+ }
23619
+
23620
+ var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
23621
+ return function() {
23622
+ var req = new Request(arguments[0], arguments[1]);
23623
+ /** @type {Response | undefined} */
23624
+ var res;
23625
+ /** @type {Partial<NetworkRequest>} */
23626
+ var networkRequest = {};
23627
+ /** @type {number | undefined} */
23628
+ var after;
23629
+ /** @type {number | undefined} */
23630
+ var before;
23631
+
23632
+ var originalFetchPromise;
23633
+ var requestBodyPromise = Promise.resolve(undefined);
23634
+ var responseBodyPromise = Promise.resolve(undefined);
23635
+ try {
23636
+ /** @type {Headers} */
23637
+ var requestHeaders = {};
23638
+ req.headers.forEach(function(value, header) {
23639
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23640
+ requestHeaders[header] = value;
23641
+ }
23642
+ });
23643
+ networkRequest.requestHeaders = requestHeaders;
23644
+
23645
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23646
+ requestBodyPromise = tryReadFetchBody(req)
23647
+ .then(function(body) {
23648
+ networkRequest.requestBody = body;
23649
+ });
23650
+ }
23651
+
23652
+ after = win.performance.now();
23653
+ originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
23654
+ res = response;
23655
+ before = win.performance.now();
23656
+
23657
+ /** @type {Headers} */
23658
+ var responseHeaders = {};
23659
+ res.headers.forEach(function(value, header) {
23660
+ if (shouldRecordHeader('response', options.recordHeaders, header)) {
23661
+ responseHeaders[header] = value;
23662
+ }
23663
+ });
23664
+ networkRequest.responseHeaders = responseHeaders;
23665
+
23666
+ if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
23667
+ responseBodyPromise = tryReadFetchBody(res)
23668
+ .then(function(body) {
23669
+ networkRequest.responseBody = body;
23670
+ });
23671
+ }
23672
+
23673
+ return res;
23674
+ });
23675
+ } catch (e) {
23676
+ originalFetchPromise = Promise.reject(e);
23677
+ }
23678
+
23679
+ // await concurrently so we don't delay the fetch response
23680
+ Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
23681
+ .then(function () {
23682
+ return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
23683
+ })
23684
+ .then(function(entry) {
23685
+ if (!entry) {
23686
+ logger$4.error('Failed to get performance entry for fetch request to ' + req.url);
23687
+ return;
23688
+ }
23689
+ /** @type {NetworkRequest} */
23690
+ var request = {
23691
+ url: entry.name,
23692
+ method: req.method,
23693
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23694
+ status: res ? res.status : undefined,
23695
+ startTime: Math.round(entry.startTime),
23696
+ endTime: Math.round(entry.responseEnd),
23697
+ timeOrigin: getTimeOrigin(win),
23698
+ requestHeaders: networkRequest.requestHeaders,
23699
+ requestBody: networkRequest.requestBody,
23700
+ responseHeaders: networkRequest.responseHeaders,
23701
+ responseBody: networkRequest.responseBody,
23702
+ };
23703
+ cb({ requests: [request] });
23704
+ })
23705
+ .catch(function (e) {
23706
+ logger$4.error('Error recording fetch request to ' + req.url + ': ' + String(e));
23707
+ });
23708
+
23709
+ return originalFetchPromise;
23710
+ };
23711
+ });
23712
+ return function() {
23713
+ restorePatch();
23714
+ };
23715
+ }
23716
+
23717
+ /**
23718
+ * @param {networkCallback} callback
23719
+ * @param {Window} win
23720
+ * @param {NetworkRecordOptions} options
23721
+ * @returns {listenerHandler}
23722
+ */
23723
+ function initNetworkObserver(callback, win, options) {
23724
+ if (!('performance' in win)) {
23725
+ return function() {
23726
+ //
23727
+ };
23728
+ }
23729
+
23730
+ var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
23731
+ var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
23732
+ options = Object.assign({}, options, {
23733
+ recordHeaders: recordHeaders,
23734
+ recordBodyUrls: recordBodyUrls,
23735
+ });
23736
+ var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
23737
+
23738
+ /** @type {networkCallback} */
23739
+ var cb = function(data) {
23740
+ var requests = data.requests.filter(function(request) {
23741
+ var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
23742
+ return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
23743
+ });
23744
+ if (requests.length > 0 || data.isInitial) {
23745
+ callback(Object.assign({}, data, { requests: requests }));
23746
+ }
23747
+ };
23748
+ var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
23749
+ var xhrObserver = initXhrObserver(cb, win, networkOptions);
23750
+ var fetchObserver = initFetchObserver(cb, win, networkOptions);
23751
+ return function() {
23752
+ performanceObserver();
23753
+ xhrObserver();
23754
+ fetchObserver();
23755
+ };
23756
+ }
23757
+
23758
+ // arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
23759
+ // a changed format in the mixpanel product.
23760
+ var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
23761
+
23762
+ /**
23763
+ * @param {NetworkRecordOptions} [options]
23764
+ * @returns {RecordPlugin}
23765
+ */
23766
+ var getRecordNetworkPlugin = function(options) {
23767
+ return {
23768
+ name: NETWORK_PLUGIN_NAME,
23769
+ observer: initNetworkObserver,
23770
+ options: options,
23771
+ };
23772
+ };
23773
+
23774
+ /**
23775
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23776
+ */
23777
+
23778
+
23779
+ var logger$3 = console_with_prefix('recorder');
23780
+ var CompressionStream = win['CompressionStream'];
23781
+
23782
+ var RECORDER_BATCHER_LIB_CONFIG = {
23783
+ 'batch_size': 1000,
23784
+ 'batch_flush_interval_ms': 10 * 1000,
23785
+ 'batch_request_timeout_ms': 90 * 1000,
23786
+ 'batch_autostart': true
23787
+ };
23788
+
23789
+ var ACTIVE_SOURCES = new Set([
23790
+ IncrementalSource.MouseMove,
23791
+ IncrementalSource.MouseInteraction,
23792
+ IncrementalSource.Scroll,
23793
+ IncrementalSource.ViewportResize,
23794
+ IncrementalSource.Input,
23795
+ IncrementalSource.TouchMove,
23796
+ IncrementalSource.MediaInteraction,
23797
+ IncrementalSource.Drag,
23798
+ IncrementalSource.Selection,
23799
+ ]);
23800
+
23801
+ function isUserEvent(ev) {
23802
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23803
+ }
23804
+
23805
+ /**
23806
+ * @typedef {Object} SerializedRecording
23807
+ * @property {number} idleExpires
23808
+ * @property {number} maxExpires
23809
+ * @property {number} replayStartTime
23810
+ * @property {number} lastEventTimestamp
23811
+ * @property {number} seqNo
23812
+ * @property {string} batchStartUrl
23813
+ * @property {string} replayId
23814
+ * @property {string} tabId
23815
+ * @property {string} replayStartUrl
23816
+ */
23817
+
23818
+ /**
23819
+ * @typedef {Object} SessionRecordingOptions
23820
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23821
+ * @property {String} [options.replayId] - unique uuid for a single replay
23822
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23823
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23824
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23825
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23826
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23827
+ * optional properties for deserialization:
23828
+ * @property {number} idleExpires
23829
+ * @property {number} maxExpires
23830
+ * @property {number} replayStartTime
23831
+ * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23832
+ * @property {number} seqNo
23833
+ * @property {string} batchStartUrl
23834
+ * @property {string} replayStartUrl
23835
+ */
23836
+
23837
+ /**
23838
+ * @typedef {Object} UserIdInfo
23839
+ * @property {string} distinct_id
23840
+ * @property {string} user_id
23841
+ * @property {string} device_id
23842
+ */
23843
+
23844
+
23845
+ /**
23846
+ * This class encapsulates a single session recording and its lifecycle.
23847
+ * @param {SessionRecordingOptions} options
23848
+ */
23849
+ var SessionRecording = function(options) {
23850
+ this._mixpanel = options.mixpanelInstance;
23851
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23852
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23853
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23854
+ this._rrwebRecord = options.rrwebRecord || null;
23855
+
23856
+ // internal rrweb stopRecording function
23857
+ this._stopRecording = null;
23858
+ this.replayId = options.replayId;
23859
+
23860
+ this.batchStartUrl = options.batchStartUrl || null;
23861
+ this.replayStartUrl = options.replayStartUrl || null;
23862
+ this.idleExpires = options.idleExpires || null;
23863
+ this.maxExpires = options.maxExpires || null;
23864
+ this.replayStartTime = options.replayStartTime || null;
23865
+ this.lastEventTimestamp = options.lastEventTimestamp || null;
23866
+ this.seqNo = options.seqNo || 0;
23867
+
23868
+ this.idleTimeoutId = null;
23869
+ this.maxTimeoutId = null;
23870
+
23871
+ this.recordMaxMs = MAX_RECORDING_MS;
23872
+ this.recordMinMs = 0;
23873
+
23874
+ // disable persistence if localStorage is not supported
23875
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
23876
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23877
+
23878
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23879
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23880
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23881
+ this.batcher = new RequestBatcher(this.batcherKey, {
23882
+ errorReporter: this.reportError.bind(this),
23883
+ flushOnlyOnInterval: true,
23884
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
23885
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23886
+ queueStorage: this.queueStorage,
23887
+ sharedLockStorage: options.sharedLockStorage,
23888
+ usePersistence: usePersistence,
23889
+ stopAllBatchingFunc: this.stopRecording.bind(this),
23890
+
23891
+ // increased throttle and shared lock timeout because recording events are very high frequency.
23892
+ // this will minimize the amount of lock contention between enqueued events.
23893
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23894
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23895
+ sharedLockTimeoutMS: 10 * 1000,
23896
+ });
23897
+ };
23898
+
23899
+ /**
23900
+ * @returns {UserIdInfo}
23901
+ */
23902
+ SessionRecording.prototype.getUserIdInfo = function () {
23903
+ if (this.finalFlushUserIdInfo) {
23904
+ return this.finalFlushUserIdInfo;
23905
+ }
23906
+
23907
+ var userIdInfo = {
23908
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
23909
+ };
23910
+
23911
+ // send ID management props if they exist
23912
+ var deviceId = this._mixpanel.get_property('$device_id');
23913
+ if (deviceId) {
23914
+ userIdInfo['$device_id'] = deviceId;
23915
+ }
23916
+ var userId = this._mixpanel.get_property('$user_id');
23917
+ if (userId) {
23918
+ userIdInfo['$user_id'] = userId;
23919
+ }
23920
+ return userIdInfo;
23921
+ };
23922
+
23923
+ SessionRecording.prototype.unloadPersistedData = function () {
23924
+ this.batcher.stop();
23925
+
23926
+ return this.queueStorage.init().catch(function () {
23927
+ this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23928
+ }.bind(this)).then(function () {
23929
+ // if the recording is too short, just delete any stored events without flushing
23930
+ if (this.getDurationMs() < this._getRecordMinMs()) {
23931
+ return this.queueStorage.removeItem(this.batcherKey);
23932
+ }
23933
+
23934
+ return this.batcher.flush()
23935
+ .then(function () {
23936
+ return this.queueStorage.removeItem(this.batcherKey);
23937
+ }.bind(this));
23938
+ }.bind(this));
23939
+ };
23940
+
23941
+ SessionRecording.prototype.getConfig = function(configVar) {
23942
+ return this._mixpanel.get_config(configVar);
23943
+ };
23944
+
23945
+ // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
23218
23946
  // reaches into this class instance and expects the snake case version of the function.
23219
23947
  // eslint-disable-next-line camelcase
23220
23948
  SessionRecording.prototype.get_config = function(configVar) {
@@ -23228,14 +23956,14 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
23228
23956
  }
23229
23957
 
23230
23958
  if (this._stopRecording !== null) {
23231
- logger$2.log('Recording already in progress, skipping startRecording.');
23959
+ logger$3.log('Recording already in progress, skipping startRecording.');
23232
23960
  return;
23233
23961
  }
23234
23962
 
23235
23963
  this.recordMaxMs = this.getConfig('record_max_ms');
23236
23964
  if (this.recordMaxMs > MAX_RECORDING_MS) {
23237
23965
  this.recordMaxMs = MAX_RECORDING_MS;
23238
- logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
23966
+ logger$3.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
23239
23967
  }
23240
23968
 
23241
23969
  if (!this.maxExpires) {
@@ -23276,6 +24004,31 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
23276
24004
 
23277
24005
  var privacyConfig = getPrivacyConfig(this._mixpanel);
23278
24006
 
24007
+ var plugins = [];
24008
+ if (this.getConfig('record_network')) {
24009
+ var options = this.getConfig('record_network_options') || {};
24010
+ // don't track requests to Mixpanel /record API
24011
+ var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
24012
+ ignoreRequestUrls.push(this._getApiRoute());
24013
+ options.ignoreRequestUrls = ignoreRequestUrls;
24014
+
24015
+ plugins.push(getRecordNetworkPlugin(options));
24016
+ }
24017
+
24018
+ if (this.getConfig('record_console')) {
24019
+ plugins.push(
24020
+ getRecordConsolePlugin({
24021
+ stringifyOptions: {
24022
+ stringLengthLimit: 1000,
24023
+ numOfKeysLimit: 50,
24024
+ depthOfLimit: 2
24025
+ }
24026
+ })
24027
+ );
24028
+ }
24029
+
24030
+ var validatedOrigins = validateAllowedOrigins(this.getConfig('record_allowed_iframe_origins'), logger$3);
24031
+
23279
24032
  try {
23280
24033
  this._stopRecording = this._rrwebRecord({
23281
24034
  'emit': function (ev) {
@@ -23310,19 +24063,13 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
23310
24063
  'maskTextSelector': '*',
23311
24064
  'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
23312
24065
  'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
24066
+ 'recordCrossOriginIframes': validatedOrigins.length > 0,
24067
+ 'allowedIframeOrigins': validatedOrigins,
23313
24068
  'recordCanvas': this.getConfig('record_canvas'),
23314
24069
  'sampling': {
23315
24070
  'canvas': 15
23316
24071
  },
23317
- 'plugins': this.getConfig('record_console') ? [
23318
- getRecordConsolePlugin({
23319
- stringifyOptions: {
23320
- stringLengthLimit: 1000,
23321
- numOfKeysLimit: 50,
23322
- depthOfLimit: 2
23323
- }
23324
- })
23325
- ] : []
24072
+ 'plugins': plugins,
23326
24073
  });
23327
24074
  } catch (err) {
23328
24075
  this.reportError('Unexpected error when starting rrweb recording.', err);
@@ -23437,6 +24184,10 @@ SessionRecording.deserialize = function (serializedRecording, options) {
23437
24184
  return recording;
23438
24185
  };
23439
24186
 
24187
+ SessionRecording.prototype._getApiRoute = function () {
24188
+ return this.getConfig('api_routes')['record'];
24189
+ };
24190
+
23440
24191
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
23441
24192
  var onSuccess = function (response, responseBody) {
23442
24193
  // Update batch specific props only if the request was successful to guarantee ordering.
@@ -23456,7 +24207,7 @@ SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, r
23456
24207
  });
23457
24208
  }.bind(this);
23458
24209
  var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
23459
- win['fetch'](apiHost + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
24210
+ win['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
23460
24211
  'method': 'POST',
23461
24212
  'headers': {
23462
24213
  'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
@@ -23538,14 +24289,14 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
23538
24289
 
23539
24290
 
23540
24291
  SessionRecording.prototype.reportError = function(msg, err) {
23541
- logger$2.error.apply(logger$2.error, arguments);
24292
+ logger$3.error.apply(logger$3.error, arguments);
23542
24293
  try {
23543
24294
  if (!err && !(msg instanceof Error)) {
23544
24295
  msg = new Error(msg);
23545
24296
  }
23546
24297
  this.getConfig('error_reporter')(msg, err);
23547
24298
  } catch(err) {
23548
- logger$2.error(err);
24299
+ logger$3.error(err);
23549
24300
  }
23550
24301
  };
23551
24302
 
@@ -23574,7 +24325,7 @@ SessionRecording.prototype._getRecordMinMs = function() {
23574
24325
  var configValue = this.getConfig('record_min_ms');
23575
24326
 
23576
24327
  if (configValue > MAX_VALUE_FOR_MIN_RECORDING_MS) {
23577
- logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
24328
+ logger$3.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
23578
24329
  return MAX_VALUE_FOR_MIN_RECORDING_MS;
23579
24330
  }
23580
24331
 
@@ -23737,7 +24488,7 @@ RecordingRegistry.prototype.flushInactiveRecordings = function () {
23737
24488
  .catch(this.handleError.bind(this));
23738
24489
  };
23739
24490
 
23740
- var logger$1 = console_with_prefix('recorder');
24491
+ var logger$2 = console_with_prefix('recorder');
23741
24492
 
23742
24493
  /**
23743
24494
  * Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
@@ -23753,7 +24504,7 @@ var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage
23753
24504
  */
23754
24505
  this.recordingRegistry = new RecordingRegistry({
23755
24506
  mixpanelInstance: this.mixpanelInstance,
23756
- errorReporter: logger$1.error,
24507
+ errorReporter: logger$2.error,
23757
24508
  sharedLockStorage: sharedLockStorage
23758
24509
  });
23759
24510
  this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
@@ -23765,17 +24516,17 @@ var MixpanelRecorder = function(mixpanelInstance, rrwebRecord, sharedLockStorage
23765
24516
  MixpanelRecorder.prototype.startRecording = function(options) {
23766
24517
  options = options || {};
23767
24518
  if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
23768
- logger$1.log('Recording already in progress, skipping startRecording.');
24519
+ logger$2.log('Recording already in progress, skipping startRecording.');
23769
24520
  return;
23770
24521
  }
23771
24522
 
23772
24523
  var onIdleTimeout = function () {
23773
- logger$1.log('Idle timeout reached, restarting recording.');
24524
+ logger$2.log('Idle timeout reached, restarting recording.');
23774
24525
  this.resetRecording();
23775
24526
  }.bind(this);
23776
24527
 
23777
24528
  var onMaxLengthReached = function () {
23778
- logger$1.log('Max recording length reached, stopping recording.');
24529
+ logger$2.log('Max recording length reached, stopping recording.');
23779
24530
  this.resetRecording();
23780
24531
  }.bind(this);
23781
24532
 
@@ -23845,7 +24596,7 @@ MixpanelRecorder.prototype.resumeRecording = function (startNewIfInactive) {
23845
24596
  } else if (startNewIfInactive) {
23846
24597
  return this.startRecording({shouldStopBatcher: false});
23847
24598
  } else {
23848
- logger$1.log('No resumable recording found.');
24599
+ logger$2.log('No resumable recording found.');
23849
24600
  return null;
23850
24601
  }
23851
24602
  }.bind(this));
@@ -23857,8 +24608,12 @@ MixpanelRecorder.prototype.resetRecording = function () {
23857
24608
  this.startRecording({shouldStopBatcher: true});
23858
24609
  };
23859
24610
 
24611
+ MixpanelRecorder.prototype.isRecording = function () {
24612
+ return this.activeRecording && !this.activeRecording.isRrwebStopped();
24613
+ };
24614
+
23860
24615
  MixpanelRecorder.prototype.getActiveReplayId = function () {
23861
- if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
24616
+ if (this.isRecording()) {
23862
24617
  return this.activeRecording.replayId;
23863
24618
  } else {
23864
24619
  return null;
@@ -24362,53 +25117,6 @@ function requireLogic () {
24362
25117
  var logicExports = requireLogic();
24363
25118
  var jsonLogic = /*@__PURE__*/getDefaultExportFromCjs(logicExports);
24364
25119
 
24365
- /**
24366
- * Shared helper to recursively lowercase strings in nested structures
24367
- * @param {*} obj - Value to process
24368
- * @param {boolean} lowercaseKeys - Whether to lowercase object keys
24369
- * @returns {*} Processed value with lowercased strings
24370
- */
24371
- var lowercaseJson = function(obj, lowercaseKeys) {
24372
- if (obj === null || obj === undefined) {
24373
- return obj;
24374
- } else if (typeof obj === 'string') {
24375
- return obj.toLowerCase();
24376
- } else if (Array.isArray(obj)) {
24377
- return obj.map(function(item) {
24378
- return lowercaseJson(item, lowercaseKeys);
24379
- });
24380
- } else if (obj === Object(obj)) {
24381
- var result = {};
24382
- for (var key in obj) {
24383
- if (obj.hasOwnProperty(key)) {
24384
- var newKey = lowercaseKeys && typeof key === 'string' ? key.toLowerCase() : key;
24385
- result[newKey] = lowercaseJson(obj[key], lowercaseKeys);
24386
- }
24387
- }
24388
- return result;
24389
- } else {
24390
- return obj;
24391
- }
24392
- };
24393
-
24394
- /**
24395
- * Lowercase all string keys and values in a nested structure
24396
- * @param {*} val - Value to process
24397
- * @returns {*} Processed value with lowercased strings
24398
- */
24399
- var lowercaseKeysAndValues = function(val) {
24400
- return lowercaseJson(val, true);
24401
- };
24402
-
24403
- /**
24404
- * Lowercase only leaf node string values in a nested structure (keys unchanged)
24405
- * @param {*} val - Value to process
24406
- * @returns {*} Processed value with lowercased leaf strings
24407
- */
24408
- var lowercaseOnlyLeafNodes = function(val) {
24409
- return lowercaseJson(val, false);
24410
- };
24411
-
24412
25120
  /**
24413
25121
  * Check if an event matches the given criteria
24414
25122
  * @param {string} eventName - The name of the event being checked
@@ -24432,13 +25140,8 @@ var eventMatchesCriteria = function(eventName, properties, criteria) {
24432
25140
 
24433
25141
  if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
24434
25142
  try {
24435
- // Lowercase all keys and values in event properties for case-insensitive matching
24436
- var lowercasedProperties = lowercaseKeysAndValues(properties || {});
24437
-
24438
- // Lowercase only leaf nodes in JsonLogic filters (keep operators intact)
24439
- var lowercasedFilters = lowercaseOnlyLeafNodes(propertyFilters);
24440
-
24441
- filtersMatch = jsonLogic.apply(lowercasedFilters, lowercasedProperties);
25143
+ // Use properties as-is for case-sensitive matching
25144
+ filtersMatch = jsonLogic.apply(propertyFilters, properties || {});
24442
25145
  } catch (error) {
24443
25146
  return {
24444
25147
  matches: false,
@@ -24558,7 +25261,7 @@ ShadowDOMObserver.prototype.observeShadowRoot = function(shadowRoot) {
24558
25261
  observer.observe(shadowRoot, this.observerConfig);
24559
25262
  this.shadowObservers.push(observer);
24560
25263
  } catch (e) {
24561
- logger$3.critical('Error while observing shadow root', e);
25264
+ logger$5.critical('Error while observing shadow root', e);
24562
25265
  }
24563
25266
  };
24564
25267
 
@@ -24569,7 +25272,7 @@ ShadowDOMObserver.prototype.start = function() {
24569
25272
  }
24570
25273
 
24571
25274
  if (!weakSetSupported()) {
24572
- logger$3.critical('Shadow DOM observation unavailable: WeakSet not supported');
25275
+ logger$5.critical('Shadow DOM observation unavailable: WeakSet not supported');
24573
25276
  return;
24574
25277
  }
24575
25278
 
@@ -24585,7 +25288,7 @@ ShadowDOMObserver.prototype.stop = function() {
24585
25288
  try {
24586
25289
  this.shadowObservers[i].disconnect();
24587
25290
  } catch (e) {
24588
- logger$3.critical('Error while disconnecting shadow DOM observer', e);
25291
+ logger$5.critical('Error while disconnecting shadow DOM observer', e);
24589
25292
  }
24590
25293
  }
24591
25294
  this.shadowObservers = [];
@@ -24773,7 +25476,7 @@ DeadClickTracker.prototype.startTracking = function() {
24773
25476
 
24774
25477
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
24775
25478
  } catch (e) {
24776
- logger$3.critical('Error while setting up mutation observer', e);
25479
+ logger$5.critical('Error while setting up mutation observer', e);
24777
25480
  }
24778
25481
  }
24779
25482
 
@@ -24788,7 +25491,7 @@ DeadClickTracker.prototype.startTracking = function() {
24788
25491
  );
24789
25492
  this.shadowDOMObserver.start();
24790
25493
  } catch (e) {
24791
- logger$3.critical('Error while setting up shadow DOM observer', e);
25494
+ logger$5.critical('Error while setting up shadow DOM observer', e);
24792
25495
  this.shadowDOMObserver = null;
24793
25496
  }
24794
25497
  }
@@ -24815,7 +25518,7 @@ DeadClickTracker.prototype.stopTracking = function() {
24815
25518
  try {
24816
25519
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
24817
25520
  } catch (e) {
24818
- logger$3.critical('Error while removing event listener', e);
25521
+ logger$5.critical('Error while removing event listener', e);
24819
25522
  }
24820
25523
  }
24821
25524
  this.eventListeners = [];
@@ -24824,7 +25527,7 @@ DeadClickTracker.prototype.stopTracking = function() {
24824
25527
  try {
24825
25528
  this.mutationObserver.disconnect();
24826
25529
  } catch (e) {
24827
- logger$3.critical('Error while disconnecting mutation observer', e);
25530
+ logger$5.critical('Error while disconnecting mutation observer', e);
24828
25531
  }
24829
25532
  this.mutationObserver = null;
24830
25533
  }
@@ -24833,7 +25536,7 @@ DeadClickTracker.prototype.stopTracking = function() {
24833
25536
  try {
24834
25537
  this.shadowDOMObserver.stop();
24835
25538
  } catch (e) {
24836
- logger$3.critical('Error while stopping shadow DOM observer', e);
25539
+ logger$5.critical('Error while stopping shadow DOM observer', e);
24837
25540
  }
24838
25541
  this.shadowDOMObserver = null;
24839
25542
  }
@@ -24911,7 +25614,7 @@ var Autocapture = function(mp) {
24911
25614
 
24912
25615
  Autocapture.prototype.init = function() {
24913
25616
  if (!minDOMApisSupported()) {
24914
- logger$3.critical('Autocapture unavailable: missing required DOM APIs');
25617
+ logger$5.critical('Autocapture unavailable: missing required DOM APIs');
24915
25618
  return;
24916
25619
  }
24917
25620
  this.initPageListeners();
@@ -24943,27 +25646,15 @@ Autocapture.prototype.getConfig = function(key) {
24943
25646
  };
24944
25647
 
24945
25648
  Autocapture.prototype.currentUrlBlocked = function() {
24946
- var i;
24947
25649
  var currentUrl = _.info.currentUrl();
24948
25650
 
24949
25651
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
24950
25652
  if (allowUrlRegexes.length) {
24951
25653
  // we're using an allowlist, only track if current URL matches
24952
- var allowed = false;
24953
- for (i = 0; i < allowUrlRegexes.length; i++) {
24954
- var allowRegex = allowUrlRegexes[i];
24955
- try {
24956
- if (currentUrl.match(allowRegex)) {
24957
- allowed = true;
24958
- break;
24959
- }
24960
- } catch (err) {
24961
- logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
24962
- return true;
24963
- }
24964
- }
24965
- if (!allowed) {
24966
- // wasn't allowed by any regex
25654
+ try {
25655
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
25656
+ } catch (err) {
25657
+ logger$5.critical('Error while checking block URL regexes: ', err);
24967
25658
  return true;
24968
25659
  }
24969
25660
  }
@@ -24973,17 +25664,12 @@ Autocapture.prototype.currentUrlBlocked = function() {
24973
25664
  return false;
24974
25665
  }
24975
25666
 
24976
- for (i = 0; i < blockUrlRegexes.length; i++) {
24977
- try {
24978
- if (currentUrl.match(blockUrlRegexes[i])) {
24979
- return true;
24980
- }
24981
- } catch (err) {
24982
- logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
24983
- return true;
24984
- }
25667
+ try {
25668
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
25669
+ } catch (err) {
25670
+ logger$5.critical('Error while checking block URL regexes: ', err);
25671
+ return true;
24985
25672
  }
24986
- return false;
24987
25673
  };
24988
25674
 
24989
25675
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -25119,7 +25805,7 @@ Autocapture.prototype._initScrollDepthTracking = function() {
25119
25805
  return;
25120
25806
  }
25121
25807
 
25122
- logger$3.log('Initializing scroll depth tracking');
25808
+ logger$5.log('Initializing scroll depth tracking');
25123
25809
 
25124
25810
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
25125
25811
 
@@ -25145,7 +25831,7 @@ Autocapture.prototype.initClickTracking = function() {
25145
25831
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
25146
25832
  return;
25147
25833
  }
25148
- logger$3.log('Initializing click tracking');
25834
+ logger$5.log('Initializing click tracking');
25149
25835
 
25150
25836
  this.listenerClick = function(ev) {
25151
25837
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -25164,7 +25850,7 @@ Autocapture.prototype.initDeadClickTracking = function() {
25164
25850
  return;
25165
25851
  }
25166
25852
 
25167
- logger$3.log('Initializing dead click tracking');
25853
+ logger$5.log('Initializing dead click tracking');
25168
25854
  if (!this._deadClickTracker) {
25169
25855
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
25170
25856
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -25198,7 +25884,7 @@ Autocapture.prototype.initInputTracking = function() {
25198
25884
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
25199
25885
  return;
25200
25886
  }
25201
- logger$3.log('Initializing input tracking');
25887
+ logger$5.log('Initializing input tracking');
25202
25888
 
25203
25889
  this.listenerChange = function(ev) {
25204
25890
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -25215,7 +25901,7 @@ Autocapture.prototype.initPageviewTracking = function() {
25215
25901
  if (!this.pageviewTrackingConfig()) {
25216
25902
  return;
25217
25903
  }
25218
- logger$3.log('Initializing pageview tracking');
25904
+ logger$5.log('Initializing pageview tracking');
25219
25905
 
25220
25906
  var previousTrackedUrl = '';
25221
25907
  var tracked = false;
@@ -25250,7 +25936,7 @@ Autocapture.prototype.initPageviewTracking = function() {
25250
25936
  }
25251
25937
  if (didPathChange) {
25252
25938
  this.lastScrollCheckpoint = 0;
25253
- logger$3.log('Path change: re-initializing scroll depth checkpoints');
25939
+ logger$5.log('Path change: re-initializing scroll depth checkpoints');
25254
25940
  }
25255
25941
  }
25256
25942
  }.bind(this));
@@ -25265,7 +25951,7 @@ Autocapture.prototype.initRageClickTracking = function() {
25265
25951
  return;
25266
25952
  }
25267
25953
 
25268
- logger$3.log('Initializing rage click tracking');
25954
+ logger$5.log('Initializing rage click tracking');
25269
25955
  if (!this._rageClickTracker) {
25270
25956
  this._rageClickTracker = new RageClickTracker();
25271
25957
  }
@@ -25295,7 +25981,7 @@ Autocapture.prototype.initScrollTracking = function() {
25295
25981
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
25296
25982
  return;
25297
25983
  }
25298
- logger$3.log('Initializing scroll tracking');
25984
+ logger$5.log('Initializing scroll tracking');
25299
25985
  this.lastScrollCheckpoint = 0;
25300
25986
 
25301
25987
  var scrollTrackFunction = function() {
@@ -25332,7 +26018,7 @@ Autocapture.prototype.initScrollTracking = function() {
25332
26018
  }
25333
26019
  }
25334
26020
  } catch (err) {
25335
- logger$3.critical('Error while calculating scroll percentage', err);
26021
+ logger$5.critical('Error while calculating scroll percentage', err);
25336
26022
  }
25337
26023
  if (shouldTrack) {
25338
26024
  this.mp.track(MP_EV_SCROLL, props);
@@ -25350,7 +26036,7 @@ Autocapture.prototype.initSubmitTracking = function() {
25350
26036
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
25351
26037
  return;
25352
26038
  }
25353
- logger$3.log('Initializing submit tracking');
26039
+ logger$5.log('Initializing submit tracking');
25354
26040
 
25355
26041
  this.listenerSubmit = function(ev) {
25356
26042
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -25372,7 +26058,7 @@ Autocapture.prototype.initPageLeaveTracking = function() {
25372
26058
  return;
25373
26059
  }
25374
26060
 
25375
- logger$3.log('Initializing page visibility tracking.');
26061
+ logger$5.log('Initializing page visibility tracking.');
25376
26062
  this._initScrollDepthTracking();
25377
26063
  var previousTrackedUrl = _.info.currentUrl();
25378
26064
 
@@ -25457,7 +26143,7 @@ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
25457
26143
  return win[TARGETING_GLOBAL_NAME];
25458
26144
  };
25459
26145
 
25460
- var logger = console_with_prefix('flags');
26146
+ var logger$1 = console_with_prefix('flags');
25461
26147
  var FLAGS_CONFIG_KEY = 'flags';
25462
26148
 
25463
26149
  var CONFIG_CONTEXT = 'context';
@@ -25500,7 +26186,7 @@ var FeatureFlagManager = function(initOptions) {
25500
26186
 
25501
26187
  FeatureFlagManager.prototype.init = function() {
25502
26188
  if (!this.minApisSupported()) {
25503
- logger.critical('Feature Flags unavailable: missing minimum required APIs');
26189
+ logger$1.critical('Feature Flags unavailable: missing minimum required APIs');
25504
26190
  return;
25505
26191
  }
25506
26192
 
@@ -25535,7 +26221,7 @@ FeatureFlagManager.prototype.isSystemEnabled = function() {
25535
26221
 
25536
26222
  FeatureFlagManager.prototype.updateContext = function(newContext, options) {
25537
26223
  if (!this.isSystemEnabled()) {
25538
- logger.critical('Feature Flags not enabled, cannot update context');
26224
+ logger$1.critical('Feature Flags not enabled, cannot update context');
25539
26225
  return Promise.resolve();
25540
26226
  }
25541
26227
 
@@ -25552,7 +26238,7 @@ FeatureFlagManager.prototype.updateContext = function(newContext, options) {
25552
26238
 
25553
26239
  FeatureFlagManager.prototype.areFlagsReady = function() {
25554
26240
  if (!this.isSystemEnabled()) {
25555
- logger.error('Feature Flags not enabled');
26241
+ logger$1.error('Feature Flags not enabled');
25556
26242
  }
25557
26243
  return !!this.flags;
25558
26244
  };
@@ -25565,7 +26251,7 @@ FeatureFlagManager.prototype.fetchFlags = function() {
25565
26251
  var distinctId = this.getMpProperty('distinct_id');
25566
26252
  var deviceId = this.getMpProperty('$device_id');
25567
26253
  var traceparent = generateTraceparent();
25568
- logger.log('Fetching flags for distinct ID: ' + distinctId);
26254
+ logger$1.log('Fetching flags for distinct ID: ' + distinctId);
25569
26255
 
25570
26256
  var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
25571
26257
  var searchParams = new URLSearchParams();
@@ -25664,11 +26350,11 @@ FeatureFlagManager.prototype.fetchFlags = function() {
25664
26350
  this._loadTargetingIfNeeded();
25665
26351
  }.bind(this)).catch(function(error) {
25666
26352
  this.markFetchComplete();
25667
- logger.error(error);
26353
+ logger$1.error(error);
25668
26354
  }.bind(this));
25669
26355
  }.bind(this)).catch(function(error) {
25670
26356
  this.markFetchComplete();
25671
- logger.error(error);
26357
+ logger$1.error(error);
25672
26358
  }.bind(this));
25673
26359
 
25674
26360
  return this.fetchPromise;
@@ -25676,7 +26362,7 @@ FeatureFlagManager.prototype.fetchFlags = function() {
25676
26362
 
25677
26363
  FeatureFlagManager.prototype.markFetchComplete = function() {
25678
26364
  if (!this._fetchInProgressStartTime) {
25679
- logger.error('Fetch in progress started time not set, cannot mark fetch complete');
26365
+ logger$1.error('Fetch in progress started time not set, cannot mark fetch complete');
25680
26366
  return;
25681
26367
  }
25682
26368
  this._fetchStartTime = this._fetchInProgressStartTime;
@@ -25698,7 +26384,7 @@ FeatureFlagManager.prototype._loadTargetingIfNeeded = function() {
25698
26384
 
25699
26385
  if (hasPropertyFilters) {
25700
26386
  this.getTargeting().then(function() {
25701
- logger.log('targeting loaded for property filter evaluation');
26387
+ logger$1.log('targeting loaded for property filter evaluation');
25702
26388
  });
25703
26389
  }
25704
26390
  };
@@ -25713,7 +26399,7 @@ FeatureFlagManager.prototype.getTargeting = function() {
25713
26399
  this.loadExtraBundle.bind(this),
25714
26400
  this.targetingSrc
25715
26401
  ).catch(function(error) {
25716
- logger.error('Failed to load targeting: ' + error);
26402
+ logger$1.error('Failed to load targeting: ' + error);
25717
26403
  }.bind(this));
25718
26404
  };
25719
26405
 
@@ -25767,7 +26453,7 @@ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, p
25767
26453
 
25768
26454
  // If no targeting library and event has property filters, skip it
25769
26455
  if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
25770
- logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
26456
+ logger$1.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
25771
26457
  return;
25772
26458
  }
25773
26459
 
@@ -25790,7 +26476,7 @@ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, p
25790
26476
  }
25791
26477
 
25792
26478
  if (matchResult.error) {
25793
- logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
26479
+ logger$1.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
25794
26480
  return;
25795
26481
  }
25796
26482
 
@@ -25798,7 +26484,7 @@ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, p
25798
26484
  return;
25799
26485
  }
25800
26486
 
25801
- logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
26487
+ logger$1.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
25802
26488
 
25803
26489
  var newVariant = {
25804
26490
  'key': pendingEvent['pending_variant']['variant_key'],
@@ -25839,7 +26525,7 @@ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId,
25839
26525
  'first_time_event_hash': firstTimeEventHash
25840
26526
  };
25841
26527
 
25842
- logger.log('Recording first-time event for flag: ' + flagId);
26528
+ logger$1.log('Recording first-time event for flag: ' + flagId);
25843
26529
 
25844
26530
  // Fire-and-forget POST request
25845
26531
  this.fetch.call(win, url, {
@@ -25852,130 +26538,446 @@ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId,
25852
26538
  'body': JSON.stringify(payload)
25853
26539
  }).catch(function(error) {
25854
26540
  // Silent failure - cohort sync will catch up
25855
- logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26541
+ logger$1.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26542
+ });
26543
+ };
26544
+
26545
+ FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
26546
+ if (!this.fetchPromise) {
26547
+ return new Promise(function(resolve) {
26548
+ logger$1.critical('Feature Flags not initialized');
26549
+ resolve(fallback);
26550
+ });
26551
+ }
26552
+
26553
+ return this.fetchPromise.then(function() {
26554
+ return this.getVariantSync(featureName, fallback);
26555
+ }.bind(this)).catch(function(error) {
26556
+ logger$1.error(error);
26557
+ return fallback;
26558
+ });
26559
+ };
26560
+
26561
+ FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
26562
+ if (!this.areFlagsReady()) {
26563
+ logger$1.log('Flags not loaded yet');
26564
+ return fallback;
26565
+ }
26566
+ var feature = this.flags.get(featureName);
26567
+ if (!feature) {
26568
+ logger$1.log('No flag found: "' + featureName + '"');
26569
+ return fallback;
26570
+ }
26571
+ this.trackFeatureCheck(featureName, feature);
26572
+ return feature;
26573
+ };
26574
+
26575
+ FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
26576
+ return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
26577
+ return feature['value'];
26578
+ }).catch(function(error) {
26579
+ logger$1.error(error);
26580
+ return fallbackValue;
26581
+ });
26582
+ };
26583
+
26584
+ // TODO remove deprecated method
26585
+ FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
26586
+ logger$1.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
26587
+ return this.getVariantValue(featureName, fallbackValue);
26588
+ };
26589
+
26590
+ FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
26591
+ return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
26592
+ };
26593
+
26594
+ FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
26595
+ return this.getVariantValue(featureName).then(function() {
26596
+ return this.isEnabledSync(featureName, fallbackValue);
26597
+ }.bind(this)).catch(function(error) {
26598
+ logger$1.error(error);
26599
+ return fallbackValue;
25856
26600
  });
25857
26601
  };
25858
26602
 
25859
- FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
25860
- if (!this.fetchPromise) {
25861
- return new Promise(function(resolve) {
25862
- logger.critical('Feature Flags not initialized');
25863
- resolve(fallback);
25864
- });
26603
+ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
26604
+ fallbackValue = fallbackValue || false;
26605
+ var val = this.getVariantValueSync(featureName, fallbackValue);
26606
+ if (val !== true && val !== false) {
26607
+ logger$1.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
26608
+ val = fallbackValue;
26609
+ }
26610
+ return val;
26611
+ };
26612
+
26613
+ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
26614
+ if (this.trackedFeatures.has(featureName)) {
26615
+ return;
26616
+ }
26617
+ this.trackedFeatures.add(featureName);
26618
+
26619
+ var trackingProperties = {
26620
+ 'Experiment name': featureName,
26621
+ 'Variant name': feature['key'],
26622
+ '$experiment_type': 'feature_flag',
26623
+ 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
26624
+ 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
26625
+ 'Variant fetch latency (ms)': this._fetchLatency,
26626
+ 'Variant fetch traceparent': this._traceparent,
26627
+ };
26628
+
26629
+ if (feature['experiment_id'] !== 'undefined') {
26630
+ trackingProperties['$experiment_id'] = feature['experiment_id'];
26631
+ }
26632
+ if (feature['is_experiment_active'] !== 'undefined') {
26633
+ trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
26634
+ }
26635
+ if (feature['is_qa_tester'] !== 'undefined') {
26636
+ trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
26637
+ }
26638
+
26639
+ this.track('$experiment_started', trackingProperties);
26640
+ };
26641
+
26642
+ FeatureFlagManager.prototype.minApisSupported = function() {
26643
+ return !!this.fetch &&
26644
+ typeof Promise !== 'undefined' &&
26645
+ typeof Map !== 'undefined' &&
26646
+ typeof Set !== 'undefined';
26647
+ };
26648
+
26649
+ safewrapClass(FeatureFlagManager);
26650
+
26651
+ FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
26652
+ FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
26653
+ FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
26654
+ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
26655
+ FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
26656
+ FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
26657
+ FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
26658
+ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
26659
+
26660
+ // Deprecated method
26661
+ FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
26662
+
26663
+ // Exports intended only for testing
26664
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26665
+
26666
+ /* eslint camelcase: "off" */
26667
+
26668
+
26669
+ var logger = console_with_prefix('recorder');
26670
+
26671
+ var IFRAME_HANDSHAKE_REQUEST = 'mp_iframe_handshake_request';
26672
+ var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
26673
+
26674
+
26675
+ /**
26676
+ * RecorderManager: manages session recording initialization, lifecycle and state
26677
+ * @constructor
26678
+ */
26679
+ var RecorderManager = function(initOptions) {
26680
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
26681
+ // but ideally we should be able to remove this dependency.
26682
+ this.mixpanelInstance = initOptions.mixpanelInstance;
26683
+
26684
+ this.getMpConfig = initOptions.getConfigFunc;
26685
+ this.getTabId = initOptions.getTabIdFunc;
26686
+ this.reportError = initOptions.reportErrorFunc;
26687
+ this.getDistinctId = initOptions.getDistinctIdFunc;
26688
+ this.loadExtraBundle = initOptions.loadExtraBundle;
26689
+ this.recorderSrc = initOptions.recorderSrc;
26690
+ this.targetingSrc = initOptions.targetingSrc;
26691
+ this.libBasePath = initOptions.libBasePath;
26692
+
26693
+ this._recorder = null;
26694
+ this._parentReplayId = null;
26695
+ this._parentFrameRetryInterval = null;
26696
+ };
26697
+
26698
+ RecorderManager.prototype.shouldLoadRecorder = function() {
26699
+ if (this.getMpConfig('disable_persistence')) {
26700
+ console$1.log('Load recorder check skipped due to disable_persistence config');
26701
+ return PromisePolyfill.resolve(false);
26702
+ }
26703
+
26704
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26705
+ var tab_id = this.getTabId();
26706
+ return recording_registry_idb.init()
26707
+ .then(function () {
26708
+ return recording_registry_idb.getAll();
26709
+ })
26710
+ .then(function (recordings) {
26711
+ for (var i = 0; i < recordings.length; i++) {
26712
+ // if there are expired recordings in the registry, we should load the recorder to flush them
26713
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
26714
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26715
+ return true;
26716
+ }
26717
+ }
26718
+ return false;
26719
+ })
26720
+ .catch(_.bind(function (err) {
26721
+ this.reportError('Error checking recording registry', err);
26722
+ return false;
26723
+ }, this));
26724
+ };
26725
+
26726
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
26727
+ if (!win['MutationObserver']) {
26728
+ console$1.critical('Browser does not support MutationObserver; skipping session recording');
26729
+ return PromisePolyfill.resolve();
26730
+ }
26731
+
26732
+ var loadRecorder = _.bind(function(startNewIfInactive) {
26733
+ return new PromisePolyfill(_.bind(function(resolve) {
26734
+ var handleLoadedRecorder = safewrap(_.bind(function() {
26735
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
26736
+ this._recorder['resumeRecording'](startNewIfInactive);
26737
+ resolve();
26738
+ }, this));
26739
+
26740
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
26741
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
26742
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
26743
+ } else {
26744
+ handleLoadedRecorder();
26745
+ }
26746
+ }, this));
26747
+ }, this);
26748
+
26749
+ // Cross-origin iframe handling
26750
+ var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger);
26751
+ var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
26752
+
26753
+ if (isCrossOriginRecordingEnabled) {
26754
+ // listen for handshake requests from their own child iframes (including nested)
26755
+ this._setupParentFrameListener(allowedOrigins);
26756
+
26757
+ if (win.parent !== win) {
26758
+ // also wait for parent's replay ID
26759
+ this._setupChildFrameListener(allowedOrigins, loadRecorder);
26760
+ this._sendParentFrameRequestWithRetry(allowedOrigins);
26761
+ return PromisePolyfill.resolve();
26762
+ }
26763
+ }
26764
+
26765
+ /**
26766
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26767
+ * 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.
26768
+ */
26769
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
26770
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
26771
+ if (force_start || is_sampled) {
26772
+ return loadRecorder(true);
26773
+ } else {
26774
+ return this.shouldLoadRecorder()
26775
+ .then(_.bind(function (shouldLoad) {
26776
+ if (shouldLoad) {
26777
+ return loadRecorder(false);
26778
+ }
26779
+ return PromisePolyfill.resolve();
26780
+ }, this));
26781
+ }
26782
+ };
26783
+
26784
+ RecorderManager.prototype.isRecording = function() {
26785
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
26786
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
26787
+ return false;
26788
+ }
26789
+ try {
26790
+ return this._recorder['isRecording']();
26791
+ } catch (e) {
26792
+ this.reportError('Error checking if recording is active', e);
26793
+ return false;
25865
26794
  }
26795
+ };
25866
26796
 
25867
- return this.fetchPromise.then(function() {
25868
- return this.getVariantSync(featureName, fallback);
25869
- }.bind(this)).catch(function(error) {
25870
- logger.error(error);
25871
- return fallback;
25872
- });
26797
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
26798
+ var isRecording = this.isRecording();
26799
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
26800
+
26801
+ if (!isRecording && recordingTriggerEvents) {
26802
+ var trigger = recordingTriggerEvents[event_name];
26803
+ if (trigger && typeof trigger['percentage'] === 'number') {
26804
+ var newRate = trigger['percentage'];
26805
+ var propertyFilters = trigger['property_filters'];
26806
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
26807
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
26808
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
26809
+ .then(function(targeting) {
26810
+ try {
26811
+ var result = targeting['eventMatchesCriteria'](
26812
+ event_name,
26813
+ properties,
26814
+ {
26815
+ 'event_name': event_name,
26816
+ 'property_filters': propertyFilters
26817
+ }
26818
+ );
26819
+ if (result['matches']) {
26820
+ this.checkAndStartSessionRecording(false, newRate);
26821
+ }
26822
+ } catch (err) {
26823
+ console$1.critical('Could not parse recording event trigger properties logic:', err);
26824
+ }
26825
+ }.bind(this)).catch(function(err) {
26826
+ console$1.critical('Failed to load targeting library:', err);
26827
+ });
26828
+ } else {
26829
+ this.checkAndStartSessionRecording(false, newRate);
26830
+ }
26831
+ }
26832
+ }
25873
26833
  };
25874
26834
 
25875
- FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
25876
- if (!this.areFlagsReady()) {
25877
- logger.log('Flags not loaded yet');
25878
- return fallback;
26835
+ RecorderManager.prototype.stopSessionRecording = function() {
26836
+ if (this._recorder) {
26837
+ return this._recorder['stopRecording']();
25879
26838
  }
25880
- var feature = this.flags.get(featureName);
25881
- if (!feature) {
25882
- logger.log('No flag found: "' + featureName + '"');
25883
- return fallback;
26839
+ return PromisePolyfill.resolve();
26840
+ };
26841
+
26842
+ RecorderManager.prototype.pauseSessionRecording = function() {
26843
+ if (this._recorder) {
26844
+ return this._recorder['pauseRecording']();
25884
26845
  }
25885
- this.trackFeatureCheck(featureName, feature);
25886
- return feature;
26846
+ return PromisePolyfill.resolve();
25887
26847
  };
25888
26848
 
25889
- FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
25890
- return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
25891
- return feature['value'];
25892
- }).catch(function(error) {
25893
- logger.error(error);
25894
- return fallbackValue;
25895
- });
26849
+ RecorderManager.prototype.resumeSessionRecording = function() {
26850
+ if (this._recorder) {
26851
+ return this._recorder['resumeRecording']();
26852
+ }
26853
+ return PromisePolyfill.resolve();
25896
26854
  };
25897
26855
 
25898
- // TODO remove deprecated method
25899
- FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
25900
- logger.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
25901
- return this.getVariantValue(featureName, fallbackValue);
26856
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
26857
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
25902
26858
  };
25903
26859
 
25904
- FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
25905
- return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
26860
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
26861
+ var props = {};
26862
+ var replay_id = this.getSessionReplayId();
26863
+ if (replay_id) {
26864
+ props['$mp_replay_id'] = replay_id;
26865
+ }
26866
+ return props;
25906
26867
  };
25907
26868
 
25908
- FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
25909
- return this.getVariantValue(featureName).then(function() {
25910
- return this.isEnabledSync(featureName, fallbackValue);
25911
- }.bind(this)).catch(function(error) {
25912
- logger.error(error);
25913
- return fallbackValue;
25914
- });
26869
+ RecorderManager.prototype.getSessionReplayUrl = function() {
26870
+ var replay_url = null;
26871
+ var replay_id = this.getSessionReplayId();
26872
+ if (replay_id) {
26873
+ var query_params = _.HTTPBuildQuery({
26874
+ 'replay_id': replay_id,
26875
+ 'distinct_id': this.getDistinctId(),
26876
+ 'token': this.getMpConfig('token')
26877
+ });
26878
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26879
+ }
26880
+ return replay_url;
25915
26881
  };
25916
26882
 
25917
- FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
25918
- fallbackValue = fallbackValue || false;
25919
- var val = this.getVariantValueSync(featureName, fallbackValue);
25920
- if (val !== true && val !== false) {
25921
- logger.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
25922
- val = fallbackValue;
26883
+ RecorderManager.prototype.getSessionReplayId = function() {
26884
+ // Child iframe uses parent's replay ID
26885
+ if (this._parentReplayId) {
26886
+ return this._parentReplayId;
25923
26887
  }
25924
- return val;
26888
+ var replay_id = null;
26889
+ if (this._recorder) {
26890
+ replay_id = this._recorder['replayId'];
26891
+ }
26892
+ return replay_id || null;
25925
26893
  };
25926
26894
 
25927
- FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
25928
- if (this.trackedFeatures.has(featureName)) {
26895
+ // "private" public method to reach into the recorder in test cases
26896
+ RecorderManager.prototype.getRecorder = function() {
26897
+ return this._recorder;
26898
+ };
26899
+
26900
+ RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
26901
+ if (this._childFrameMessageHandler) {
25929
26902
  return;
25930
26903
  }
25931
- this.trackedFeatures.add(featureName);
25932
-
25933
- var trackingProperties = {
25934
- 'Experiment name': featureName,
25935
- 'Variant name': feature['key'],
25936
- '$experiment_type': 'feature_flag',
25937
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
25938
- 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
25939
- 'Variant fetch latency (ms)': this._fetchLatency,
25940
- 'Variant fetch traceparent': this._traceparent,
26904
+ var self = this;
26905
+ this._childFrameMessageHandler = function(event) {
26906
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
26907
+ var data = event.data;
26908
+ if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
26909
+ self._parentReplayId = data['replayId'];
26910
+ if (data['distinctId']) {
26911
+ self.mixpanelInstance['identify'](data['distinctId']);
26912
+ }
26913
+ self._parentFrameRetryActive = false;
26914
+ win.removeEventListener('message', self._childFrameMessageHandler);
26915
+ self._childFrameMessageHandler = null;
26916
+ loadRecorder(true);
26917
+ }
25941
26918
  };
26919
+ win.addEventListener('message', this._childFrameMessageHandler);
26920
+ };
25942
26921
 
25943
- if (feature['experiment_id'] !== 'undefined') {
25944
- trackingProperties['$experiment_id'] = feature['experiment_id'];
25945
- }
25946
- if (feature['is_experiment_active'] !== 'undefined') {
25947
- trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
25948
- }
25949
- if (feature['is_qa_tester'] !== 'undefined') {
25950
- trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
26922
+ RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
26923
+ var message = {};
26924
+ message['type'] = IFRAME_HANDSHAKE_REQUEST;
26925
+ message['token'] = this.getMpConfig('token');
26926
+ for (var i = 0; i < allowedOrigins.length; i++) {
26927
+ try {
26928
+ win.parent.postMessage(message, allowedOrigins[i]);
26929
+ } catch (e) {
26930
+ // origin mismatch - ignore
26931
+ }
25951
26932
  }
25952
-
25953
- this.track('$experiment_started', trackingProperties);
25954
26933
  };
25955
26934
 
25956
- FeatureFlagManager.prototype.minApisSupported = function() {
25957
- return !!this.fetch &&
25958
- typeof Promise !== 'undefined' &&
25959
- typeof Map !== 'undefined' &&
25960
- typeof Set !== 'undefined';
25961
- };
26935
+ RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
26936
+ var self = this;
26937
+ var maxRetries = 10;
26938
+ var retryCount = 0;
26939
+ var delay = 50;
26940
+ this._parentFrameRetryActive = true;
25962
26941
 
25963
- safewrapClass(FeatureFlagManager);
26942
+ this._sendParentFrameRequest(allowedOrigins);
25964
26943
 
25965
- FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
25966
- FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
25967
- FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
25968
- FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
25969
- FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
25970
- FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
25971
- FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
25972
- FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
26944
+ function scheduleRetry() {
26945
+ setTimeout(function() {
26946
+ if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
26947
+ return;
26948
+ }
26949
+ self._sendParentFrameRequest(allowedOrigins);
26950
+ delay *= 2;
26951
+ scheduleRetry();
26952
+ }, delay);
26953
+ }
26954
+ scheduleRetry();
26955
+ };
25973
26956
 
25974
- // Deprecated method
25975
- FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
26957
+ RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
26958
+ if (this._parentFrameMessageHandler) {
26959
+ return;
26960
+ }
26961
+ var self = this;
26962
+ this._parentFrameMessageHandler = function(event) {
26963
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
26964
+ var data = event.data;
26965
+ if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
26966
+ var replayId = self.getSessionReplayId();
26967
+ if (replayId) {
26968
+ var response = {};
26969
+ response['type'] = IFRAME_HANDSHAKE_RESPONSE;
26970
+ response['token'] = self.getMpConfig('token');
26971
+ response['replayId'] = replayId;
26972
+ response['distinctId'] = self.getDistinctId();
26973
+ event.source.postMessage(response, event.origin);
26974
+ }
26975
+ }
26976
+ };
26977
+ win.addEventListener('message', this._parentFrameMessageHandler);
26978
+ };
25976
26979
 
25977
- // Exports intended only for testing
25978
- FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26980
+ safewrapClass(RecorderManager);
25979
26981
 
25980
26982
  /* eslint camelcase: "off" */
25981
26983
 
@@ -27349,7 +28351,6 @@ var INIT_SNIPPET = 1;
27349
28351
  /** @const */ var SETTING_FALLBACK = 'fallback';
27350
28352
  /** @const */ var SETTING_DISABLED = 'disabled';
27351
28353
 
27352
-
27353
28354
  /*
27354
28355
  * Dynamic... constants? Is that an oxymoron?
27355
28356
  */
@@ -27434,19 +28435,24 @@ var DEFAULT_CONFIG = {
27434
28435
  'batch_request_timeout_ms': 90000,
27435
28436
  'batch_autostart': true,
27436
28437
  'hooks': {},
28438
+ 'record_allowed_iframe_origins': [],
27437
28439
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
27438
28440
  'record_block_selector': 'img, video, audio',
27439
28441
  'record_canvas': false,
27440
28442
  'record_collect_fonts': false,
27441
28443
  'record_console': true,
27442
28444
  'record_heatmap_data': false,
28445
+ 'recording_event_triggers': {},
27443
28446
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
27444
28447
  'record_mask_inputs': true,
27445
28448
  'record_max_ms': MAX_RECORDING_MS,
27446
28449
  'record_min_ms': 0,
28450
+ 'record_network': false,
28451
+ 'record_network_options': {},
27447
28452
  'record_sessions_percent': 0,
27448
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
27449
- 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
28453
+ 'recorder_src': null,
28454
+ 'targeting_src': null,
28455
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
27450
28456
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
27451
28457
  };
27452
28458
 
@@ -27600,6 +28606,19 @@ MixpanelLib.prototype._init = function(token, config, name) {
27600
28606
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
27601
28607
  }));
27602
28608
 
28609
+ this.recorderManager = new RecorderManager({
28610
+ mixpanelInstance: this,
28611
+ getConfigFunc: _.bind(this.get_config, this),
28612
+ setConfigFunc: _.bind(this.set_config, this),
28613
+ getTabIdFunc: _.bind(this.get_tab_id, this),
28614
+ reportErrorFunc: _.bind(this.report_error, this),
28615
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
28616
+ recorderSrc: this.get_config('recorder_src'),
28617
+ targetingSrc: this.get_config('targeting_src'),
28618
+ libBasePath: this.get_config('lib_base_path'),
28619
+ loadExtraBundle: load_extra_bundle
28620
+ });
28621
+
27603
28622
  this['_jsc'] = NOOP_FUNC;
27604
28623
 
27605
28624
  this.__dom_loaded_queue = [];
@@ -27678,7 +28697,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
27678
28697
  getPropertyFunc: _.bind(this.get_property, this),
27679
28698
  trackingFunc: _.bind(this.track, this),
27680
28699
  loadExtraBundle: load_extra_bundle,
27681
- targetingSrc: this.get_config('targeting_src')
28700
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
27682
28701
  });
27683
28702
  this.flags.init();
27684
28703
  this['flags'] = this.flags;
@@ -27691,11 +28710,11 @@ MixpanelLib.prototype._init = function(token, config, name) {
27691
28710
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
27692
28711
  var mode = this.get_config('remote_settings_mode');
27693
28712
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
27694
- this._fetch_remote_settings(mode).then(_.bind(function() {
27695
- this._check_and_start_session_recording();
28713
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
28714
+ return this._check_and_start_session_recording();
27696
28715
  }, this));
27697
28716
  } else {
27698
- this._check_and_start_session_recording();
28717
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
27699
28718
  }
27700
28719
  };
27701
28720
 
@@ -27739,132 +28758,50 @@ MixpanelLib.prototype.get_tab_id = function () {
27739
28758
  return this.tab_id || null;
27740
28759
  };
27741
28760
 
27742
- MixpanelLib.prototype._should_load_recorder = function () {
27743
- if (this.get_config('disable_persistence')) {
27744
- console$1.log('Load recorder check skipped due to disable_persistence config');
27745
- return Promise.resolve(false);
27746
- }
27747
-
27748
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
27749
- var tab_id = this.get_tab_id();
27750
- return recording_registry_idb.init()
27751
- .then(function () {
27752
- return recording_registry_idb.getAll();
27753
- })
27754
- .then(function (recordings) {
27755
- for (var i = 0; i < recordings.length; i++) {
27756
- // if there are expired recordings in the registry, we should load the recorder to flush them
27757
- // if there's a recording for this tab id, we should load the recorder to continue the recording
27758
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
27759
- return true;
27760
- }
27761
- }
27762
- return false;
27763
- })
27764
- .catch(_.bind(function (err) {
27765
- this.report_error('Error checking recording registry', err);
27766
- }, this));
27767
- };
27768
-
27769
28761
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
27770
- if (!win['MutationObserver']) {
27771
- console$1.critical('Browser does not support MutationObserver; skipping session recording');
27772
- return;
27773
- }
27774
-
27775
- var loadRecorder = _.bind(function(startNewIfInactive) {
27776
- var handleLoadedRecorder = _.bind(function() {
27777
- this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
27778
- this._recorder['resumeRecording'](startNewIfInactive);
27779
- }, this);
27780
-
27781
- if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
27782
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
27783
- } else {
27784
- handleLoadedRecorder();
27785
- }
27786
- }, this);
27787
-
27788
- /**
27789
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
27790
- * 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.
27791
- */
27792
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
27793
- if (force_start || is_sampled) {
27794
- loadRecorder(true);
27795
- } else {
27796
- this._should_load_recorder()
27797
- .then(function (shouldLoad) {
27798
- if (shouldLoad) {
27799
- loadRecorder(false);
27800
- }
27801
- });
27802
- }
28762
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
27803
28763
  });
27804
28764
 
28765
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
28766
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
28767
+ };
28768
+
27805
28769
  MixpanelLib.prototype.start_session_recording = function () {
27806
- this._check_and_start_session_recording(true);
28770
+ return this._check_and_start_session_recording(true);
27807
28771
  };
27808
28772
 
27809
28773
  MixpanelLib.prototype.stop_session_recording = function () {
27810
- if (this._recorder) {
27811
- return this._recorder['stopRecording']();
27812
- }
27813
- return Promise.resolve();
28774
+ return this.recorderManager.stopSessionRecording();
27814
28775
  };
27815
28776
 
27816
28777
  MixpanelLib.prototype.pause_session_recording = function () {
27817
- if (this._recorder) {
27818
- return this._recorder['pauseRecording']();
27819
- }
27820
- return Promise.resolve();
28778
+ return this.recorderManager.pauseSessionRecording();
27821
28779
  };
27822
28780
 
27823
28781
  MixpanelLib.prototype.resume_session_recording = function () {
27824
- if (this._recorder) {
27825
- return this._recorder['resumeRecording']();
27826
- }
27827
- return Promise.resolve();
28782
+ return this.recorderManager.resumeSessionRecording();
27828
28783
  };
27829
28784
 
27830
28785
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
27831
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
28786
+ return this.recorderManager.isRecordingHeatmapData();
27832
28787
  };
27833
28788
 
27834
28789
  MixpanelLib.prototype.get_session_recording_properties = function () {
27835
- var props = {};
27836
- var replay_id = this._get_session_replay_id();
27837
- if (replay_id) {
27838
- props['$mp_replay_id'] = replay_id;
27839
- }
27840
- return props;
28790
+ return this.recorderManager.getSessionRecordingProperties();
27841
28791
  };
27842
28792
 
27843
28793
  MixpanelLib.prototype.get_session_replay_url = function () {
27844
- var replay_url = null;
27845
- var replay_id = this._get_session_replay_id();
27846
- if (replay_id) {
27847
- var query_params = _.HTTPBuildQuery({
27848
- 'replay_id': replay_id,
27849
- 'distinct_id': this.get_distinct_id(),
27850
- 'token': this.get_config('token')
27851
- });
27852
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
27853
- }
27854
- return replay_url;
27855
- };
27856
-
27857
- MixpanelLib.prototype._get_session_replay_id = function () {
27858
- var replay_id = null;
27859
- if (this._recorder) {
27860
- replay_id = this._recorder['replayId'];
27861
- }
27862
- return replay_id || null;
28794
+ return this.recorderManager.getSessionReplayUrl();
27863
28795
  };
27864
28796
 
27865
28797
  // "private" public method to reach into the recorder in test cases
27866
28798
  MixpanelLib.prototype.__get_recorder = function () {
27867
- return this._recorder;
28799
+ return this.recorderManager.getRecorder();
28800
+ };
28801
+
28802
+ // "private" public method to get session recording init promise in test cases
28803
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
28804
+ return this.__session_recording_init_promise;
27868
28805
  };
27869
28806
 
27870
28807
  // Private methods
@@ -28122,6 +29059,7 @@ MixpanelLib.prototype._send_request = function(url, data, options, callback) {
28122
29059
  };
28123
29060
 
28124
29061
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
29062
+ var self = this;
28125
29063
  var disableRecordingIfStrict = function() {
28126
29064
  if (mode === 'strict') {
28127
29065
  self.set_config({'record_sessions_percent': 0});
@@ -28142,7 +29080,6 @@ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
28142
29080
  };
28143
29081
  var query_string = _.HTTPBuildQuery(request_params);
28144
29082
  var full_url = settings_endpoint + '?' + query_string;
28145
- var self = this;
28146
29083
 
28147
29084
  var abortController = new AbortController();
28148
29085
  var timeout_id = setTimeout(function() {
@@ -28334,6 +29271,34 @@ MixpanelLib.prototype.push = function(item) {
28334
29271
  this._execute_array([item]);
28335
29272
  };
28336
29273
 
29274
+ /**
29275
+ * Enables events on the Mixpanel object. If passed no arguments,
29276
+ * this function enable tracking of all events. If passed an
29277
+ * array of event names, those events will be enabled, but other
29278
+ * existing disabled events will continue to be not tracked.
29279
+ *
29280
+ * @param {Array} [events] An array of event names to enable
29281
+ */
29282
+ MixpanelLib.prototype.enable = function(events) {
29283
+ var keys, new_disabled_events, i, j;
29284
+
29285
+ if (typeof(events) === 'undefined') {
29286
+ this._flags.disable_all_events = false;
29287
+ } else {
29288
+ keys = {};
29289
+ new_disabled_events = [];
29290
+ for (i = 0; i < events.length; i++) {
29291
+ keys[events[i]] = true;
29292
+ }
29293
+ for (j = 0; j < this.__disabled_events.length; j++) {
29294
+ if (!keys[this.__disabled_events[j]]) {
29295
+ new_disabled_events.push(this.__disabled_events[j]);
29296
+ }
29297
+ }
29298
+ this.__disabled_events = new_disabled_events;
29299
+ }
29300
+ };
29301
+
28337
29302
  /**
28338
29303
  * Disable events on the Mixpanel object. If passed no arguments,
28339
29304
  * this function disables tracking of any event. If passed an
@@ -28507,6 +29472,8 @@ MixpanelLib.prototype.track = addOptOutCheckMixpanelLib(function(event_name, pro
28507
29472
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
28508
29473
  }
28509
29474
 
29475
+ this._start_recording_on_event(event_name, properties);
29476
+
28510
29477
  var data = {
28511
29478
  'event': event_name,
28512
29479
  'properties': properties
@@ -29715,6 +30682,7 @@ MixpanelLib.prototype.remove_hook = function(hook_name, hook_fn) {
29715
30682
  // MixpanelLib Exports
29716
30683
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
29717
30684
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
30685
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
29718
30686
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
29719
30687
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
29720
30688
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -29758,6 +30726,7 @@ MixpanelLib.prototype['DEFAULT_API_ROUTES'] = DEFAULT_API_ROUTES
29758
30726
 
29759
30727
  // Exports intended only for testing
29760
30728
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
30729
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
29761
30730
 
29762
30731
  // MixpanelPersistence Exports
29763
30732
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;