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
@@ -26,16 +26,19 @@
26
26
  win = window;
27
27
  }
28
28
 
29
- /**
30
- * Shared global window property names used across modules
31
- */
29
+ var Config = {
30
+ DEBUG: false,
31
+ LIB_VERSION: '2.77.0'
32
+ };
32
33
 
33
- // Targeting library global (used by flags and targeting modules)
34
+ // Window global names for async modules
34
35
  var TARGETING_GLOBAL_NAME = '__mp_targeting';
35
-
36
- // Recorder library global (used by recorder and mixpanel-core)
37
36
  var RECORDER_GLOBAL_NAME = '__mp_recorder';
38
37
 
38
+ // Constants that are injected at build-time for the names of async modules.
39
+ var RECORDER_FILENAME = '__MP_RECORDER_FILENAME__';
40
+ var TARGETING_FILENAME = '__MP_TARGETING_FILENAME__';
41
+
39
42
  function _array_like_to_array(arr, len) {
40
43
  if (len == null || len > arr.length) len = arr.length;
41
44
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -10733,13 +10736,7 @@
10733
10736
  };
10734
10737
  while(_this.mapRemoves.length){
10735
10738
  var removedNode = _this.mapRemoves.shift();
10736
- if (removedNode.nodeName === "IFRAME") {
10737
- try {
10738
- _this.iframeManager.removeIframe(removedNode);
10739
- } catch (e2) {}
10740
- } else {
10741
- _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10742
- }
10739
+ _this.cleanupRemovedNode(removedNode);
10743
10740
  _this.mirror.removeNodeFromMap(removedNode);
10744
10741
  }
10745
10742
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
@@ -11059,6 +11056,20 @@
11059
11056
  }
11060
11057
  }
11061
11058
  });
11059
+ __publicField$1(this, "cleanupRemovedNode", function(node2) {
11060
+ if (node2.nodeName === "IFRAME") {
11061
+ try {
11062
+ _this.iframeManager.removeIframe(node2);
11063
+ } catch (e2) {}
11064
+ } else {
11065
+ try {
11066
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
11067
+ } catch (e2) {}
11068
+ }
11069
+ node2.childNodes.forEach(function(child) {
11070
+ _this.cleanupRemovedNode(child);
11071
+ });
11072
+ });
11062
11073
  }
11063
11074
  var _proto = MutationBuffer.prototype;
11064
11075
  _proto.init = function init(options) {
@@ -13286,6 +13297,31 @@
13286
13297
  _proto.destroy = function destroy() {};
13287
13298
  return ProcessedNodeManager;
13288
13299
  }();
13300
+ function toOrigin(url) {
13301
+ try {
13302
+ var origin = new URL(url).origin;
13303
+ return origin !== "null" ? origin : null;
13304
+ } catch (e) {
13305
+ return null;
13306
+ }
13307
+ }
13308
+ function buildAllowedOriginSet(origins) {
13309
+ if (!Array.isArray(origins) || origins.length === 0) {
13310
+ throw new Error("[rrweb] allowedIframeOrigins must be a non-empty array of origin strings.");
13311
+ }
13312
+ var set = /* @__PURE__ */ new Set();
13313
+ for(var i2 = 0; i2 < origins.length; i2++){
13314
+ var entry = origins[i2];
13315
+ if (typeof entry !== "string") {
13316
+ throw new Error("[rrweb] allowedIframeOrigins[" + i2 + "] must be a string, got " + (typeof entry === "undefined" ? "undefined" : _type_of(entry)) + ".");
13317
+ }
13318
+ var origin = toOrigin(entry);
13319
+ if (origin) {
13320
+ set.add(origin);
13321
+ }
13322
+ }
13323
+ return Object.freeze(set);
13324
+ }
13289
13325
  var wrappedEmit;
13290
13326
  var takeFullSnapshot$1;
13291
13327
  var canvasManager;
@@ -13307,10 +13343,17 @@
13307
13343
  var mirror = createMirror$2();
13308
13344
  function record(options) {
13309
13345
  if (options === void 0) options = {};
13310
- 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() {
13346
+ 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() {
13311
13347
  return false;
13312
13348
  } : _options_keepIframeSrcFn, _options_ignoreCSSAttributes = options.ignoreCSSAttributes, ignoreCSSAttributes = _options_ignoreCSSAttributes === void 0 ? /* @__PURE__ */ new Set([]) : _options_ignoreCSSAttributes, errorHandler2 = options.errorHandler;
13313
13349
  registerErrorHandler(errorHandler2);
13350
+ var validatedOrigins;
13351
+ if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
13352
+ validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
13353
+ if (validatedOrigins.size === 0) {
13354
+ validatedOrigins = void 0;
13355
+ }
13356
+ }
13314
13357
  var inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
13315
13358
  var passEmitsToParent = false;
13316
13359
  if (!inEmittingFrame) {
@@ -13402,7 +13445,14 @@
13402
13445
  origin: window.location.origin,
13403
13446
  isCheckout: isCheckout
13404
13447
  };
13405
- window.parent.postMessage(message, "*");
13448
+ if (validatedOrigins) {
13449
+ for(var _iterator = _create_for_of_iterator_helper_loose(validatedOrigins), _step; !(_step = _iterator()).done;){
13450
+ var targetOrigin = _step.value;
13451
+ window.parent.postMessage(message, targetOrigin);
13452
+ }
13453
+ } else {
13454
+ window.parent.postMessage(message, "*");
13455
+ }
13406
13456
  }
13407
13457
  if (e2.type === EventType.FullSnapshot) {
13408
13458
  lastFullSnapshotEvent = e2;
@@ -18135,7 +18185,7 @@
18135
18185
  var __publicField = function(obj, key, value) {
18136
18186
  return __defNormalProp(obj, (typeof key === "undefined" ? "undefined" : _type_of(key)) !== "symbol" ? key + "" : key, value);
18137
18187
  };
18138
- function patch(source, name, replacement) {
18188
+ function patch$3(source, name, replacement) {
18139
18189
  try {
18140
18190
  if (!(name in source)) {
18141
18191
  return function() {};
@@ -18552,7 +18602,7 @@
18552
18602
  if (!_logger[level]) {
18553
18603
  return function() {};
18554
18604
  }
18555
- return patch(_logger, level, function(original) {
18605
+ return patch$3(_logger, level, function(original) {
18556
18606
  var _this1 = _this;
18557
18607
  return function() {
18558
18608
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
@@ -18973,11 +19023,6 @@
18973
19023
  PromisePolyfill = NpoPromise;
18974
19024
  }
18975
19025
 
18976
- var Config = {
18977
- DEBUG: false,
18978
- LIB_VERSION: '2.75.0'
18979
- };
18980
-
18981
19026
  /* eslint camelcase: "off", eqeqeq: "off" */
18982
19027
 
18983
19028
  // Maximum allowed session recording length
@@ -20709,6 +20754,17 @@
20709
20754
 
20710
20755
  var NOOP_FUNC = function () {};
20711
20756
 
20757
+ var urlMatchesRegexList = function (url, regexList) {
20758
+ var matches = false;
20759
+ for (var i = 0; i < regexList.length; i++) {
20760
+ if (url.match(regexList[i])) {
20761
+ matches = true;
20762
+ break;
20763
+ }
20764
+ }
20765
+ return matches;
20766
+ };
20767
+
20712
20768
  var JSONStringify = null, JSONParse = null;
20713
20769
  if (typeof JSON !== 'undefined') {
20714
20770
  JSONStringify = JSON.stringify;
@@ -21180,7 +21236,7 @@
21180
21236
  };
21181
21237
  }
21182
21238
 
21183
- var logger$6 = console_with_prefix('lock');
21239
+ var logger$8 = console_with_prefix('lock');
21184
21240
 
21185
21241
  /**
21186
21242
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -21232,7 +21288,7 @@
21232
21288
 
21233
21289
  var delay = function(cb) {
21234
21290
  if (new Date().getTime() - startTime > timeoutMS) {
21235
- logger$6.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21291
+ logger$8.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21236
21292
  storage.removeItem(keyZ);
21237
21293
  storage.removeItem(keyY);
21238
21294
  loop();
@@ -21379,7 +21435,7 @@
21379
21435
  }, this));
21380
21436
  };
21381
21437
 
21382
- var logger$5 = console_with_prefix('batch');
21438
+ var logger$7 = console_with_prefix('batch');
21383
21439
 
21384
21440
  /**
21385
21441
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -21408,7 +21464,7 @@
21408
21464
  timeoutMS: options.sharedLockTimeoutMS,
21409
21465
  });
21410
21466
  }
21411
- this.reportError = options.errorReporter || _.bind(logger$5.error, logger$5);
21467
+ this.reportError = options.errorReporter || _.bind(logger$7.error, logger$7);
21412
21468
 
21413
21469
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
21414
21470
 
@@ -21741,7 +21797,7 @@
21741
21797
  // maximum interval between request retries after exponential backoff
21742
21798
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
21743
21799
 
21744
- var logger$4 = console_with_prefix('batch');
21800
+ var logger$6 = console_with_prefix('batch');
21745
21801
 
21746
21802
  /**
21747
21803
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -21869,7 +21925,7 @@
21869
21925
  */
21870
21926
  RequestBatcher.prototype.flush = function(options) {
21871
21927
  if (this.requestInProgress) {
21872
- logger$4.log('Flush: Request already in progress');
21928
+ logger$6.log('Flush: Request already in progress');
21873
21929
  return PromisePolyfill.resolve();
21874
21930
  }
21875
21931
 
@@ -22046,7 +22102,7 @@
22046
22102
  if (options.unloading) {
22047
22103
  requestOptions.transport = 'sendBeacon';
22048
22104
  }
22049
- logger$4.log('MIXPANEL REQUEST:', dataForRequest);
22105
+ logger$6.log('MIXPANEL REQUEST:', dataForRequest);
22050
22106
  return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
22051
22107
  }, this))
22052
22108
  .catch(_.bind(function(err) {
@@ -22059,7 +22115,7 @@
22059
22115
  * Log error to global logger and optional user-defined logger.
22060
22116
  */
22061
22117
  RequestBatcher.prototype.reportError = function(msg, err) {
22062
- logger$4.error.apply(logger$4.error, arguments);
22118
+ logger$6.error.apply(logger$6.error, arguments);
22063
22119
  if (this.errorReporter) {
22064
22120
  try {
22065
22121
  if (!(err instanceof Error)) {
@@ -22067,7 +22123,7 @@
22067
22123
  }
22068
22124
  this.errorReporter(msg, err);
22069
22125
  } catch(err) {
22070
- logger$4.error(err);
22126
+ logger$6.error(err);
22071
22127
  }
22072
22128
  }
22073
22129
  };
@@ -22084,6 +22140,29 @@
22084
22140
 
22085
22141
  var RECORD_ENQUEUE_THROTTLE_MS = 250;
22086
22142
 
22143
+ var validateAllowedOrigins = function(origins, logger) {
22144
+ if (!_.isArray(origins)) {
22145
+ if (origins) {
22146
+ logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
22147
+ }
22148
+ return [];
22149
+ }
22150
+ var valid = [];
22151
+ for (var i = 0; i < origins.length; i++) {
22152
+ try {
22153
+ var origin = new URL(origins[i]).origin;
22154
+ if (origin === 'null') {
22155
+ logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
22156
+ continue;
22157
+ }
22158
+ valid.push(origin);
22159
+ } catch (e) {
22160
+ logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
22161
+ }
22162
+ }
22163
+ return valid;
22164
+ };
22165
+
22087
22166
  // stateless utils
22088
22167
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
22089
22168
 
@@ -22189,7 +22268,7 @@
22189
22268
 
22190
22269
  var MAX_DEPTH = 5;
22191
22270
 
22192
- var logger$3 = console_with_prefix('autocapture');
22271
+ var logger$5 = console_with_prefix('autocapture');
22193
22272
 
22194
22273
 
22195
22274
  function getClasses(el) {
@@ -22453,7 +22532,7 @@
22453
22532
  return false;
22454
22533
  }
22455
22534
  } catch (err) {
22456
- logger$3.critical('Error while checking element in allowElementCallback', err);
22535
+ logger$5.critical('Error while checking element in allowElementCallback', err);
22457
22536
  return false;
22458
22537
  }
22459
22538
  }
@@ -22470,7 +22549,7 @@
22470
22549
  return true;
22471
22550
  }
22472
22551
  } catch (err) {
22473
- logger$3.critical('Error while checking selector: ' + sel, err);
22552
+ logger$5.critical('Error while checking selector: ' + sel, err);
22474
22553
  }
22475
22554
  }
22476
22555
  return false;
@@ -22485,7 +22564,7 @@
22485
22564
  return true;
22486
22565
  }
22487
22566
  } catch (err) {
22488
- logger$3.critical('Error while checking element in blockElementCallback', err);
22567
+ logger$5.critical('Error while checking element in blockElementCallback', err);
22489
22568
  return true;
22490
22569
  }
22491
22570
  }
@@ -22499,7 +22578,7 @@
22499
22578
  return true;
22500
22579
  }
22501
22580
  } catch (err) {
22502
- logger$3.critical('Error while checking selector: ' + sel, err);
22581
+ logger$5.critical('Error while checking selector: ' + sel, err);
22503
22582
  }
22504
22583
  }
22505
22584
  }
@@ -23047,177 +23126,826 @@
23047
23126
  }
23048
23127
 
23049
23128
  /**
23050
- * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23129
+ * This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
23130
+ * the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
23131
+ *
23132
+ * This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
23133
+ * header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
23134
+ *
23051
23135
  */
23052
23136
 
23137
+ var logger$4 = console_with_prefix('network-plugin');
23053
23138
 
23054
- var logger$2 = console_with_prefix('recorder');
23055
- var CompressionStream = win['CompressionStream'];
23056
-
23057
- var RECORDER_BATCHER_LIB_CONFIG = {
23058
- 'batch_size': 1000,
23059
- 'batch_flush_interval_ms': 10 * 1000,
23060
- 'batch_request_timeout_ms': 90 * 1000,
23061
- 'batch_autostart': true
23062
- };
23139
+ /**
23140
+ * Get the time origin for converting performance timestamps to absolute timestamps.
23141
+ * Uses Date.now() - performance.now() instead of performance.timeOrigin because
23142
+ * browsers can report timeOrigin values that are skewed from actual time, and some
23143
+ * browsers (notably older Safari versions) don't implement timeOrigin at all.
23144
+ * See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
23145
+ * @param {Window} win
23146
+ * @returns {number}
23147
+ */
23148
+ function getTimeOrigin(win) {
23149
+ return Math.round(Date.now() - win.performance.now());
23150
+ }
23063
23151
 
23064
- var ACTIVE_SOURCES = new Set([
23065
- IncrementalSource.MouseMove,
23066
- IncrementalSource.MouseInteraction,
23067
- IncrementalSource.Scroll,
23068
- IncrementalSource.ViewportResize,
23069
- IncrementalSource.Input,
23070
- IncrementalSource.TouchMove,
23071
- IncrementalSource.MediaInteraction,
23072
- IncrementalSource.Drag,
23073
- IncrementalSource.Selection,
23074
- ]);
23152
+ /**
23153
+ * @typedef {import('../index.d.ts').InitiatorType} InitiatorType
23154
+ * @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
23155
+ * @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
23156
+ * @typedef {import('../index.d.ts').NetworkData} NetworkData
23157
+ */
23075
23158
 
23076
- function isUserEvent(ev) {
23077
- return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23078
- }
23159
+ /**
23160
+ * @typedef {Record<string, string>} Headers
23161
+ */
23079
23162
 
23080
23163
  /**
23081
- * @typedef {Object} SerializedRecording
23082
- * @property {number} idleExpires
23083
- * @property {number} maxExpires
23084
- * @property {number} replayStartTime
23085
- * @property {number} lastEventTimestamp
23086
- * @property {number} seqNo
23087
- * @property {string} batchStartUrl
23088
- * @property {string} replayId
23089
- * @property {string} tabId
23090
- * @property {string} replayStartUrl
23164
+ * @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
23091
23165
  */
23092
23166
 
23093
23167
  /**
23094
- * @typedef {Object} SessionRecordingOptions
23095
- * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23096
- * @property {String} [options.replayId] - unique uuid for a single replay
23097
- * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23098
- * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23099
- * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23100
- * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23101
- * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23102
- * optional properties for deserialization:
23103
- * @property {number} idleExpires
23104
- * @property {number} maxExpires
23105
- * @property {number} replayStartTime
23106
- * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23107
- * @property {number} seqNo
23108
- * @property {string} batchStartUrl
23109
- * @property {string} replayStartUrl
23168
+ * @callback networkCallback
23169
+ * @param {NetworkData} data
23170
+ * @returns {void}
23110
23171
  */
23111
23172
 
23112
23173
  /**
23113
- * @typedef {Object} UserIdInfo
23114
- * @property {string} distinct_id
23115
- * @property {string} user_id
23116
- * @property {string} device_id
23174
+ * @callback listenerHandler
23175
+ * @returns {void}
23117
23176
  */
23118
23177
 
23178
+ /**
23179
+ * @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
23180
+ */
23119
23181
 
23120
23182
  /**
23121
- * This class encapsulates a single session recording and its lifecycle.
23122
- * @param {SessionRecordingOptions} options
23183
+ * @typedef {Object} RecordPlugin
23184
+ * @property {string} name
23185
+ * @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
23186
+ * @property {NetworkRecordOptions} [options]
23123
23187
  */
23124
- var SessionRecording = function(options) {
23125
- this._mixpanel = options.mixpanelInstance;
23126
- this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23127
- this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23128
- this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23129
- this._rrwebRecord = options.rrwebRecord || null;
23130
23188
 
23131
- // internal rrweb stopRecording function
23132
- this._stopRecording = null;
23133
- this.replayId = options.replayId;
23189
+ /** @type {Required<NetworkRecordOptions>} */
23190
+ var defaultNetworkOptions = {
23191
+ initiatorTypes: [
23192
+ 'audio',
23193
+ 'beacon',
23194
+ 'body',
23195
+ 'css',
23196
+ 'early-hint',
23197
+ 'embed',
23198
+ 'fetch',
23199
+ 'frame',
23200
+ 'iframe',
23201
+ 'icon',
23202
+ 'image',
23203
+ 'img',
23204
+ 'input',
23205
+ 'link',
23206
+ 'navigation',
23207
+ 'object',
23208
+ 'ping',
23209
+ 'script',
23210
+ 'track',
23211
+ 'video',
23212
+ 'xmlhttprequest',
23213
+ ],
23214
+ ignoreRequestFn: function() { return false; },
23215
+ recordHeaders: {
23216
+ request: [],
23217
+ response: [],
23218
+ },
23219
+ recordBodyUrls: {
23220
+ request: [],
23221
+ response: [],
23222
+ },
23223
+ recordInitialRequests: false,
23224
+ };
23134
23225
 
23135
- this.batchStartUrl = options.batchStartUrl || null;
23136
- this.replayStartUrl = options.replayStartUrl || null;
23137
- this.idleExpires = options.idleExpires || null;
23138
- this.maxExpires = options.maxExpires || null;
23139
- this.replayStartTime = options.replayStartTime || null;
23140
- this.lastEventTimestamp = options.lastEventTimestamp || null;
23141
- this.seqNo = options.seqNo || 0;
23226
+ /**
23227
+ * @param {PerformanceEntry} entry
23228
+ * @returns {entry is PerformanceNavigationTiming}
23229
+ */
23230
+ function isNavigationTiming(entry) {
23231
+ return entry.entryType === 'navigation';
23232
+ }
23142
23233
 
23143
- this.idleTimeoutId = null;
23144
- this.maxTimeoutId = null;
23234
+ /**
23235
+ * @param {PerformanceEntry} entry
23236
+ * @returns {entry is PerformanceResourceTiming}
23237
+ */
23238
+ function isResourceTiming (entry) {
23239
+ return entry.entryType === 'resource';
23240
+ }
23145
23241
 
23146
- this.recordMaxMs = MAX_RECORDING_MS;
23147
- this.recordMinMs = 0;
23242
+ function findLast(array, predicate) {
23243
+ var length = array.length;
23244
+ for (var i = length - 1; i >= 0; i -= 1) {
23245
+ if (predicate(array[i])) {
23246
+ return array[i];
23247
+ }
23248
+ }
23249
+ }
23148
23250
 
23149
- // disable persistence if localStorage is not supported
23150
- // request-queue will automatically disable persistence if indexedDB fails to initialize
23151
- var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23251
+ /**
23252
+ * Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
23253
+ * Adapted from Sentry's `fill` utility:
23254
+ * https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
23255
+ *
23256
+ * @param {object} source - The object containing the method to patch
23257
+ * @param {string} name - The method name to patch
23258
+ * @param {function} replacementFactory - A function that receives the original method and returns the replacement
23259
+ * @returns {function} A function that restores the original method
23260
+ */
23261
+ function patch(source, name, replacementFactory) {
23262
+ if (!(name in source) || typeof source[name] !== 'function') {
23263
+ return function() {};
23264
+ }
23265
+ var original = source[name];
23266
+ var wrapped = replacementFactory(original);
23267
+ source[name] = wrapped;
23268
+ return function() {
23269
+ source[name] = original;
23270
+ };
23271
+ }
23152
23272
 
23153
- // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23154
- this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23155
- this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23156
- this.batcher = new RequestBatcher(this.batcherKey, {
23157
- errorReporter: this.reportError.bind(this),
23158
- flushOnlyOnInterval: true,
23159
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
23160
- sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23161
- queueStorage: this.queueStorage,
23162
- sharedLockStorage: options.sharedLockStorage,
23163
- usePersistence: usePersistence,
23164
- stopAllBatchingFunc: this.stopRecording.bind(this),
23165
23273
 
23166
- // increased throttle and shared lock timeout because recording events are very high frequency.
23167
- // this will minimize the amount of lock contention between enqueued events.
23168
- // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23169
- enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23170
- sharedLockTimeoutMS: 10 * 1000,
23171
- });
23172
- };
23274
+ /**
23275
+ * Maximum body size to record (1MB)
23276
+ */
23277
+ var MAX_BODY_SIZE = 1024 * 1024;
23173
23278
 
23174
23279
  /**
23175
- * @returns {UserIdInfo}
23280
+ * Truncate string if it exceeds max size
23281
+ * @param {string} str
23282
+ * @returns {string}
23176
23283
  */
23177
- SessionRecording.prototype.getUserIdInfo = function () {
23178
- if (this.finalFlushUserIdInfo) {
23179
- return this.finalFlushUserIdInfo;
23284
+ function truncateBody(str) {
23285
+ if (!str || typeof str !== 'string') {
23286
+ return str;
23287
+ }
23288
+ if (str.length > MAX_BODY_SIZE) {
23289
+ logger$4.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
23290
+ return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
23180
23291
  }
23292
+ return str;
23293
+ }
23181
23294
 
23182
- var userIdInfo = {
23183
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
23295
+ /**
23296
+ * @param {networkCallback} cb
23297
+ * @param {Window} win
23298
+ * @param {Required<NetworkRecordOptions>} options
23299
+ * @returns {listenerHandler}
23300
+ */
23301
+ function initPerformanceObserver(cb, win, options) {
23302
+ if (!win.PerformanceObserver) {
23303
+ logger$4.error('PerformanceObserver not supported');
23304
+ return function() {
23305
+ //
23306
+ };
23307
+ }
23308
+ if (options.recordInitialRequests) {
23309
+ var initialPerformanceEntries = win.performance
23310
+ .getEntries()
23311
+ .filter(function(entry) {
23312
+ return isNavigationTiming(entry) ||
23313
+ (isResourceTiming(entry) &&
23314
+ options.initiatorTypes.includes(entry.initiatorType));
23315
+ });
23316
+ cb({
23317
+ requests: initialPerformanceEntries.map(function(entry) {
23318
+ return {
23319
+ url: entry.name,
23320
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23321
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23322
+ startTime: Math.round(entry.startTime),
23323
+ endTime: Math.round(entry.responseEnd),
23324
+ timeOrigin: getTimeOrigin(win),
23325
+ };
23326
+ }),
23327
+ isInitial: true,
23328
+ });
23329
+ }
23330
+ var observer = new win.PerformanceObserver(function(entries) {
23331
+ var performanceEntries = entries
23332
+ .getEntries()
23333
+ .filter(function(entry) {
23334
+ return isResourceTiming(entry) &&
23335
+ options.initiatorTypes.includes(entry.initiatorType) &&
23336
+ entry.initiatorType !== 'xmlhttprequest' &&
23337
+ entry.initiatorType !== 'fetch';
23338
+ });
23339
+ cb({
23340
+ requests: performanceEntries.map(function(entry) {
23341
+ return {
23342
+ url: entry.name,
23343
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23344
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23345
+ startTime: Math.round(entry.startTime),
23346
+ endTime: Math.round(entry.responseEnd),
23347
+ timeOrigin: getTimeOrigin(win),
23348
+ };
23349
+ }),
23350
+ });
23351
+ });
23352
+ observer.observe({ entryTypes: ['navigation', 'resource'] });
23353
+ return function() {
23354
+ observer.disconnect();
23184
23355
  };
23356
+ }
23185
23357
 
23186
- // send ID management props if they exist
23187
- var deviceId = this._mixpanel.get_property('$device_id');
23188
- if (deviceId) {
23189
- userIdInfo['$device_id'] = deviceId;
23190
- }
23191
- var userId = this._mixpanel.get_property('$user_id');
23192
- if (userId) {
23193
- userIdInfo['$user_id'] = userId;
23358
+ /**
23359
+ * Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
23360
+ * @param {'request' | 'response'} type
23361
+ * @param {NetworkRecordOptions['recordHeaders']} recordHeaders
23362
+ * @param {string} headerName
23363
+ * @returns {boolean}
23364
+ */
23365
+ function shouldRecordHeader(type, recordHeaders, headerName) {
23366
+ if (!recordHeaders[type] || recordHeaders[type].length === 0) {
23367
+ return false;
23194
23368
  }
23195
- return userIdInfo;
23196
- };
23197
23369
 
23198
- SessionRecording.prototype.unloadPersistedData = function () {
23199
- this.batcher.stop();
23370
+ return recordHeaders[type].includes(headerName.toLowerCase());
23371
+ }
23200
23372
 
23201
- return this.queueStorage.init().catch(function () {
23202
- this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23203
- }.bind(this)).then(function () {
23204
- // if the recording is too short, just delete any stored events without flushing
23205
- if (this.getDurationMs() < this._getRecordMinMs()) {
23206
- return this.queueStorage.removeItem(this.batcherKey);
23207
- }
23373
+ /**
23374
+ * Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
23375
+ * @param {'request' | 'response'} type
23376
+ * @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
23377
+ * @param {string} url
23378
+ * @returns {boolean}
23379
+ */
23380
+ function shouldRecordBody(type, recordBodyUrls, url) {
23381
+ if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
23382
+ return false;
23383
+ }
23208
23384
 
23209
- return this.batcher.flush()
23210
- .then(function () {
23211
- return this.queueStorage.removeItem(this.batcherKey);
23212
- }.bind(this));
23213
- }.bind(this));
23214
- };
23385
+ return urlMatchesRegexList(url, recordBodyUrls[type]);
23386
+ }
23215
23387
 
23216
- SessionRecording.prototype.getConfig = function(configVar) {
23217
- return this._mixpanel.get_config(configVar);
23218
- };
23388
+ function tryReadXHRBody(body) {
23389
+ if (body === null || body === undefined) {
23390
+ return null;
23391
+ }
23219
23392
 
23220
- // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
23393
+ var result;
23394
+ if (typeof body === 'string') {
23395
+ result = body;
23396
+ } else if (body instanceof Document) {
23397
+ result = body.textContent;
23398
+ } else if (body instanceof FormData) {
23399
+ result = _.HTTPBuildQuery(body);
23400
+ } else if (_.isObject(body)) {
23401
+ try {
23402
+ result = JSON.stringify(body);
23403
+ } catch (e) {
23404
+ return 'Failed to stringify response object';
23405
+ }
23406
+ } else {
23407
+ return 'Cannot read body of type ' + typeof body;
23408
+ }
23409
+
23410
+ return truncateBody(result);
23411
+ }
23412
+
23413
+ /**
23414
+ * @param {Request | Response} r
23415
+ * @returns {Promise<string>}
23416
+ */
23417
+ function tryReadFetchBody(r) {
23418
+ return new Promise(function(resolve) {
23419
+ var timeout = setTimeout(function() {
23420
+ resolve('Timeout while trying to read body');
23421
+ }, 500);
23422
+ try {
23423
+ r.clone()
23424
+ .text()
23425
+ .then(
23426
+ function(txt) {
23427
+ clearTimeout(timeout);
23428
+ resolve(truncateBody(txt));
23429
+ },
23430
+ function(reason) {
23431
+ clearTimeout(timeout);
23432
+ resolve('Failed to read body: ' + String(reason));
23433
+ }
23434
+ );
23435
+ } catch (e) {
23436
+ clearTimeout(timeout);
23437
+ resolve('Failed to read body: ' + String(e));
23438
+ }
23439
+ });
23440
+ }
23441
+
23442
+ /**
23443
+ * @param {Window} win
23444
+ * @param {string} initiatorType
23445
+ * @param {string} url
23446
+ * @param {number} [after]
23447
+ * @param {number} [before]
23448
+ * @param {number} [attempt]
23449
+ * @returns {Promise<PerformanceResourceTiming>}
23450
+ */
23451
+ function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
23452
+ if (attempt === undefined) {
23453
+ attempt = 0;
23454
+ }
23455
+ if (attempt > 10) {
23456
+ logger$4.error('Cannot find performance entry');
23457
+ return Promise.resolve(null);
23458
+ }
23459
+ var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
23460
+ win.performance.getEntriesByName(url)
23461
+ );
23462
+ var performanceEntry = findLast(
23463
+ urlPerformanceEntries,
23464
+ function(entry) {
23465
+ return isResourceTiming(entry) &&
23466
+ entry.initiatorType === initiatorType &&
23467
+ (!after || entry.startTime >= after) &&
23468
+ (!before || entry.startTime <= before);
23469
+ }
23470
+ );
23471
+ if (!performanceEntry) {
23472
+ return new Promise(function(resolve) {
23473
+ setTimeout(resolve, 50 * attempt);
23474
+ }).then(function() {
23475
+ return getRequestPerformanceEntry(
23476
+ win,
23477
+ initiatorType,
23478
+ url,
23479
+ after,
23480
+ before,
23481
+ attempt + 1
23482
+ );
23483
+ });
23484
+ }
23485
+ return Promise.resolve(performanceEntry);
23486
+ }
23487
+
23488
+ /**
23489
+ * @param {networkCallback} cb
23490
+ * @param {Window} win
23491
+ * @param {Required<NetworkRecordOptions>} options
23492
+ * @returns {listenerHandler}
23493
+ */
23494
+ function initXhrObserver(cb, win, options) {
23495
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
23496
+ return function() {
23497
+ //
23498
+ };
23499
+ }
23500
+ var restorePatch = patch(
23501
+ win.XMLHttpRequest.prototype,
23502
+ 'open',
23503
+ function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
23504
+ return function(
23505
+ /** @type {string} */ method,
23506
+ /** @type {string | URL} */ url,
23507
+ /** @type {boolean} */ async,
23508
+ username, password
23509
+ ) {
23510
+ if (async === undefined) {
23511
+ async = true;
23512
+ }
23513
+ var xhr = /** @type {XMLHttpRequest} */ (this);
23514
+ var req = new Request(url, { method: method });
23515
+ /** @type {Partial<NetworkRequest>} */
23516
+ var networkRequest = {};
23517
+ /** @type {number | undefined} */
23518
+ var after;
23519
+ /** @type {number | undefined} */
23520
+ var before;
23521
+
23522
+ /** @type {Headers} */
23523
+ var requestHeaders = {};
23524
+ var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
23525
+ xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
23526
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23527
+ requestHeaders[header] = value;
23528
+ }
23529
+ return originalSetRequestHeader(header, value);
23530
+ };
23531
+ networkRequest.requestHeaders = requestHeaders;
23532
+
23533
+ var originalSend = xhr.send.bind(xhr);
23534
+ xhr.send = function(/** @type {Body} */ body) {
23535
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23536
+ networkRequest.requestBody = tryReadXHRBody(body);
23537
+ }
23538
+ after = win.performance.now();
23539
+ return originalSend(body);
23540
+ };
23541
+ xhr.addEventListener('readystatechange', function() {
23542
+ if (xhr.readyState !== xhr.DONE) {
23543
+ return;
23544
+ }
23545
+ before = win.performance.now();
23546
+ /** @type {Headers} */
23547
+ var responseHeaders = {};
23548
+ var rawHeaders = xhr.getAllResponseHeaders();
23549
+ if (rawHeaders) {
23550
+ var headers = rawHeaders.trim().split(/[\r\n]+/);
23551
+ headers.forEach(function(line) {
23552
+ if (!line) return;
23553
+ var colonIndex = line.indexOf(': ');
23554
+ if (colonIndex === -1) return;
23555
+ var header = line.substring(0, colonIndex);
23556
+ var value = line.substring(colonIndex + 2);
23557
+ if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
23558
+ responseHeaders[header] = value;
23559
+ }
23560
+ });
23561
+ }
23562
+ networkRequest.responseHeaders = responseHeaders;
23563
+ if (
23564
+ shouldRecordBody('response', options.recordBodyUrls, req.url)
23565
+ ) {
23566
+ networkRequest.responseBody = tryReadXHRBody(xhr.response);
23567
+ }
23568
+ getRequestPerformanceEntry(
23569
+ win,
23570
+ 'xmlhttprequest',
23571
+ req.url,
23572
+ after,
23573
+ before
23574
+ )
23575
+ .then(function(entry) {
23576
+ if (!entry) {
23577
+ logger$4.error('Failed to get performance entry for XHR request to ' + req.url);
23578
+ return;
23579
+ }
23580
+ /** @type {NetworkRequest} */
23581
+ var request = {
23582
+ url: entry.name,
23583
+ method: req.method,
23584
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23585
+ status: xhr.status,
23586
+ startTime: Math.round(entry.startTime),
23587
+ endTime: Math.round(entry.responseEnd),
23588
+ timeOrigin: getTimeOrigin(win),
23589
+ requestHeaders: networkRequest.requestHeaders,
23590
+ requestBody: networkRequest.requestBody,
23591
+ responseHeaders: networkRequest.responseHeaders,
23592
+ responseBody: networkRequest.responseBody,
23593
+ };
23594
+ cb({ requests: [request] });
23595
+ })
23596
+ .catch(function(e) {
23597
+ logger$4.error('Error recording XHR request to ' + req.url + ': ' + String(e));
23598
+ });
23599
+ });
23600
+
23601
+ originalOpen.call(xhr, method, url, async, username, password);
23602
+ };
23603
+ }
23604
+ );
23605
+ return function() {
23606
+ restorePatch();
23607
+ };
23608
+ }
23609
+
23610
+ /**
23611
+ * @param {networkCallback} cb
23612
+ * @param {Window} win
23613
+ * @param {Required<NetworkRecordOptions>} options
23614
+ * @returns {listenerHandler}
23615
+ */
23616
+ function initFetchObserver(cb, win, options) {
23617
+ if (!options.initiatorTypes.includes('fetch')) {
23618
+ return function() {
23619
+ //
23620
+ };
23621
+ }
23622
+
23623
+ var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
23624
+ return function() {
23625
+ var req = new Request(arguments[0], arguments[1]);
23626
+ /** @type {Response | undefined} */
23627
+ var res;
23628
+ /** @type {Partial<NetworkRequest>} */
23629
+ var networkRequest = {};
23630
+ /** @type {number | undefined} */
23631
+ var after;
23632
+ /** @type {number | undefined} */
23633
+ var before;
23634
+
23635
+ var originalFetchPromise;
23636
+ var requestBodyPromise = Promise.resolve(undefined);
23637
+ var responseBodyPromise = Promise.resolve(undefined);
23638
+ try {
23639
+ /** @type {Headers} */
23640
+ var requestHeaders = {};
23641
+ req.headers.forEach(function(value, header) {
23642
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23643
+ requestHeaders[header] = value;
23644
+ }
23645
+ });
23646
+ networkRequest.requestHeaders = requestHeaders;
23647
+
23648
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23649
+ requestBodyPromise = tryReadFetchBody(req)
23650
+ .then(function(body) {
23651
+ networkRequest.requestBody = body;
23652
+ });
23653
+ }
23654
+
23655
+ after = win.performance.now();
23656
+ originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
23657
+ res = response;
23658
+ before = win.performance.now();
23659
+
23660
+ /** @type {Headers} */
23661
+ var responseHeaders = {};
23662
+ res.headers.forEach(function(value, header) {
23663
+ if (shouldRecordHeader('response', options.recordHeaders, header)) {
23664
+ responseHeaders[header] = value;
23665
+ }
23666
+ });
23667
+ networkRequest.responseHeaders = responseHeaders;
23668
+
23669
+ if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
23670
+ responseBodyPromise = tryReadFetchBody(res)
23671
+ .then(function(body) {
23672
+ networkRequest.responseBody = body;
23673
+ });
23674
+ }
23675
+
23676
+ return res;
23677
+ });
23678
+ } catch (e) {
23679
+ originalFetchPromise = Promise.reject(e);
23680
+ }
23681
+
23682
+ // await concurrently so we don't delay the fetch response
23683
+ Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
23684
+ .then(function () {
23685
+ return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
23686
+ })
23687
+ .then(function(entry) {
23688
+ if (!entry) {
23689
+ logger$4.error('Failed to get performance entry for fetch request to ' + req.url);
23690
+ return;
23691
+ }
23692
+ /** @type {NetworkRequest} */
23693
+ var request = {
23694
+ url: entry.name,
23695
+ method: req.method,
23696
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23697
+ status: res ? res.status : undefined,
23698
+ startTime: Math.round(entry.startTime),
23699
+ endTime: Math.round(entry.responseEnd),
23700
+ timeOrigin: getTimeOrigin(win),
23701
+ requestHeaders: networkRequest.requestHeaders,
23702
+ requestBody: networkRequest.requestBody,
23703
+ responseHeaders: networkRequest.responseHeaders,
23704
+ responseBody: networkRequest.responseBody,
23705
+ };
23706
+ cb({ requests: [request] });
23707
+ })
23708
+ .catch(function (e) {
23709
+ logger$4.error('Error recording fetch request to ' + req.url + ': ' + String(e));
23710
+ });
23711
+
23712
+ return originalFetchPromise;
23713
+ };
23714
+ });
23715
+ return function() {
23716
+ restorePatch();
23717
+ };
23718
+ }
23719
+
23720
+ /**
23721
+ * @param {networkCallback} callback
23722
+ * @param {Window} win
23723
+ * @param {NetworkRecordOptions} options
23724
+ * @returns {listenerHandler}
23725
+ */
23726
+ function initNetworkObserver(callback, win, options) {
23727
+ if (!('performance' in win)) {
23728
+ return function() {
23729
+ //
23730
+ };
23731
+ }
23732
+
23733
+ var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
23734
+ var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
23735
+ options = Object.assign({}, options, {
23736
+ recordHeaders: recordHeaders,
23737
+ recordBodyUrls: recordBodyUrls,
23738
+ });
23739
+ var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
23740
+
23741
+ /** @type {networkCallback} */
23742
+ var cb = function(data) {
23743
+ var requests = data.requests.filter(function(request) {
23744
+ var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
23745
+ return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
23746
+ });
23747
+ if (requests.length > 0 || data.isInitial) {
23748
+ callback(Object.assign({}, data, { requests: requests }));
23749
+ }
23750
+ };
23751
+ var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
23752
+ var xhrObserver = initXhrObserver(cb, win, networkOptions);
23753
+ var fetchObserver = initFetchObserver(cb, win, networkOptions);
23754
+ return function() {
23755
+ performanceObserver();
23756
+ xhrObserver();
23757
+ fetchObserver();
23758
+ };
23759
+ }
23760
+
23761
+ // arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
23762
+ // a changed format in the mixpanel product.
23763
+ var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
23764
+
23765
+ /**
23766
+ * @param {NetworkRecordOptions} [options]
23767
+ * @returns {RecordPlugin}
23768
+ */
23769
+ var getRecordNetworkPlugin = function(options) {
23770
+ return {
23771
+ name: NETWORK_PLUGIN_NAME,
23772
+ observer: initNetworkObserver,
23773
+ options: options,
23774
+ };
23775
+ };
23776
+
23777
+ /**
23778
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23779
+ */
23780
+
23781
+
23782
+ var logger$3 = console_with_prefix('recorder');
23783
+ var CompressionStream = win['CompressionStream'];
23784
+
23785
+ var RECORDER_BATCHER_LIB_CONFIG = {
23786
+ 'batch_size': 1000,
23787
+ 'batch_flush_interval_ms': 10 * 1000,
23788
+ 'batch_request_timeout_ms': 90 * 1000,
23789
+ 'batch_autostart': true
23790
+ };
23791
+
23792
+ var ACTIVE_SOURCES = new Set([
23793
+ IncrementalSource.MouseMove,
23794
+ IncrementalSource.MouseInteraction,
23795
+ IncrementalSource.Scroll,
23796
+ IncrementalSource.ViewportResize,
23797
+ IncrementalSource.Input,
23798
+ IncrementalSource.TouchMove,
23799
+ IncrementalSource.MediaInteraction,
23800
+ IncrementalSource.Drag,
23801
+ IncrementalSource.Selection,
23802
+ ]);
23803
+
23804
+ function isUserEvent(ev) {
23805
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23806
+ }
23807
+
23808
+ /**
23809
+ * @typedef {Object} SerializedRecording
23810
+ * @property {number} idleExpires
23811
+ * @property {number} maxExpires
23812
+ * @property {number} replayStartTime
23813
+ * @property {number} lastEventTimestamp
23814
+ * @property {number} seqNo
23815
+ * @property {string} batchStartUrl
23816
+ * @property {string} replayId
23817
+ * @property {string} tabId
23818
+ * @property {string} replayStartUrl
23819
+ */
23820
+
23821
+ /**
23822
+ * @typedef {Object} SessionRecordingOptions
23823
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23824
+ * @property {String} [options.replayId] - unique uuid for a single replay
23825
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23826
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23827
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23828
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23829
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23830
+ * optional properties for deserialization:
23831
+ * @property {number} idleExpires
23832
+ * @property {number} maxExpires
23833
+ * @property {number} replayStartTime
23834
+ * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23835
+ * @property {number} seqNo
23836
+ * @property {string} batchStartUrl
23837
+ * @property {string} replayStartUrl
23838
+ */
23839
+
23840
+ /**
23841
+ * @typedef {Object} UserIdInfo
23842
+ * @property {string} distinct_id
23843
+ * @property {string} user_id
23844
+ * @property {string} device_id
23845
+ */
23846
+
23847
+
23848
+ /**
23849
+ * This class encapsulates a single session recording and its lifecycle.
23850
+ * @param {SessionRecordingOptions} options
23851
+ */
23852
+ var SessionRecording = function(options) {
23853
+ this._mixpanel = options.mixpanelInstance;
23854
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23855
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23856
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23857
+ this._rrwebRecord = options.rrwebRecord || null;
23858
+
23859
+ // internal rrweb stopRecording function
23860
+ this._stopRecording = null;
23861
+ this.replayId = options.replayId;
23862
+
23863
+ this.batchStartUrl = options.batchStartUrl || null;
23864
+ this.replayStartUrl = options.replayStartUrl || null;
23865
+ this.idleExpires = options.idleExpires || null;
23866
+ this.maxExpires = options.maxExpires || null;
23867
+ this.replayStartTime = options.replayStartTime || null;
23868
+ this.lastEventTimestamp = options.lastEventTimestamp || null;
23869
+ this.seqNo = options.seqNo || 0;
23870
+
23871
+ this.idleTimeoutId = null;
23872
+ this.maxTimeoutId = null;
23873
+
23874
+ this.recordMaxMs = MAX_RECORDING_MS;
23875
+ this.recordMinMs = 0;
23876
+
23877
+ // disable persistence if localStorage is not supported
23878
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
23879
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23880
+
23881
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23882
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23883
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23884
+ this.batcher = new RequestBatcher(this.batcherKey, {
23885
+ errorReporter: this.reportError.bind(this),
23886
+ flushOnlyOnInterval: true,
23887
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
23888
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23889
+ queueStorage: this.queueStorage,
23890
+ sharedLockStorage: options.sharedLockStorage,
23891
+ usePersistence: usePersistence,
23892
+ stopAllBatchingFunc: this.stopRecording.bind(this),
23893
+
23894
+ // increased throttle and shared lock timeout because recording events are very high frequency.
23895
+ // this will minimize the amount of lock contention between enqueued events.
23896
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23897
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23898
+ sharedLockTimeoutMS: 10 * 1000,
23899
+ });
23900
+ };
23901
+
23902
+ /**
23903
+ * @returns {UserIdInfo}
23904
+ */
23905
+ SessionRecording.prototype.getUserIdInfo = function () {
23906
+ if (this.finalFlushUserIdInfo) {
23907
+ return this.finalFlushUserIdInfo;
23908
+ }
23909
+
23910
+ var userIdInfo = {
23911
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
23912
+ };
23913
+
23914
+ // send ID management props if they exist
23915
+ var deviceId = this._mixpanel.get_property('$device_id');
23916
+ if (deviceId) {
23917
+ userIdInfo['$device_id'] = deviceId;
23918
+ }
23919
+ var userId = this._mixpanel.get_property('$user_id');
23920
+ if (userId) {
23921
+ userIdInfo['$user_id'] = userId;
23922
+ }
23923
+ return userIdInfo;
23924
+ };
23925
+
23926
+ SessionRecording.prototype.unloadPersistedData = function () {
23927
+ this.batcher.stop();
23928
+
23929
+ return this.queueStorage.init().catch(function () {
23930
+ this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23931
+ }.bind(this)).then(function () {
23932
+ // if the recording is too short, just delete any stored events without flushing
23933
+ if (this.getDurationMs() < this._getRecordMinMs()) {
23934
+ return this.queueStorage.removeItem(this.batcherKey);
23935
+ }
23936
+
23937
+ return this.batcher.flush()
23938
+ .then(function () {
23939
+ return this.queueStorage.removeItem(this.batcherKey);
23940
+ }.bind(this));
23941
+ }.bind(this));
23942
+ };
23943
+
23944
+ SessionRecording.prototype.getConfig = function(configVar) {
23945
+ return this._mixpanel.get_config(configVar);
23946
+ };
23947
+
23948
+ // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
23221
23949
  // reaches into this class instance and expects the snake case version of the function.
23222
23950
  // eslint-disable-next-line camelcase
23223
23951
  SessionRecording.prototype.get_config = function(configVar) {
@@ -23231,14 +23959,14 @@
23231
23959
  }
23232
23960
 
23233
23961
  if (this._stopRecording !== null) {
23234
- logger$2.log('Recording already in progress, skipping startRecording.');
23962
+ logger$3.log('Recording already in progress, skipping startRecording.');
23235
23963
  return;
23236
23964
  }
23237
23965
 
23238
23966
  this.recordMaxMs = this.getConfig('record_max_ms');
23239
23967
  if (this.recordMaxMs > MAX_RECORDING_MS) {
23240
23968
  this.recordMaxMs = MAX_RECORDING_MS;
23241
- logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
23969
+ logger$3.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
23242
23970
  }
23243
23971
 
23244
23972
  if (!this.maxExpires) {
@@ -23279,6 +24007,31 @@
23279
24007
 
23280
24008
  var privacyConfig = getPrivacyConfig(this._mixpanel);
23281
24009
 
24010
+ var plugins = [];
24011
+ if (this.getConfig('record_network')) {
24012
+ var options = this.getConfig('record_network_options') || {};
24013
+ // don't track requests to Mixpanel /record API
24014
+ var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
24015
+ ignoreRequestUrls.push(this._getApiRoute());
24016
+ options.ignoreRequestUrls = ignoreRequestUrls;
24017
+
24018
+ plugins.push(getRecordNetworkPlugin(options));
24019
+ }
24020
+
24021
+ if (this.getConfig('record_console')) {
24022
+ plugins.push(
24023
+ getRecordConsolePlugin({
24024
+ stringifyOptions: {
24025
+ stringLengthLimit: 1000,
24026
+ numOfKeysLimit: 50,
24027
+ depthOfLimit: 2
24028
+ }
24029
+ })
24030
+ );
24031
+ }
24032
+
24033
+ var validatedOrigins = validateAllowedOrigins(this.getConfig('record_allowed_iframe_origins'), logger$3);
24034
+
23282
24035
  try {
23283
24036
  this._stopRecording = this._rrwebRecord({
23284
24037
  'emit': function (ev) {
@@ -23313,19 +24066,13 @@
23313
24066
  'maskTextSelector': '*',
23314
24067
  'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
23315
24068
  'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
24069
+ 'recordCrossOriginIframes': validatedOrigins.length > 0,
24070
+ 'allowedIframeOrigins': validatedOrigins,
23316
24071
  'recordCanvas': this.getConfig('record_canvas'),
23317
24072
  'sampling': {
23318
24073
  'canvas': 15
23319
24074
  },
23320
- 'plugins': this.getConfig('record_console') ? [
23321
- getRecordConsolePlugin({
23322
- stringifyOptions: {
23323
- stringLengthLimit: 1000,
23324
- numOfKeysLimit: 50,
23325
- depthOfLimit: 2
23326
- }
23327
- })
23328
- ] : []
24075
+ 'plugins': plugins,
23329
24076
  });
23330
24077
  } catch (err) {
23331
24078
  this.reportError('Unexpected error when starting rrweb recording.', err);
@@ -23440,6 +24187,10 @@
23440
24187
  return recording;
23441
24188
  };
23442
24189
 
24190
+ SessionRecording.prototype._getApiRoute = function () {
24191
+ return this.getConfig('api_routes')['record'];
24192
+ };
24193
+
23443
24194
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
23444
24195
  var onSuccess = function (response, responseBody) {
23445
24196
  // Update batch specific props only if the request was successful to guarantee ordering.
@@ -23459,7 +24210,7 @@
23459
24210
  });
23460
24211
  }.bind(this);
23461
24212
  var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
23462
- win['fetch'](apiHost + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
24213
+ win['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
23463
24214
  'method': 'POST',
23464
24215
  'headers': {
23465
24216
  'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
@@ -23541,14 +24292,14 @@
23541
24292
 
23542
24293
 
23543
24294
  SessionRecording.prototype.reportError = function(msg, err) {
23544
- logger$2.error.apply(logger$2.error, arguments);
24295
+ logger$3.error.apply(logger$3.error, arguments);
23545
24296
  try {
23546
24297
  if (!err && !(msg instanceof Error)) {
23547
24298
  msg = new Error(msg);
23548
24299
  }
23549
24300
  this.getConfig('error_reporter')(msg, err);
23550
24301
  } catch(err) {
23551
- logger$2.error(err);
24302
+ logger$3.error(err);
23552
24303
  }
23553
24304
  };
23554
24305
 
@@ -23577,7 +24328,7 @@
23577
24328
  var configValue = this.getConfig('record_min_ms');
23578
24329
 
23579
24330
  if (configValue > MAX_VALUE_FOR_MIN_RECORDING_MS) {
23580
- logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
24331
+ logger$3.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
23581
24332
  return MAX_VALUE_FOR_MIN_RECORDING_MS;
23582
24333
  }
23583
24334
 
@@ -23740,7 +24491,7 @@
23740
24491
  .catch(this.handleError.bind(this));
23741
24492
  };
23742
24493
 
23743
- var logger$1 = console_with_prefix('recorder');
24494
+ var logger$2 = console_with_prefix('recorder');
23744
24495
 
23745
24496
  /**
23746
24497
  * Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
@@ -23756,7 +24507,7 @@
23756
24507
  */
23757
24508
  this.recordingRegistry = new RecordingRegistry({
23758
24509
  mixpanelInstance: this.mixpanelInstance,
23759
- errorReporter: logger$1.error,
24510
+ errorReporter: logger$2.error,
23760
24511
  sharedLockStorage: sharedLockStorage
23761
24512
  });
23762
24513
  this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
@@ -23768,17 +24519,17 @@
23768
24519
  MixpanelRecorder.prototype.startRecording = function(options) {
23769
24520
  options = options || {};
23770
24521
  if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
23771
- logger$1.log('Recording already in progress, skipping startRecording.');
24522
+ logger$2.log('Recording already in progress, skipping startRecording.');
23772
24523
  return;
23773
24524
  }
23774
24525
 
23775
24526
  var onIdleTimeout = function () {
23776
- logger$1.log('Idle timeout reached, restarting recording.');
24527
+ logger$2.log('Idle timeout reached, restarting recording.');
23777
24528
  this.resetRecording();
23778
24529
  }.bind(this);
23779
24530
 
23780
24531
  var onMaxLengthReached = function () {
23781
- logger$1.log('Max recording length reached, stopping recording.');
24532
+ logger$2.log('Max recording length reached, stopping recording.');
23782
24533
  this.resetRecording();
23783
24534
  }.bind(this);
23784
24535
 
@@ -23848,7 +24599,7 @@
23848
24599
  } else if (startNewIfInactive) {
23849
24600
  return this.startRecording({shouldStopBatcher: false});
23850
24601
  } else {
23851
- logger$1.log('No resumable recording found.');
24602
+ logger$2.log('No resumable recording found.');
23852
24603
  return null;
23853
24604
  }
23854
24605
  }.bind(this));
@@ -23860,8 +24611,12 @@
23860
24611
  this.startRecording({shouldStopBatcher: true});
23861
24612
  };
23862
24613
 
24614
+ MixpanelRecorder.prototype.isRecording = function () {
24615
+ return this.activeRecording && !this.activeRecording.isRrwebStopped();
24616
+ };
24617
+
23863
24618
  MixpanelRecorder.prototype.getActiveReplayId = function () {
23864
- if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
24619
+ if (this.isRecording()) {
23865
24620
  return this.activeRecording.replayId;
23866
24621
  } else {
23867
24622
  return null;
@@ -23978,7 +24733,7 @@
23978
24733
  observer.observe(shadowRoot, this.observerConfig);
23979
24734
  this.shadowObservers.push(observer);
23980
24735
  } catch (e) {
23981
- logger$3.critical('Error while observing shadow root', e);
24736
+ logger$5.critical('Error while observing shadow root', e);
23982
24737
  }
23983
24738
  };
23984
24739
 
@@ -23989,7 +24744,7 @@
23989
24744
  }
23990
24745
 
23991
24746
  if (!weakSetSupported()) {
23992
- logger$3.critical('Shadow DOM observation unavailable: WeakSet not supported');
24747
+ logger$5.critical('Shadow DOM observation unavailable: WeakSet not supported');
23993
24748
  return;
23994
24749
  }
23995
24750
 
@@ -24005,7 +24760,7 @@
24005
24760
  try {
24006
24761
  this.shadowObservers[i].disconnect();
24007
24762
  } catch (e) {
24008
- logger$3.critical('Error while disconnecting shadow DOM observer', e);
24763
+ logger$5.critical('Error while disconnecting shadow DOM observer', e);
24009
24764
  }
24010
24765
  }
24011
24766
  this.shadowObservers = [];
@@ -24193,7 +24948,7 @@
24193
24948
 
24194
24949
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
24195
24950
  } catch (e) {
24196
- logger$3.critical('Error while setting up mutation observer', e);
24951
+ logger$5.critical('Error while setting up mutation observer', e);
24197
24952
  }
24198
24953
  }
24199
24954
 
@@ -24208,7 +24963,7 @@
24208
24963
  );
24209
24964
  this.shadowDOMObserver.start();
24210
24965
  } catch (e) {
24211
- logger$3.critical('Error while setting up shadow DOM observer', e);
24966
+ logger$5.critical('Error while setting up shadow DOM observer', e);
24212
24967
  this.shadowDOMObserver = null;
24213
24968
  }
24214
24969
  }
@@ -24235,7 +24990,7 @@
24235
24990
  try {
24236
24991
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
24237
24992
  } catch (e) {
24238
- logger$3.critical('Error while removing event listener', e);
24993
+ logger$5.critical('Error while removing event listener', e);
24239
24994
  }
24240
24995
  }
24241
24996
  this.eventListeners = [];
@@ -24244,7 +24999,7 @@
24244
24999
  try {
24245
25000
  this.mutationObserver.disconnect();
24246
25001
  } catch (e) {
24247
- logger$3.critical('Error while disconnecting mutation observer', e);
25002
+ logger$5.critical('Error while disconnecting mutation observer', e);
24248
25003
  }
24249
25004
  this.mutationObserver = null;
24250
25005
  }
@@ -24253,7 +25008,7 @@
24253
25008
  try {
24254
25009
  this.shadowDOMObserver.stop();
24255
25010
  } catch (e) {
24256
- logger$3.critical('Error while stopping shadow DOM observer', e);
25011
+ logger$5.critical('Error while stopping shadow DOM observer', e);
24257
25012
  }
24258
25013
  this.shadowDOMObserver = null;
24259
25014
  }
@@ -24331,7 +25086,7 @@
24331
25086
 
24332
25087
  Autocapture.prototype.init = function() {
24333
25088
  if (!minDOMApisSupported()) {
24334
- logger$3.critical('Autocapture unavailable: missing required DOM APIs');
25089
+ logger$5.critical('Autocapture unavailable: missing required DOM APIs');
24335
25090
  return;
24336
25091
  }
24337
25092
  this.initPageListeners();
@@ -24363,27 +25118,15 @@
24363
25118
  };
24364
25119
 
24365
25120
  Autocapture.prototype.currentUrlBlocked = function() {
24366
- var i;
24367
25121
  var currentUrl = _.info.currentUrl();
24368
25122
 
24369
25123
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
24370
25124
  if (allowUrlRegexes.length) {
24371
25125
  // we're using an allowlist, only track if current URL matches
24372
- var allowed = false;
24373
- for (i = 0; i < allowUrlRegexes.length; i++) {
24374
- var allowRegex = allowUrlRegexes[i];
24375
- try {
24376
- if (currentUrl.match(allowRegex)) {
24377
- allowed = true;
24378
- break;
24379
- }
24380
- } catch (err) {
24381
- logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
24382
- return true;
24383
- }
24384
- }
24385
- if (!allowed) {
24386
- // wasn't allowed by any regex
25126
+ try {
25127
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
25128
+ } catch (err) {
25129
+ logger$5.critical('Error while checking block URL regexes: ', err);
24387
25130
  return true;
24388
25131
  }
24389
25132
  }
@@ -24393,17 +25136,12 @@
24393
25136
  return false;
24394
25137
  }
24395
25138
 
24396
- for (i = 0; i < blockUrlRegexes.length; i++) {
24397
- try {
24398
- if (currentUrl.match(blockUrlRegexes[i])) {
24399
- return true;
24400
- }
24401
- } catch (err) {
24402
- logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
24403
- return true;
24404
- }
25139
+ try {
25140
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
25141
+ } catch (err) {
25142
+ logger$5.critical('Error while checking block URL regexes: ', err);
25143
+ return true;
24405
25144
  }
24406
- return false;
24407
25145
  };
24408
25146
 
24409
25147
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -24539,7 +25277,7 @@
24539
25277
  return;
24540
25278
  }
24541
25279
 
24542
- logger$3.log('Initializing scroll depth tracking');
25280
+ logger$5.log('Initializing scroll depth tracking');
24543
25281
 
24544
25282
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
24545
25283
 
@@ -24565,7 +25303,7 @@
24565
25303
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
24566
25304
  return;
24567
25305
  }
24568
- logger$3.log('Initializing click tracking');
25306
+ logger$5.log('Initializing click tracking');
24569
25307
 
24570
25308
  this.listenerClick = function(ev) {
24571
25309
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -24584,7 +25322,7 @@
24584
25322
  return;
24585
25323
  }
24586
25324
 
24587
- logger$3.log('Initializing dead click tracking');
25325
+ logger$5.log('Initializing dead click tracking');
24588
25326
  if (!this._deadClickTracker) {
24589
25327
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
24590
25328
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -24618,7 +25356,7 @@
24618
25356
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
24619
25357
  return;
24620
25358
  }
24621
- logger$3.log('Initializing input tracking');
25359
+ logger$5.log('Initializing input tracking');
24622
25360
 
24623
25361
  this.listenerChange = function(ev) {
24624
25362
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -24635,7 +25373,7 @@
24635
25373
  if (!this.pageviewTrackingConfig()) {
24636
25374
  return;
24637
25375
  }
24638
- logger$3.log('Initializing pageview tracking');
25376
+ logger$5.log('Initializing pageview tracking');
24639
25377
 
24640
25378
  var previousTrackedUrl = '';
24641
25379
  var tracked = false;
@@ -24670,7 +25408,7 @@
24670
25408
  }
24671
25409
  if (didPathChange) {
24672
25410
  this.lastScrollCheckpoint = 0;
24673
- logger$3.log('Path change: re-initializing scroll depth checkpoints');
25411
+ logger$5.log('Path change: re-initializing scroll depth checkpoints');
24674
25412
  }
24675
25413
  }
24676
25414
  }.bind(this));
@@ -24685,7 +25423,7 @@
24685
25423
  return;
24686
25424
  }
24687
25425
 
24688
- logger$3.log('Initializing rage click tracking');
25426
+ logger$5.log('Initializing rage click tracking');
24689
25427
  if (!this._rageClickTracker) {
24690
25428
  this._rageClickTracker = new RageClickTracker();
24691
25429
  }
@@ -24715,7 +25453,7 @@
24715
25453
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
24716
25454
  return;
24717
25455
  }
24718
- logger$3.log('Initializing scroll tracking');
25456
+ logger$5.log('Initializing scroll tracking');
24719
25457
  this.lastScrollCheckpoint = 0;
24720
25458
 
24721
25459
  var scrollTrackFunction = function() {
@@ -24752,7 +25490,7 @@
24752
25490
  }
24753
25491
  }
24754
25492
  } catch (err) {
24755
- logger$3.critical('Error while calculating scroll percentage', err);
25493
+ logger$5.critical('Error while calculating scroll percentage', err);
24756
25494
  }
24757
25495
  if (shouldTrack) {
24758
25496
  this.mp.track(MP_EV_SCROLL, props);
@@ -24770,7 +25508,7 @@
24770
25508
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
24771
25509
  return;
24772
25510
  }
24773
- logger$3.log('Initializing submit tracking');
25511
+ logger$5.log('Initializing submit tracking');
24774
25512
 
24775
25513
  this.listenerSubmit = function(ev) {
24776
25514
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -24792,7 +25530,7 @@
24792
25530
  return;
24793
25531
  }
24794
25532
 
24795
- logger$3.log('Initializing page visibility tracking.');
25533
+ logger$5.log('Initializing page visibility tracking.');
24796
25534
  this._initScrollDepthTracking();
24797
25535
  var previousTrackedUrl = _.info.currentUrl();
24798
25536
 
@@ -24877,7 +25615,7 @@
24877
25615
  return win[TARGETING_GLOBAL_NAME];
24878
25616
  };
24879
25617
 
24880
- var logger = console_with_prefix('flags');
25618
+ var logger$1 = console_with_prefix('flags');
24881
25619
  var FLAGS_CONFIG_KEY = 'flags';
24882
25620
 
24883
25621
  var CONFIG_CONTEXT = 'context';
@@ -24920,7 +25658,7 @@
24920
25658
 
24921
25659
  FeatureFlagManager.prototype.init = function() {
24922
25660
  if (!this.minApisSupported()) {
24923
- logger.critical('Feature Flags unavailable: missing minimum required APIs');
25661
+ logger$1.critical('Feature Flags unavailable: missing minimum required APIs');
24924
25662
  return;
24925
25663
  }
24926
25664
 
@@ -24955,7 +25693,7 @@
24955
25693
 
24956
25694
  FeatureFlagManager.prototype.updateContext = function(newContext, options) {
24957
25695
  if (!this.isSystemEnabled()) {
24958
- logger.critical('Feature Flags not enabled, cannot update context');
25696
+ logger$1.critical('Feature Flags not enabled, cannot update context');
24959
25697
  return Promise.resolve();
24960
25698
  }
24961
25699
 
@@ -24972,7 +25710,7 @@
24972
25710
 
24973
25711
  FeatureFlagManager.prototype.areFlagsReady = function() {
24974
25712
  if (!this.isSystemEnabled()) {
24975
- logger.error('Feature Flags not enabled');
25713
+ logger$1.error('Feature Flags not enabled');
24976
25714
  }
24977
25715
  return !!this.flags;
24978
25716
  };
@@ -24985,7 +25723,7 @@
24985
25723
  var distinctId = this.getMpProperty('distinct_id');
24986
25724
  var deviceId = this.getMpProperty('$device_id');
24987
25725
  var traceparent = generateTraceparent();
24988
- logger.log('Fetching flags for distinct ID: ' + distinctId);
25726
+ logger$1.log('Fetching flags for distinct ID: ' + distinctId);
24989
25727
 
24990
25728
  var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
24991
25729
  var searchParams = new URLSearchParams();
@@ -25084,11 +25822,11 @@
25084
25822
  this._loadTargetingIfNeeded();
25085
25823
  }.bind(this)).catch(function(error) {
25086
25824
  this.markFetchComplete();
25087
- logger.error(error);
25825
+ logger$1.error(error);
25088
25826
  }.bind(this));
25089
25827
  }.bind(this)).catch(function(error) {
25090
25828
  this.markFetchComplete();
25091
- logger.error(error);
25829
+ logger$1.error(error);
25092
25830
  }.bind(this));
25093
25831
 
25094
25832
  return this.fetchPromise;
@@ -25096,7 +25834,7 @@
25096
25834
 
25097
25835
  FeatureFlagManager.prototype.markFetchComplete = function() {
25098
25836
  if (!this._fetchInProgressStartTime) {
25099
- logger.error('Fetch in progress started time not set, cannot mark fetch complete');
25837
+ logger$1.error('Fetch in progress started time not set, cannot mark fetch complete');
25100
25838
  return;
25101
25839
  }
25102
25840
  this._fetchStartTime = this._fetchInProgressStartTime;
@@ -25118,7 +25856,7 @@
25118
25856
 
25119
25857
  if (hasPropertyFilters) {
25120
25858
  this.getTargeting().then(function() {
25121
- logger.log('targeting loaded for property filter evaluation');
25859
+ logger$1.log('targeting loaded for property filter evaluation');
25122
25860
  });
25123
25861
  }
25124
25862
  };
@@ -25133,7 +25871,7 @@
25133
25871
  this.loadExtraBundle.bind(this),
25134
25872
  this.targetingSrc
25135
25873
  ).catch(function(error) {
25136
- logger.error('Failed to load targeting: ' + error);
25874
+ logger$1.error('Failed to load targeting: ' + error);
25137
25875
  }.bind(this));
25138
25876
  };
25139
25877
 
@@ -25187,7 +25925,7 @@
25187
25925
 
25188
25926
  // If no targeting library and event has property filters, skip it
25189
25927
  if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
25190
- logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
25928
+ logger$1.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
25191
25929
  return;
25192
25930
  }
25193
25931
 
@@ -25210,7 +25948,7 @@
25210
25948
  }
25211
25949
 
25212
25950
  if (matchResult.error) {
25213
- logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
25951
+ logger$1.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
25214
25952
  return;
25215
25953
  }
25216
25954
 
@@ -25218,7 +25956,7 @@
25218
25956
  return;
25219
25957
  }
25220
25958
 
25221
- logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
25959
+ logger$1.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
25222
25960
 
25223
25961
  var newVariant = {
25224
25962
  'key': pendingEvent['pending_variant']['variant_key'],
@@ -25259,7 +25997,7 @@
25259
25997
  'first_time_event_hash': firstTimeEventHash
25260
25998
  };
25261
25999
 
25262
- logger.log('Recording first-time event for flag: ' + flagId);
26000
+ logger$1.log('Recording first-time event for flag: ' + flagId);
25263
26001
 
25264
26002
  // Fire-and-forget POST request
25265
26003
  this.fetch.call(win, url, {
@@ -25272,130 +26010,446 @@
25272
26010
  'body': JSON.stringify(payload)
25273
26011
  }).catch(function(error) {
25274
26012
  // Silent failure - cohort sync will catch up
25275
- logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26013
+ logger$1.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26014
+ });
26015
+ };
26016
+
26017
+ FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
26018
+ if (!this.fetchPromise) {
26019
+ return new Promise(function(resolve) {
26020
+ logger$1.critical('Feature Flags not initialized');
26021
+ resolve(fallback);
26022
+ });
26023
+ }
26024
+
26025
+ return this.fetchPromise.then(function() {
26026
+ return this.getVariantSync(featureName, fallback);
26027
+ }.bind(this)).catch(function(error) {
26028
+ logger$1.error(error);
26029
+ return fallback;
26030
+ });
26031
+ };
26032
+
26033
+ FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
26034
+ if (!this.areFlagsReady()) {
26035
+ logger$1.log('Flags not loaded yet');
26036
+ return fallback;
26037
+ }
26038
+ var feature = this.flags.get(featureName);
26039
+ if (!feature) {
26040
+ logger$1.log('No flag found: "' + featureName + '"');
26041
+ return fallback;
26042
+ }
26043
+ this.trackFeatureCheck(featureName, feature);
26044
+ return feature;
26045
+ };
26046
+
26047
+ FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
26048
+ return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
26049
+ return feature['value'];
26050
+ }).catch(function(error) {
26051
+ logger$1.error(error);
26052
+ return fallbackValue;
26053
+ });
26054
+ };
26055
+
26056
+ // TODO remove deprecated method
26057
+ FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
26058
+ 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.');
26059
+ return this.getVariantValue(featureName, fallbackValue);
26060
+ };
26061
+
26062
+ FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
26063
+ return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
26064
+ };
26065
+
26066
+ FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
26067
+ return this.getVariantValue(featureName).then(function() {
26068
+ return this.isEnabledSync(featureName, fallbackValue);
26069
+ }.bind(this)).catch(function(error) {
26070
+ logger$1.error(error);
26071
+ return fallbackValue;
25276
26072
  });
25277
26073
  };
25278
26074
 
25279
- FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
25280
- if (!this.fetchPromise) {
25281
- return new Promise(function(resolve) {
25282
- logger.critical('Feature Flags not initialized');
25283
- resolve(fallback);
25284
- });
26075
+ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
26076
+ fallbackValue = fallbackValue || false;
26077
+ var val = this.getVariantValueSync(featureName, fallbackValue);
26078
+ if (val !== true && val !== false) {
26079
+ logger$1.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
26080
+ val = fallbackValue;
26081
+ }
26082
+ return val;
26083
+ };
26084
+
26085
+ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
26086
+ if (this.trackedFeatures.has(featureName)) {
26087
+ return;
26088
+ }
26089
+ this.trackedFeatures.add(featureName);
26090
+
26091
+ var trackingProperties = {
26092
+ 'Experiment name': featureName,
26093
+ 'Variant name': feature['key'],
26094
+ '$experiment_type': 'feature_flag',
26095
+ 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
26096
+ 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
26097
+ 'Variant fetch latency (ms)': this._fetchLatency,
26098
+ 'Variant fetch traceparent': this._traceparent,
26099
+ };
26100
+
26101
+ if (feature['experiment_id'] !== 'undefined') {
26102
+ trackingProperties['$experiment_id'] = feature['experiment_id'];
26103
+ }
26104
+ if (feature['is_experiment_active'] !== 'undefined') {
26105
+ trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
26106
+ }
26107
+ if (feature['is_qa_tester'] !== 'undefined') {
26108
+ trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
26109
+ }
26110
+
26111
+ this.track('$experiment_started', trackingProperties);
26112
+ };
26113
+
26114
+ FeatureFlagManager.prototype.minApisSupported = function() {
26115
+ return !!this.fetch &&
26116
+ typeof Promise !== 'undefined' &&
26117
+ typeof Map !== 'undefined' &&
26118
+ typeof Set !== 'undefined';
26119
+ };
26120
+
26121
+ safewrapClass(FeatureFlagManager);
26122
+
26123
+ FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
26124
+ FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
26125
+ FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
26126
+ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
26127
+ FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
26128
+ FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
26129
+ FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
26130
+ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
26131
+
26132
+ // Deprecated method
26133
+ FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
26134
+
26135
+ // Exports intended only for testing
26136
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26137
+
26138
+ /* eslint camelcase: "off" */
26139
+
26140
+
26141
+ var logger = console_with_prefix('recorder');
26142
+
26143
+ var IFRAME_HANDSHAKE_REQUEST = 'mp_iframe_handshake_request';
26144
+ var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
26145
+
26146
+
26147
+ /**
26148
+ * RecorderManager: manages session recording initialization, lifecycle and state
26149
+ * @constructor
26150
+ */
26151
+ var RecorderManager = function(initOptions) {
26152
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
26153
+ // but ideally we should be able to remove this dependency.
26154
+ this.mixpanelInstance = initOptions.mixpanelInstance;
26155
+
26156
+ this.getMpConfig = initOptions.getConfigFunc;
26157
+ this.getTabId = initOptions.getTabIdFunc;
26158
+ this.reportError = initOptions.reportErrorFunc;
26159
+ this.getDistinctId = initOptions.getDistinctIdFunc;
26160
+ this.loadExtraBundle = initOptions.loadExtraBundle;
26161
+ this.recorderSrc = initOptions.recorderSrc;
26162
+ this.targetingSrc = initOptions.targetingSrc;
26163
+ this.libBasePath = initOptions.libBasePath;
26164
+
26165
+ this._recorder = null;
26166
+ this._parentReplayId = null;
26167
+ this._parentFrameRetryInterval = null;
26168
+ };
26169
+
26170
+ RecorderManager.prototype.shouldLoadRecorder = function() {
26171
+ if (this.getMpConfig('disable_persistence')) {
26172
+ console$1.log('Load recorder check skipped due to disable_persistence config');
26173
+ return PromisePolyfill.resolve(false);
26174
+ }
26175
+
26176
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26177
+ var tab_id = this.getTabId();
26178
+ return recording_registry_idb.init()
26179
+ .then(function () {
26180
+ return recording_registry_idb.getAll();
26181
+ })
26182
+ .then(function (recordings) {
26183
+ for (var i = 0; i < recordings.length; i++) {
26184
+ // if there are expired recordings in the registry, we should load the recorder to flush them
26185
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
26186
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26187
+ return true;
26188
+ }
26189
+ }
26190
+ return false;
26191
+ })
26192
+ .catch(_.bind(function (err) {
26193
+ this.reportError('Error checking recording registry', err);
26194
+ return false;
26195
+ }, this));
26196
+ };
26197
+
26198
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
26199
+ if (!win['MutationObserver']) {
26200
+ console$1.critical('Browser does not support MutationObserver; skipping session recording');
26201
+ return PromisePolyfill.resolve();
26202
+ }
26203
+
26204
+ var loadRecorder = _.bind(function(startNewIfInactive) {
26205
+ return new PromisePolyfill(_.bind(function(resolve) {
26206
+ var handleLoadedRecorder = safewrap(_.bind(function() {
26207
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
26208
+ this._recorder['resumeRecording'](startNewIfInactive);
26209
+ resolve();
26210
+ }, this));
26211
+
26212
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
26213
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
26214
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
26215
+ } else {
26216
+ handleLoadedRecorder();
26217
+ }
26218
+ }, this));
26219
+ }, this);
26220
+
26221
+ // Cross-origin iframe handling
26222
+ var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger);
26223
+ var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
26224
+
26225
+ if (isCrossOriginRecordingEnabled) {
26226
+ // listen for handshake requests from their own child iframes (including nested)
26227
+ this._setupParentFrameListener(allowedOrigins);
26228
+
26229
+ if (win.parent !== win) {
26230
+ // also wait for parent's replay ID
26231
+ this._setupChildFrameListener(allowedOrigins, loadRecorder);
26232
+ this._sendParentFrameRequestWithRetry(allowedOrigins);
26233
+ return PromisePolyfill.resolve();
26234
+ }
26235
+ }
26236
+
26237
+ /**
26238
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26239
+ * 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.
26240
+ */
26241
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
26242
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
26243
+ if (force_start || is_sampled) {
26244
+ return loadRecorder(true);
26245
+ } else {
26246
+ return this.shouldLoadRecorder()
26247
+ .then(_.bind(function (shouldLoad) {
26248
+ if (shouldLoad) {
26249
+ return loadRecorder(false);
26250
+ }
26251
+ return PromisePolyfill.resolve();
26252
+ }, this));
26253
+ }
26254
+ };
26255
+
26256
+ RecorderManager.prototype.isRecording = function() {
26257
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
26258
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
26259
+ return false;
26260
+ }
26261
+ try {
26262
+ return this._recorder['isRecording']();
26263
+ } catch (e) {
26264
+ this.reportError('Error checking if recording is active', e);
26265
+ return false;
25285
26266
  }
26267
+ };
25286
26268
 
25287
- return this.fetchPromise.then(function() {
25288
- return this.getVariantSync(featureName, fallback);
25289
- }.bind(this)).catch(function(error) {
25290
- logger.error(error);
25291
- return fallback;
25292
- });
26269
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
26270
+ var isRecording = this.isRecording();
26271
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
26272
+
26273
+ if (!isRecording && recordingTriggerEvents) {
26274
+ var trigger = recordingTriggerEvents[event_name];
26275
+ if (trigger && typeof trigger['percentage'] === 'number') {
26276
+ var newRate = trigger['percentage'];
26277
+ var propertyFilters = trigger['property_filters'];
26278
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
26279
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
26280
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
26281
+ .then(function(targeting) {
26282
+ try {
26283
+ var result = targeting['eventMatchesCriteria'](
26284
+ event_name,
26285
+ properties,
26286
+ {
26287
+ 'event_name': event_name,
26288
+ 'property_filters': propertyFilters
26289
+ }
26290
+ );
26291
+ if (result['matches']) {
26292
+ this.checkAndStartSessionRecording(false, newRate);
26293
+ }
26294
+ } catch (err) {
26295
+ console$1.critical('Could not parse recording event trigger properties logic:', err);
26296
+ }
26297
+ }.bind(this)).catch(function(err) {
26298
+ console$1.critical('Failed to load targeting library:', err);
26299
+ });
26300
+ } else {
26301
+ this.checkAndStartSessionRecording(false, newRate);
26302
+ }
26303
+ }
26304
+ }
25293
26305
  };
25294
26306
 
25295
- FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
25296
- if (!this.areFlagsReady()) {
25297
- logger.log('Flags not loaded yet');
25298
- return fallback;
26307
+ RecorderManager.prototype.stopSessionRecording = function() {
26308
+ if (this._recorder) {
26309
+ return this._recorder['stopRecording']();
25299
26310
  }
25300
- var feature = this.flags.get(featureName);
25301
- if (!feature) {
25302
- logger.log('No flag found: "' + featureName + '"');
25303
- return fallback;
26311
+ return PromisePolyfill.resolve();
26312
+ };
26313
+
26314
+ RecorderManager.prototype.pauseSessionRecording = function() {
26315
+ if (this._recorder) {
26316
+ return this._recorder['pauseRecording']();
25304
26317
  }
25305
- this.trackFeatureCheck(featureName, feature);
25306
- return feature;
26318
+ return PromisePolyfill.resolve();
25307
26319
  };
25308
26320
 
25309
- FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
25310
- return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
25311
- return feature['value'];
25312
- }).catch(function(error) {
25313
- logger.error(error);
25314
- return fallbackValue;
25315
- });
26321
+ RecorderManager.prototype.resumeSessionRecording = function() {
26322
+ if (this._recorder) {
26323
+ return this._recorder['resumeRecording']();
26324
+ }
26325
+ return PromisePolyfill.resolve();
25316
26326
  };
25317
26327
 
25318
- // TODO remove deprecated method
25319
- FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
25320
- logger.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
25321
- return this.getVariantValue(featureName, fallbackValue);
26328
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
26329
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
25322
26330
  };
25323
26331
 
25324
- FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
25325
- return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
26332
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
26333
+ var props = {};
26334
+ var replay_id = this.getSessionReplayId();
26335
+ if (replay_id) {
26336
+ props['$mp_replay_id'] = replay_id;
26337
+ }
26338
+ return props;
25326
26339
  };
25327
26340
 
25328
- FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
25329
- return this.getVariantValue(featureName).then(function() {
25330
- return this.isEnabledSync(featureName, fallbackValue);
25331
- }.bind(this)).catch(function(error) {
25332
- logger.error(error);
25333
- return fallbackValue;
25334
- });
26341
+ RecorderManager.prototype.getSessionReplayUrl = function() {
26342
+ var replay_url = null;
26343
+ var replay_id = this.getSessionReplayId();
26344
+ if (replay_id) {
26345
+ var query_params = _.HTTPBuildQuery({
26346
+ 'replay_id': replay_id,
26347
+ 'distinct_id': this.getDistinctId(),
26348
+ 'token': this.getMpConfig('token')
26349
+ });
26350
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26351
+ }
26352
+ return replay_url;
25335
26353
  };
25336
26354
 
25337
- FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
25338
- fallbackValue = fallbackValue || false;
25339
- var val = this.getVariantValueSync(featureName, fallbackValue);
25340
- if (val !== true && val !== false) {
25341
- logger.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
25342
- val = fallbackValue;
26355
+ RecorderManager.prototype.getSessionReplayId = function() {
26356
+ // Child iframe uses parent's replay ID
26357
+ if (this._parentReplayId) {
26358
+ return this._parentReplayId;
25343
26359
  }
25344
- return val;
26360
+ var replay_id = null;
26361
+ if (this._recorder) {
26362
+ replay_id = this._recorder['replayId'];
26363
+ }
26364
+ return replay_id || null;
25345
26365
  };
25346
26366
 
25347
- FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
25348
- if (this.trackedFeatures.has(featureName)) {
26367
+ // "private" public method to reach into the recorder in test cases
26368
+ RecorderManager.prototype.getRecorder = function() {
26369
+ return this._recorder;
26370
+ };
26371
+
26372
+ RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
26373
+ if (this._childFrameMessageHandler) {
25349
26374
  return;
25350
26375
  }
25351
- this.trackedFeatures.add(featureName);
25352
-
25353
- var trackingProperties = {
25354
- 'Experiment name': featureName,
25355
- 'Variant name': feature['key'],
25356
- '$experiment_type': 'feature_flag',
25357
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
25358
- 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
25359
- 'Variant fetch latency (ms)': this._fetchLatency,
25360
- 'Variant fetch traceparent': this._traceparent,
26376
+ var self = this;
26377
+ this._childFrameMessageHandler = function(event) {
26378
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
26379
+ var data = event.data;
26380
+ if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
26381
+ self._parentReplayId = data['replayId'];
26382
+ if (data['distinctId']) {
26383
+ self.mixpanelInstance['identify'](data['distinctId']);
26384
+ }
26385
+ self._parentFrameRetryActive = false;
26386
+ win.removeEventListener('message', self._childFrameMessageHandler);
26387
+ self._childFrameMessageHandler = null;
26388
+ loadRecorder(true);
26389
+ }
25361
26390
  };
26391
+ win.addEventListener('message', this._childFrameMessageHandler);
26392
+ };
25362
26393
 
25363
- if (feature['experiment_id'] !== 'undefined') {
25364
- trackingProperties['$experiment_id'] = feature['experiment_id'];
25365
- }
25366
- if (feature['is_experiment_active'] !== 'undefined') {
25367
- trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
25368
- }
25369
- if (feature['is_qa_tester'] !== 'undefined') {
25370
- trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
26394
+ RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
26395
+ var message = {};
26396
+ message['type'] = IFRAME_HANDSHAKE_REQUEST;
26397
+ message['token'] = this.getMpConfig('token');
26398
+ for (var i = 0; i < allowedOrigins.length; i++) {
26399
+ try {
26400
+ win.parent.postMessage(message, allowedOrigins[i]);
26401
+ } catch (e) {
26402
+ // origin mismatch - ignore
26403
+ }
25371
26404
  }
25372
-
25373
- this.track('$experiment_started', trackingProperties);
25374
26405
  };
25375
26406
 
25376
- FeatureFlagManager.prototype.minApisSupported = function() {
25377
- return !!this.fetch &&
25378
- typeof Promise !== 'undefined' &&
25379
- typeof Map !== 'undefined' &&
25380
- typeof Set !== 'undefined';
25381
- };
26407
+ RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
26408
+ var self = this;
26409
+ var maxRetries = 10;
26410
+ var retryCount = 0;
26411
+ var delay = 50;
26412
+ this._parentFrameRetryActive = true;
25382
26413
 
25383
- safewrapClass(FeatureFlagManager);
26414
+ this._sendParentFrameRequest(allowedOrigins);
25384
26415
 
25385
- FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
25386
- FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
25387
- FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
25388
- FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
25389
- FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
25390
- FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
25391
- FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
25392
- FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
26416
+ function scheduleRetry() {
26417
+ setTimeout(function() {
26418
+ if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
26419
+ return;
26420
+ }
26421
+ self._sendParentFrameRequest(allowedOrigins);
26422
+ delay *= 2;
26423
+ scheduleRetry();
26424
+ }, delay);
26425
+ }
26426
+ scheduleRetry();
26427
+ };
25393
26428
 
25394
- // Deprecated method
25395
- FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
26429
+ RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
26430
+ if (this._parentFrameMessageHandler) {
26431
+ return;
26432
+ }
26433
+ var self = this;
26434
+ this._parentFrameMessageHandler = function(event) {
26435
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
26436
+ var data = event.data;
26437
+ if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
26438
+ var replayId = self.getSessionReplayId();
26439
+ if (replayId) {
26440
+ var response = {};
26441
+ response['type'] = IFRAME_HANDSHAKE_RESPONSE;
26442
+ response['token'] = self.getMpConfig('token');
26443
+ response['replayId'] = replayId;
26444
+ response['distinctId'] = self.getDistinctId();
26445
+ event.source.postMessage(response, event.origin);
26446
+ }
26447
+ }
26448
+ };
26449
+ win.addEventListener('message', this._parentFrameMessageHandler);
26450
+ };
25396
26451
 
25397
- // Exports intended only for testing
25398
- FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26452
+ safewrapClass(RecorderManager);
25399
26453
 
25400
26454
  /* eslint camelcase: "off" */
25401
26455
 
@@ -26769,7 +27823,6 @@
26769
27823
  /** @const */ var SETTING_FALLBACK = 'fallback';
26770
27824
  /** @const */ var SETTING_DISABLED = 'disabled';
26771
27825
 
26772
-
26773
27826
  /*
26774
27827
  * Dynamic... constants? Is that an oxymoron?
26775
27828
  */
@@ -26854,19 +27907,24 @@
26854
27907
  'batch_request_timeout_ms': 90000,
26855
27908
  'batch_autostart': true,
26856
27909
  'hooks': {},
27910
+ 'record_allowed_iframe_origins': [],
26857
27911
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
26858
27912
  'record_block_selector': 'img, video, audio',
26859
27913
  'record_canvas': false,
26860
27914
  'record_collect_fonts': false,
26861
27915
  'record_console': true,
26862
27916
  'record_heatmap_data': false,
27917
+ 'recording_event_triggers': {},
26863
27918
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
26864
27919
  'record_mask_inputs': true,
26865
27920
  'record_max_ms': MAX_RECORDING_MS,
26866
27921
  'record_min_ms': 0,
27922
+ 'record_network': false,
27923
+ 'record_network_options': {},
26867
27924
  'record_sessions_percent': 0,
26868
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
26869
- 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
27925
+ 'recorder_src': null,
27926
+ 'targeting_src': null,
27927
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
26870
27928
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
26871
27929
  };
26872
27930
 
@@ -27020,6 +28078,19 @@
27020
28078
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
27021
28079
  }));
27022
28080
 
28081
+ this.recorderManager = new RecorderManager({
28082
+ mixpanelInstance: this,
28083
+ getConfigFunc: _.bind(this.get_config, this),
28084
+ setConfigFunc: _.bind(this.set_config, this),
28085
+ getTabIdFunc: _.bind(this.get_tab_id, this),
28086
+ reportErrorFunc: _.bind(this.report_error, this),
28087
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
28088
+ recorderSrc: this.get_config('recorder_src'),
28089
+ targetingSrc: this.get_config('targeting_src'),
28090
+ libBasePath: this.get_config('lib_base_path'),
28091
+ loadExtraBundle: load_extra_bundle
28092
+ });
28093
+
27023
28094
  this['_jsc'] = NOOP_FUNC;
27024
28095
 
27025
28096
  this.__dom_loaded_queue = [];
@@ -27098,7 +28169,7 @@
27098
28169
  getPropertyFunc: _.bind(this.get_property, this),
27099
28170
  trackingFunc: _.bind(this.track, this),
27100
28171
  loadExtraBundle: load_extra_bundle,
27101
- targetingSrc: this.get_config('targeting_src')
28172
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
27102
28173
  });
27103
28174
  this.flags.init();
27104
28175
  this['flags'] = this.flags;
@@ -27111,11 +28182,11 @@
27111
28182
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
27112
28183
  var mode = this.get_config('remote_settings_mode');
27113
28184
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
27114
- this._fetch_remote_settings(mode).then(_.bind(function() {
27115
- this._check_and_start_session_recording();
28185
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
28186
+ return this._check_and_start_session_recording();
27116
28187
  }, this));
27117
28188
  } else {
27118
- this._check_and_start_session_recording();
28189
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
27119
28190
  }
27120
28191
  };
27121
28192
 
@@ -27159,132 +28230,50 @@
27159
28230
  return this.tab_id || null;
27160
28231
  };
27161
28232
 
27162
- MixpanelLib.prototype._should_load_recorder = function () {
27163
- if (this.get_config('disable_persistence')) {
27164
- console$1.log('Load recorder check skipped due to disable_persistence config');
27165
- return Promise.resolve(false);
27166
- }
27167
-
27168
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
27169
- var tab_id = this.get_tab_id();
27170
- return recording_registry_idb.init()
27171
- .then(function () {
27172
- return recording_registry_idb.getAll();
27173
- })
27174
- .then(function (recordings) {
27175
- for (var i = 0; i < recordings.length; i++) {
27176
- // if there are expired recordings in the registry, we should load the recorder to flush them
27177
- // if there's a recording for this tab id, we should load the recorder to continue the recording
27178
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
27179
- return true;
27180
- }
27181
- }
27182
- return false;
27183
- })
27184
- .catch(_.bind(function (err) {
27185
- this.report_error('Error checking recording registry', err);
27186
- }, this));
27187
- };
27188
-
27189
28233
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
27190
- if (!win['MutationObserver']) {
27191
- console$1.critical('Browser does not support MutationObserver; skipping session recording');
27192
- return;
27193
- }
27194
-
27195
- var loadRecorder = _.bind(function(startNewIfInactive) {
27196
- var handleLoadedRecorder = _.bind(function() {
27197
- this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
27198
- this._recorder['resumeRecording'](startNewIfInactive);
27199
- }, this);
27200
-
27201
- if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
27202
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
27203
- } else {
27204
- handleLoadedRecorder();
27205
- }
27206
- }, this);
27207
-
27208
- /**
27209
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
27210
- * 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.
27211
- */
27212
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
27213
- if (force_start || is_sampled) {
27214
- loadRecorder(true);
27215
- } else {
27216
- this._should_load_recorder()
27217
- .then(function (shouldLoad) {
27218
- if (shouldLoad) {
27219
- loadRecorder(false);
27220
- }
27221
- });
27222
- }
28234
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
27223
28235
  });
27224
28236
 
28237
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
28238
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
28239
+ };
28240
+
27225
28241
  MixpanelLib.prototype.start_session_recording = function () {
27226
- this._check_and_start_session_recording(true);
28242
+ return this._check_and_start_session_recording(true);
27227
28243
  };
27228
28244
 
27229
28245
  MixpanelLib.prototype.stop_session_recording = function () {
27230
- if (this._recorder) {
27231
- return this._recorder['stopRecording']();
27232
- }
27233
- return Promise.resolve();
28246
+ return this.recorderManager.stopSessionRecording();
27234
28247
  };
27235
28248
 
27236
28249
  MixpanelLib.prototype.pause_session_recording = function () {
27237
- if (this._recorder) {
27238
- return this._recorder['pauseRecording']();
27239
- }
27240
- return Promise.resolve();
28250
+ return this.recorderManager.pauseSessionRecording();
27241
28251
  };
27242
28252
 
27243
28253
  MixpanelLib.prototype.resume_session_recording = function () {
27244
- if (this._recorder) {
27245
- return this._recorder['resumeRecording']();
27246
- }
27247
- return Promise.resolve();
28254
+ return this.recorderManager.resumeSessionRecording();
27248
28255
  };
27249
28256
 
27250
28257
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
27251
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
28258
+ return this.recorderManager.isRecordingHeatmapData();
27252
28259
  };
27253
28260
 
27254
28261
  MixpanelLib.prototype.get_session_recording_properties = function () {
27255
- var props = {};
27256
- var replay_id = this._get_session_replay_id();
27257
- if (replay_id) {
27258
- props['$mp_replay_id'] = replay_id;
27259
- }
27260
- return props;
28262
+ return this.recorderManager.getSessionRecordingProperties();
27261
28263
  };
27262
28264
 
27263
28265
  MixpanelLib.prototype.get_session_replay_url = function () {
27264
- var replay_url = null;
27265
- var replay_id = this._get_session_replay_id();
27266
- if (replay_id) {
27267
- var query_params = _.HTTPBuildQuery({
27268
- 'replay_id': replay_id,
27269
- 'distinct_id': this.get_distinct_id(),
27270
- 'token': this.get_config('token')
27271
- });
27272
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
27273
- }
27274
- return replay_url;
27275
- };
27276
-
27277
- MixpanelLib.prototype._get_session_replay_id = function () {
27278
- var replay_id = null;
27279
- if (this._recorder) {
27280
- replay_id = this._recorder['replayId'];
27281
- }
27282
- return replay_id || null;
28266
+ return this.recorderManager.getSessionReplayUrl();
27283
28267
  };
27284
28268
 
27285
28269
  // "private" public method to reach into the recorder in test cases
27286
28270
  MixpanelLib.prototype.__get_recorder = function () {
27287
- return this._recorder;
28271
+ return this.recorderManager.getRecorder();
28272
+ };
28273
+
28274
+ // "private" public method to get session recording init promise in test cases
28275
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
28276
+ return this.__session_recording_init_promise;
27288
28277
  };
27289
28278
 
27290
28279
  // Private methods
@@ -27542,6 +28531,7 @@
27542
28531
  };
27543
28532
 
27544
28533
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
28534
+ var self = this;
27545
28535
  var disableRecordingIfStrict = function() {
27546
28536
  if (mode === 'strict') {
27547
28537
  self.set_config({'record_sessions_percent': 0});
@@ -27562,7 +28552,6 @@
27562
28552
  };
27563
28553
  var query_string = _.HTTPBuildQuery(request_params);
27564
28554
  var full_url = settings_endpoint + '?' + query_string;
27565
- var self = this;
27566
28555
 
27567
28556
  var abortController = new AbortController();
27568
28557
  var timeout_id = setTimeout(function() {
@@ -27754,6 +28743,34 @@
27754
28743
  this._execute_array([item]);
27755
28744
  };
27756
28745
 
28746
+ /**
28747
+ * Enables events on the Mixpanel object. If passed no arguments,
28748
+ * this function enable tracking of all events. If passed an
28749
+ * array of event names, those events will be enabled, but other
28750
+ * existing disabled events will continue to be not tracked.
28751
+ *
28752
+ * @param {Array} [events] An array of event names to enable
28753
+ */
28754
+ MixpanelLib.prototype.enable = function(events) {
28755
+ var keys, new_disabled_events, i, j;
28756
+
28757
+ if (typeof(events) === 'undefined') {
28758
+ this._flags.disable_all_events = false;
28759
+ } else {
28760
+ keys = {};
28761
+ new_disabled_events = [];
28762
+ for (i = 0; i < events.length; i++) {
28763
+ keys[events[i]] = true;
28764
+ }
28765
+ for (j = 0; j < this.__disabled_events.length; j++) {
28766
+ if (!keys[this.__disabled_events[j]]) {
28767
+ new_disabled_events.push(this.__disabled_events[j]);
28768
+ }
28769
+ }
28770
+ this.__disabled_events = new_disabled_events;
28771
+ }
28772
+ };
28773
+
27757
28774
  /**
27758
28775
  * Disable events on the Mixpanel object. If passed no arguments,
27759
28776
  * this function disables tracking of any event. If passed an
@@ -27927,6 +28944,8 @@
27927
28944
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
27928
28945
  }
27929
28946
 
28947
+ this._start_recording_on_event(event_name, properties);
28948
+
27930
28949
  var data = {
27931
28950
  'event': event_name,
27932
28951
  'properties': properties
@@ -29135,6 +30154,7 @@
29135
30154
  // MixpanelLib Exports
29136
30155
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
29137
30156
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
30157
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
29138
30158
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
29139
30159
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
29140
30160
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -29178,6 +30198,7 @@
29178
30198
 
29179
30199
  // Exports intended only for testing
29180
30200
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
30201
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
29181
30202
 
29182
30203
  // MixpanelPersistence Exports
29183
30204
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;