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
@@ -29,16 +29,19 @@
29
29
  win = window;
30
30
  }
31
31
 
32
- /**
33
- * Shared global window property names used across modules
34
- */
32
+ var Config = {
33
+ DEBUG: false,
34
+ LIB_VERSION: '2.77.0'
35
+ };
35
36
 
36
- // Targeting library global (used by flags and targeting modules)
37
+ // Window global names for async modules
37
38
  var TARGETING_GLOBAL_NAME = '__mp_targeting';
38
-
39
- // Recorder library global (used by recorder and mixpanel-core)
40
39
  var RECORDER_GLOBAL_NAME = '__mp_recorder';
41
40
 
41
+ // Constants that are injected at build-time for the names of async modules.
42
+ var RECORDER_FILENAME = '__MP_RECORDER_FILENAME__';
43
+ var TARGETING_FILENAME = '__MP_TARGETING_FILENAME__';
44
+
42
45
  function _array_like_to_array(arr, len) {
43
46
  if (len == null || len > arr.length) len = arr.length;
44
47
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -10736,13 +10739,7 @@
10736
10739
  };
10737
10740
  while(_this.mapRemoves.length){
10738
10741
  var removedNode = _this.mapRemoves.shift();
10739
- if (removedNode.nodeName === "IFRAME") {
10740
- try {
10741
- _this.iframeManager.removeIframe(removedNode);
10742
- } catch (e2) {}
10743
- } else {
10744
- _this.stylesheetManager.cleanupStylesheetsForRemovedNode(removedNode);
10745
- }
10742
+ _this.cleanupRemovedNode(removedNode);
10746
10743
  _this.mirror.removeNodeFromMap(removedNode);
10747
10744
  }
10748
10745
  for(var _iterator = _create_for_of_iterator_helper_loose(_this.movedSet), _step; !(_step = _iterator()).done;){
@@ -11062,6 +11059,20 @@
11062
11059
  }
11063
11060
  }
11064
11061
  });
11062
+ __publicField$1(this, "cleanupRemovedNode", function(node2) {
11063
+ if (node2.nodeName === "IFRAME") {
11064
+ try {
11065
+ _this.iframeManager.removeIframe(node2);
11066
+ } catch (e2) {}
11067
+ } else {
11068
+ try {
11069
+ _this.stylesheetManager.cleanupStylesheetsForRemovedNode(node2);
11070
+ } catch (e2) {}
11071
+ }
11072
+ node2.childNodes.forEach(function(child) {
11073
+ _this.cleanupRemovedNode(child);
11074
+ });
11075
+ });
11065
11076
  }
11066
11077
  var _proto = MutationBuffer.prototype;
11067
11078
  _proto.init = function init(options) {
@@ -13289,6 +13300,31 @@
13289
13300
  _proto.destroy = function destroy() {};
13290
13301
  return ProcessedNodeManager;
13291
13302
  }();
13303
+ function toOrigin(url) {
13304
+ try {
13305
+ var origin = new URL(url).origin;
13306
+ return origin !== "null" ? origin : null;
13307
+ } catch (e) {
13308
+ return null;
13309
+ }
13310
+ }
13311
+ function buildAllowedOriginSet(origins) {
13312
+ if (!Array.isArray(origins) || origins.length === 0) {
13313
+ throw new Error("[rrweb] allowedIframeOrigins must be a non-empty array of origin strings.");
13314
+ }
13315
+ var set = /* @__PURE__ */ new Set();
13316
+ for(var i2 = 0; i2 < origins.length; i2++){
13317
+ var entry = origins[i2];
13318
+ if (typeof entry !== "string") {
13319
+ throw new Error("[rrweb] allowedIframeOrigins[" + i2 + "] must be a string, got " + (typeof entry === "undefined" ? "undefined" : _type_of(entry)) + ".");
13320
+ }
13321
+ var origin = toOrigin(entry);
13322
+ if (origin) {
13323
+ set.add(origin);
13324
+ }
13325
+ }
13326
+ return Object.freeze(set);
13327
+ }
13292
13328
  var wrappedEmit;
13293
13329
  var takeFullSnapshot$1;
13294
13330
  var canvasManager;
@@ -13310,10 +13346,17 @@
13310
13346
  var mirror = createMirror$2();
13311
13347
  function record(options) {
13312
13348
  if (options === void 0) options = {};
13313
- 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() {
13349
+ 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() {
13314
13350
  return false;
13315
13351
  } : _options_keepIframeSrcFn, _options_ignoreCSSAttributes = options.ignoreCSSAttributes, ignoreCSSAttributes = _options_ignoreCSSAttributes === void 0 ? /* @__PURE__ */ new Set([]) : _options_ignoreCSSAttributes, errorHandler2 = options.errorHandler;
13316
13352
  registerErrorHandler(errorHandler2);
13353
+ var validatedOrigins;
13354
+ if (recordCrossOriginIframes && allowedIframeOrigins && allowedIframeOrigins.length > 0) {
13355
+ validatedOrigins = buildAllowedOriginSet(allowedIframeOrigins);
13356
+ if (validatedOrigins.size === 0) {
13357
+ validatedOrigins = void 0;
13358
+ }
13359
+ }
13317
13360
  var inEmittingFrame = recordCrossOriginIframes ? window.parent === window : true;
13318
13361
  var passEmitsToParent = false;
13319
13362
  if (!inEmittingFrame) {
@@ -13405,7 +13448,14 @@
13405
13448
  origin: window.location.origin,
13406
13449
  isCheckout: isCheckout
13407
13450
  };
13408
- window.parent.postMessage(message, "*");
13451
+ if (validatedOrigins) {
13452
+ for(var _iterator = _create_for_of_iterator_helper_loose(validatedOrigins), _step; !(_step = _iterator()).done;){
13453
+ var targetOrigin = _step.value;
13454
+ window.parent.postMessage(message, targetOrigin);
13455
+ }
13456
+ } else {
13457
+ window.parent.postMessage(message, "*");
13458
+ }
13409
13459
  }
13410
13460
  if (e2.type === EventType.FullSnapshot) {
13411
13461
  lastFullSnapshotEvent = e2;
@@ -18138,7 +18188,7 @@
18138
18188
  var __publicField = function(obj, key, value) {
18139
18189
  return __defNormalProp(obj, (typeof key === "undefined" ? "undefined" : _type_of(key)) !== "symbol" ? key + "" : key, value);
18140
18190
  };
18141
- function patch(source, name, replacement) {
18191
+ function patch$3(source, name, replacement) {
18142
18192
  try {
18143
18193
  if (!(name in source)) {
18144
18194
  return function() {};
@@ -18555,7 +18605,7 @@
18555
18605
  if (!_logger[level]) {
18556
18606
  return function() {};
18557
18607
  }
18558
- return patch(_logger, level, function(original) {
18608
+ return patch$3(_logger, level, function(original) {
18559
18609
  var _this1 = _this;
18560
18610
  return function() {
18561
18611
  for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
@@ -18976,11 +19026,6 @@
18976
19026
  PromisePolyfill = NpoPromise;
18977
19027
  }
18978
19028
 
18979
- var Config = {
18980
- DEBUG: false,
18981
- LIB_VERSION: '2.75.0'
18982
- };
18983
-
18984
19029
  /* eslint camelcase: "off", eqeqeq: "off" */
18985
19030
 
18986
19031
  // Maximum allowed session recording length
@@ -20712,6 +20757,17 @@
20712
20757
 
20713
20758
  var NOOP_FUNC = function () {};
20714
20759
 
20760
+ var urlMatchesRegexList = function (url, regexList) {
20761
+ var matches = false;
20762
+ for (var i = 0; i < regexList.length; i++) {
20763
+ if (url.match(regexList[i])) {
20764
+ matches = true;
20765
+ break;
20766
+ }
20767
+ }
20768
+ return matches;
20769
+ };
20770
+
20715
20771
  var JSONStringify = null, JSONParse = null;
20716
20772
  if (typeof JSON !== 'undefined') {
20717
20773
  JSONStringify = JSON.stringify;
@@ -21183,7 +21239,7 @@
21183
21239
  };
21184
21240
  }
21185
21241
 
21186
- var logger$6 = console_with_prefix('lock');
21242
+ var logger$8 = console_with_prefix('lock');
21187
21243
 
21188
21244
  /**
21189
21245
  * SharedLock: a mutex built on HTML5 localStorage, to ensure that only one browser
@@ -21235,7 +21291,7 @@
21235
21291
 
21236
21292
  var delay = function(cb) {
21237
21293
  if (new Date().getTime() - startTime > timeoutMS) {
21238
- logger$6.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21294
+ logger$8.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
21239
21295
  storage.removeItem(keyZ);
21240
21296
  storage.removeItem(keyY);
21241
21297
  loop();
@@ -21382,7 +21438,7 @@
21382
21438
  }, this));
21383
21439
  };
21384
21440
 
21385
- var logger$5 = console_with_prefix('batch');
21441
+ var logger$7 = console_with_prefix('batch');
21386
21442
 
21387
21443
  /**
21388
21444
  * RequestQueue: queue for batching API requests with localStorage backup for retries.
@@ -21411,7 +21467,7 @@
21411
21467
  timeoutMS: options.sharedLockTimeoutMS,
21412
21468
  });
21413
21469
  }
21414
- this.reportError = options.errorReporter || _.bind(logger$5.error, logger$5);
21470
+ this.reportError = options.errorReporter || _.bind(logger$7.error, logger$7);
21415
21471
 
21416
21472
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
21417
21473
 
@@ -21744,7 +21800,7 @@
21744
21800
  // maximum interval between request retries after exponential backoff
21745
21801
  var MAX_RETRY_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
21746
21802
 
21747
- var logger$4 = console_with_prefix('batch');
21803
+ var logger$6 = console_with_prefix('batch');
21748
21804
 
21749
21805
  /**
21750
21806
  * RequestBatcher: manages the queueing, flushing, retry etc of requests of one
@@ -21872,7 +21928,7 @@
21872
21928
  */
21873
21929
  RequestBatcher.prototype.flush = function(options) {
21874
21930
  if (this.requestInProgress) {
21875
- logger$4.log('Flush: Request already in progress');
21931
+ logger$6.log('Flush: Request already in progress');
21876
21932
  return PromisePolyfill.resolve();
21877
21933
  }
21878
21934
 
@@ -22049,7 +22105,7 @@
22049
22105
  if (options.unloading) {
22050
22106
  requestOptions.transport = 'sendBeacon';
22051
22107
  }
22052
- logger$4.log('MIXPANEL REQUEST:', dataForRequest);
22108
+ logger$6.log('MIXPANEL REQUEST:', dataForRequest);
22053
22109
  return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
22054
22110
  }, this))
22055
22111
  .catch(_.bind(function(err) {
@@ -22062,7 +22118,7 @@
22062
22118
  * Log error to global logger and optional user-defined logger.
22063
22119
  */
22064
22120
  RequestBatcher.prototype.reportError = function(msg, err) {
22065
- logger$4.error.apply(logger$4.error, arguments);
22121
+ logger$6.error.apply(logger$6.error, arguments);
22066
22122
  if (this.errorReporter) {
22067
22123
  try {
22068
22124
  if (!(err instanceof Error)) {
@@ -22070,7 +22126,7 @@
22070
22126
  }
22071
22127
  this.errorReporter(msg, err);
22072
22128
  } catch(err) {
22073
- logger$4.error(err);
22129
+ logger$6.error(err);
22074
22130
  }
22075
22131
  }
22076
22132
  };
@@ -22087,6 +22143,29 @@
22087
22143
 
22088
22144
  var RECORD_ENQUEUE_THROTTLE_MS = 250;
22089
22145
 
22146
+ var validateAllowedOrigins = function(origins, logger) {
22147
+ if (!_.isArray(origins)) {
22148
+ if (origins) {
22149
+ logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
22150
+ }
22151
+ return [];
22152
+ }
22153
+ var valid = [];
22154
+ for (var i = 0; i < origins.length; i++) {
22155
+ try {
22156
+ var origin = new URL(origins[i]).origin;
22157
+ if (origin === 'null') {
22158
+ logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
22159
+ continue;
22160
+ }
22161
+ valid.push(origin);
22162
+ } catch (e) {
22163
+ logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
22164
+ }
22165
+ }
22166
+ return valid;
22167
+ };
22168
+
22090
22169
  // stateless utils
22091
22170
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
22092
22171
 
@@ -22192,7 +22271,7 @@
22192
22271
 
22193
22272
  var MAX_DEPTH = 5;
22194
22273
 
22195
- var logger$3 = console_with_prefix('autocapture');
22274
+ var logger$5 = console_with_prefix('autocapture');
22196
22275
 
22197
22276
 
22198
22277
  function getClasses(el) {
@@ -22456,7 +22535,7 @@
22456
22535
  return false;
22457
22536
  }
22458
22537
  } catch (err) {
22459
- logger$3.critical('Error while checking element in allowElementCallback', err);
22538
+ logger$5.critical('Error while checking element in allowElementCallback', err);
22460
22539
  return false;
22461
22540
  }
22462
22541
  }
@@ -22473,7 +22552,7 @@
22473
22552
  return true;
22474
22553
  }
22475
22554
  } catch (err) {
22476
- logger$3.critical('Error while checking selector: ' + sel, err);
22555
+ logger$5.critical('Error while checking selector: ' + sel, err);
22477
22556
  }
22478
22557
  }
22479
22558
  return false;
@@ -22488,7 +22567,7 @@
22488
22567
  return true;
22489
22568
  }
22490
22569
  } catch (err) {
22491
- logger$3.critical('Error while checking element in blockElementCallback', err);
22570
+ logger$5.critical('Error while checking element in blockElementCallback', err);
22492
22571
  return true;
22493
22572
  }
22494
22573
  }
@@ -22502,7 +22581,7 @@
22502
22581
  return true;
22503
22582
  }
22504
22583
  } catch (err) {
22505
- logger$3.critical('Error while checking selector: ' + sel, err);
22584
+ logger$5.critical('Error while checking selector: ' + sel, err);
22506
22585
  }
22507
22586
  }
22508
22587
  }
@@ -23050,177 +23129,826 @@
23050
23129
  }
23051
23130
 
23052
23131
  /**
23053
- * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23132
+ * This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
23133
+ * the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
23134
+ *
23135
+ * This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
23136
+ * header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
23137
+ *
23054
23138
  */
23055
23139
 
23140
+ var logger$4 = console_with_prefix('network-plugin');
23056
23141
 
23057
- var logger$2 = console_with_prefix('recorder');
23058
- var CompressionStream = win['CompressionStream'];
23059
-
23060
- var RECORDER_BATCHER_LIB_CONFIG = {
23061
- 'batch_size': 1000,
23062
- 'batch_flush_interval_ms': 10 * 1000,
23063
- 'batch_request_timeout_ms': 90 * 1000,
23064
- 'batch_autostart': true
23065
- };
23142
+ /**
23143
+ * Get the time origin for converting performance timestamps to absolute timestamps.
23144
+ * Uses Date.now() - performance.now() instead of performance.timeOrigin because
23145
+ * browsers can report timeOrigin values that are skewed from actual time, and some
23146
+ * browsers (notably older Safari versions) don't implement timeOrigin at all.
23147
+ * See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
23148
+ * @param {Window} win
23149
+ * @returns {number}
23150
+ */
23151
+ function getTimeOrigin(win) {
23152
+ return Math.round(Date.now() - win.performance.now());
23153
+ }
23066
23154
 
23067
- var ACTIVE_SOURCES = new Set([
23068
- IncrementalSource.MouseMove,
23069
- IncrementalSource.MouseInteraction,
23070
- IncrementalSource.Scroll,
23071
- IncrementalSource.ViewportResize,
23072
- IncrementalSource.Input,
23073
- IncrementalSource.TouchMove,
23074
- IncrementalSource.MediaInteraction,
23075
- IncrementalSource.Drag,
23076
- IncrementalSource.Selection,
23077
- ]);
23155
+ /**
23156
+ * @typedef {import('../index.d.ts').InitiatorType} InitiatorType
23157
+ * @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
23158
+ * @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
23159
+ * @typedef {import('../index.d.ts').NetworkData} NetworkData
23160
+ */
23078
23161
 
23079
- function isUserEvent(ev) {
23080
- return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23081
- }
23162
+ /**
23163
+ * @typedef {Record<string, string>} Headers
23164
+ */
23082
23165
 
23083
23166
  /**
23084
- * @typedef {Object} SerializedRecording
23085
- * @property {number} idleExpires
23086
- * @property {number} maxExpires
23087
- * @property {number} replayStartTime
23088
- * @property {number} lastEventTimestamp
23089
- * @property {number} seqNo
23090
- * @property {string} batchStartUrl
23091
- * @property {string} replayId
23092
- * @property {string} tabId
23093
- * @property {string} replayStartUrl
23167
+ * @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
23094
23168
  */
23095
23169
 
23096
23170
  /**
23097
- * @typedef {Object} SessionRecordingOptions
23098
- * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23099
- * @property {String} [options.replayId] - unique uuid for a single replay
23100
- * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23101
- * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23102
- * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23103
- * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23104
- * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23105
- * optional properties for deserialization:
23106
- * @property {number} idleExpires
23107
- * @property {number} maxExpires
23108
- * @property {number} replayStartTime
23109
- * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23110
- * @property {number} seqNo
23111
- * @property {string} batchStartUrl
23112
- * @property {string} replayStartUrl
23171
+ * @callback networkCallback
23172
+ * @param {NetworkData} data
23173
+ * @returns {void}
23113
23174
  */
23114
23175
 
23115
23176
  /**
23116
- * @typedef {Object} UserIdInfo
23117
- * @property {string} distinct_id
23118
- * @property {string} user_id
23119
- * @property {string} device_id
23177
+ * @callback listenerHandler
23178
+ * @returns {void}
23120
23179
  */
23121
23180
 
23181
+ /**
23182
+ * @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
23183
+ */
23122
23184
 
23123
23185
  /**
23124
- * This class encapsulates a single session recording and its lifecycle.
23125
- * @param {SessionRecordingOptions} options
23186
+ * @typedef {Object} RecordPlugin
23187
+ * @property {string} name
23188
+ * @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
23189
+ * @property {NetworkRecordOptions} [options]
23126
23190
  */
23127
- var SessionRecording = function(options) {
23128
- this._mixpanel = options.mixpanelInstance;
23129
- this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23130
- this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23131
- this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23132
- this._rrwebRecord = options.rrwebRecord || null;
23133
23191
 
23134
- // internal rrweb stopRecording function
23135
- this._stopRecording = null;
23136
- this.replayId = options.replayId;
23192
+ /** @type {Required<NetworkRecordOptions>} */
23193
+ var defaultNetworkOptions = {
23194
+ initiatorTypes: [
23195
+ 'audio',
23196
+ 'beacon',
23197
+ 'body',
23198
+ 'css',
23199
+ 'early-hint',
23200
+ 'embed',
23201
+ 'fetch',
23202
+ 'frame',
23203
+ 'iframe',
23204
+ 'icon',
23205
+ 'image',
23206
+ 'img',
23207
+ 'input',
23208
+ 'link',
23209
+ 'navigation',
23210
+ 'object',
23211
+ 'ping',
23212
+ 'script',
23213
+ 'track',
23214
+ 'video',
23215
+ 'xmlhttprequest',
23216
+ ],
23217
+ ignoreRequestFn: function() { return false; },
23218
+ recordHeaders: {
23219
+ request: [],
23220
+ response: [],
23221
+ },
23222
+ recordBodyUrls: {
23223
+ request: [],
23224
+ response: [],
23225
+ },
23226
+ recordInitialRequests: false,
23227
+ };
23137
23228
 
23138
- this.batchStartUrl = options.batchStartUrl || null;
23139
- this.replayStartUrl = options.replayStartUrl || null;
23140
- this.idleExpires = options.idleExpires || null;
23141
- this.maxExpires = options.maxExpires || null;
23142
- this.replayStartTime = options.replayStartTime || null;
23143
- this.lastEventTimestamp = options.lastEventTimestamp || null;
23144
- this.seqNo = options.seqNo || 0;
23229
+ /**
23230
+ * @param {PerformanceEntry} entry
23231
+ * @returns {entry is PerformanceNavigationTiming}
23232
+ */
23233
+ function isNavigationTiming(entry) {
23234
+ return entry.entryType === 'navigation';
23235
+ }
23145
23236
 
23146
- this.idleTimeoutId = null;
23147
- this.maxTimeoutId = null;
23237
+ /**
23238
+ * @param {PerformanceEntry} entry
23239
+ * @returns {entry is PerformanceResourceTiming}
23240
+ */
23241
+ function isResourceTiming (entry) {
23242
+ return entry.entryType === 'resource';
23243
+ }
23148
23244
 
23149
- this.recordMaxMs = MAX_RECORDING_MS;
23150
- this.recordMinMs = 0;
23245
+ function findLast(array, predicate) {
23246
+ var length = array.length;
23247
+ for (var i = length - 1; i >= 0; i -= 1) {
23248
+ if (predicate(array[i])) {
23249
+ return array[i];
23250
+ }
23251
+ }
23252
+ }
23151
23253
 
23152
- // disable persistence if localStorage is not supported
23153
- // request-queue will automatically disable persistence if indexedDB fails to initialize
23154
- var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23254
+ /**
23255
+ * Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
23256
+ * Adapted from Sentry's `fill` utility:
23257
+ * https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
23258
+ *
23259
+ * @param {object} source - The object containing the method to patch
23260
+ * @param {string} name - The method name to patch
23261
+ * @param {function} replacementFactory - A function that receives the original method and returns the replacement
23262
+ * @returns {function} A function that restores the original method
23263
+ */
23264
+ function patch(source, name, replacementFactory) {
23265
+ if (!(name in source) || typeof source[name] !== 'function') {
23266
+ return function() {};
23267
+ }
23268
+ var original = source[name];
23269
+ var wrapped = replacementFactory(original);
23270
+ source[name] = wrapped;
23271
+ return function() {
23272
+ source[name] = original;
23273
+ };
23274
+ }
23155
23275
 
23156
- // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23157
- this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23158
- this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23159
- this.batcher = new RequestBatcher(this.batcherKey, {
23160
- errorReporter: this.reportError.bind(this),
23161
- flushOnlyOnInterval: true,
23162
- libConfig: RECORDER_BATCHER_LIB_CONFIG,
23163
- sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23164
- queueStorage: this.queueStorage,
23165
- sharedLockStorage: options.sharedLockStorage,
23166
- usePersistence: usePersistence,
23167
- stopAllBatchingFunc: this.stopRecording.bind(this),
23168
23276
 
23169
- // increased throttle and shared lock timeout because recording events are very high frequency.
23170
- // this will minimize the amount of lock contention between enqueued events.
23171
- // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23172
- enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23173
- sharedLockTimeoutMS: 10 * 1000,
23174
- });
23175
- };
23277
+ /**
23278
+ * Maximum body size to record (1MB)
23279
+ */
23280
+ var MAX_BODY_SIZE = 1024 * 1024;
23176
23281
 
23177
23282
  /**
23178
- * @returns {UserIdInfo}
23283
+ * Truncate string if it exceeds max size
23284
+ * @param {string} str
23285
+ * @returns {string}
23179
23286
  */
23180
- SessionRecording.prototype.getUserIdInfo = function () {
23181
- if (this.finalFlushUserIdInfo) {
23182
- return this.finalFlushUserIdInfo;
23287
+ function truncateBody(str) {
23288
+ if (!str || typeof str !== 'string') {
23289
+ return str;
23290
+ }
23291
+ if (str.length > MAX_BODY_SIZE) {
23292
+ logger$4.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
23293
+ return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
23183
23294
  }
23295
+ return str;
23296
+ }
23184
23297
 
23185
- var userIdInfo = {
23186
- 'distinct_id': String(this._mixpanel.get_distinct_id()),
23298
+ /**
23299
+ * @param {networkCallback} cb
23300
+ * @param {Window} win
23301
+ * @param {Required<NetworkRecordOptions>} options
23302
+ * @returns {listenerHandler}
23303
+ */
23304
+ function initPerformanceObserver(cb, win, options) {
23305
+ if (!win.PerformanceObserver) {
23306
+ logger$4.error('PerformanceObserver not supported');
23307
+ return function() {
23308
+ //
23309
+ };
23310
+ }
23311
+ if (options.recordInitialRequests) {
23312
+ var initialPerformanceEntries = win.performance
23313
+ .getEntries()
23314
+ .filter(function(entry) {
23315
+ return isNavigationTiming(entry) ||
23316
+ (isResourceTiming(entry) &&
23317
+ options.initiatorTypes.includes(entry.initiatorType));
23318
+ });
23319
+ cb({
23320
+ requests: initialPerformanceEntries.map(function(entry) {
23321
+ return {
23322
+ url: entry.name,
23323
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23324
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23325
+ startTime: Math.round(entry.startTime),
23326
+ endTime: Math.round(entry.responseEnd),
23327
+ timeOrigin: getTimeOrigin(win),
23328
+ };
23329
+ }),
23330
+ isInitial: true,
23331
+ });
23332
+ }
23333
+ var observer = new win.PerformanceObserver(function(entries) {
23334
+ var performanceEntries = entries
23335
+ .getEntries()
23336
+ .filter(function(entry) {
23337
+ return isResourceTiming(entry) &&
23338
+ options.initiatorTypes.includes(entry.initiatorType) &&
23339
+ entry.initiatorType !== 'xmlhttprequest' &&
23340
+ entry.initiatorType !== 'fetch';
23341
+ });
23342
+ cb({
23343
+ requests: performanceEntries.map(function(entry) {
23344
+ return {
23345
+ url: entry.name,
23346
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23347
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
23348
+ startTime: Math.round(entry.startTime),
23349
+ endTime: Math.round(entry.responseEnd),
23350
+ timeOrigin: getTimeOrigin(win),
23351
+ };
23352
+ }),
23353
+ });
23354
+ });
23355
+ observer.observe({ entryTypes: ['navigation', 'resource'] });
23356
+ return function() {
23357
+ observer.disconnect();
23187
23358
  };
23359
+ }
23188
23360
 
23189
- // send ID management props if they exist
23190
- var deviceId = this._mixpanel.get_property('$device_id');
23191
- if (deviceId) {
23192
- userIdInfo['$device_id'] = deviceId;
23193
- }
23194
- var userId = this._mixpanel.get_property('$user_id');
23195
- if (userId) {
23196
- userIdInfo['$user_id'] = userId;
23361
+ /**
23362
+ * Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
23363
+ * @param {'request' | 'response'} type
23364
+ * @param {NetworkRecordOptions['recordHeaders']} recordHeaders
23365
+ * @param {string} headerName
23366
+ * @returns {boolean}
23367
+ */
23368
+ function shouldRecordHeader(type, recordHeaders, headerName) {
23369
+ if (!recordHeaders[type] || recordHeaders[type].length === 0) {
23370
+ return false;
23197
23371
  }
23198
- return userIdInfo;
23199
- };
23200
23372
 
23201
- SessionRecording.prototype.unloadPersistedData = function () {
23202
- this.batcher.stop();
23373
+ return recordHeaders[type].includes(headerName.toLowerCase());
23374
+ }
23203
23375
 
23204
- return this.queueStorage.init().catch(function () {
23205
- this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23206
- }.bind(this)).then(function () {
23207
- // if the recording is too short, just delete any stored events without flushing
23208
- if (this.getDurationMs() < this._getRecordMinMs()) {
23209
- return this.queueStorage.removeItem(this.batcherKey);
23210
- }
23376
+ /**
23377
+ * Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
23378
+ * @param {'request' | 'response'} type
23379
+ * @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
23380
+ * @param {string} url
23381
+ * @returns {boolean}
23382
+ */
23383
+ function shouldRecordBody(type, recordBodyUrls, url) {
23384
+ if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
23385
+ return false;
23386
+ }
23211
23387
 
23212
- return this.batcher.flush()
23213
- .then(function () {
23214
- return this.queueStorage.removeItem(this.batcherKey);
23215
- }.bind(this));
23216
- }.bind(this));
23217
- };
23388
+ return urlMatchesRegexList(url, recordBodyUrls[type]);
23389
+ }
23218
23390
 
23219
- SessionRecording.prototype.getConfig = function(configVar) {
23220
- return this._mixpanel.get_config(configVar);
23221
- };
23391
+ function tryReadXHRBody(body) {
23392
+ if (body === null || body === undefined) {
23393
+ return null;
23394
+ }
23222
23395
 
23223
- // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
23396
+ var result;
23397
+ if (typeof body === 'string') {
23398
+ result = body;
23399
+ } else if (body instanceof Document) {
23400
+ result = body.textContent;
23401
+ } else if (body instanceof FormData) {
23402
+ result = _.HTTPBuildQuery(body);
23403
+ } else if (_.isObject(body)) {
23404
+ try {
23405
+ result = JSON.stringify(body);
23406
+ } catch (e) {
23407
+ return 'Failed to stringify response object';
23408
+ }
23409
+ } else {
23410
+ return 'Cannot read body of type ' + typeof body;
23411
+ }
23412
+
23413
+ return truncateBody(result);
23414
+ }
23415
+
23416
+ /**
23417
+ * @param {Request | Response} r
23418
+ * @returns {Promise<string>}
23419
+ */
23420
+ function tryReadFetchBody(r) {
23421
+ return new Promise(function(resolve) {
23422
+ var timeout = setTimeout(function() {
23423
+ resolve('Timeout while trying to read body');
23424
+ }, 500);
23425
+ try {
23426
+ r.clone()
23427
+ .text()
23428
+ .then(
23429
+ function(txt) {
23430
+ clearTimeout(timeout);
23431
+ resolve(truncateBody(txt));
23432
+ },
23433
+ function(reason) {
23434
+ clearTimeout(timeout);
23435
+ resolve('Failed to read body: ' + String(reason));
23436
+ }
23437
+ );
23438
+ } catch (e) {
23439
+ clearTimeout(timeout);
23440
+ resolve('Failed to read body: ' + String(e));
23441
+ }
23442
+ });
23443
+ }
23444
+
23445
+ /**
23446
+ * @param {Window} win
23447
+ * @param {string} initiatorType
23448
+ * @param {string} url
23449
+ * @param {number} [after]
23450
+ * @param {number} [before]
23451
+ * @param {number} [attempt]
23452
+ * @returns {Promise<PerformanceResourceTiming>}
23453
+ */
23454
+ function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
23455
+ if (attempt === undefined) {
23456
+ attempt = 0;
23457
+ }
23458
+ if (attempt > 10) {
23459
+ logger$4.error('Cannot find performance entry');
23460
+ return Promise.resolve(null);
23461
+ }
23462
+ var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
23463
+ win.performance.getEntriesByName(url)
23464
+ );
23465
+ var performanceEntry = findLast(
23466
+ urlPerformanceEntries,
23467
+ function(entry) {
23468
+ return isResourceTiming(entry) &&
23469
+ entry.initiatorType === initiatorType &&
23470
+ (!after || entry.startTime >= after) &&
23471
+ (!before || entry.startTime <= before);
23472
+ }
23473
+ );
23474
+ if (!performanceEntry) {
23475
+ return new Promise(function(resolve) {
23476
+ setTimeout(resolve, 50 * attempt);
23477
+ }).then(function() {
23478
+ return getRequestPerformanceEntry(
23479
+ win,
23480
+ initiatorType,
23481
+ url,
23482
+ after,
23483
+ before,
23484
+ attempt + 1
23485
+ );
23486
+ });
23487
+ }
23488
+ return Promise.resolve(performanceEntry);
23489
+ }
23490
+
23491
+ /**
23492
+ * @param {networkCallback} cb
23493
+ * @param {Window} win
23494
+ * @param {Required<NetworkRecordOptions>} options
23495
+ * @returns {listenerHandler}
23496
+ */
23497
+ function initXhrObserver(cb, win, options) {
23498
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
23499
+ return function() {
23500
+ //
23501
+ };
23502
+ }
23503
+ var restorePatch = patch(
23504
+ win.XMLHttpRequest.prototype,
23505
+ 'open',
23506
+ function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
23507
+ return function(
23508
+ /** @type {string} */ method,
23509
+ /** @type {string | URL} */ url,
23510
+ /** @type {boolean} */ async,
23511
+ username, password
23512
+ ) {
23513
+ if (async === undefined) {
23514
+ async = true;
23515
+ }
23516
+ var xhr = /** @type {XMLHttpRequest} */ (this);
23517
+ var req = new Request(url, { method: method });
23518
+ /** @type {Partial<NetworkRequest>} */
23519
+ var networkRequest = {};
23520
+ /** @type {number | undefined} */
23521
+ var after;
23522
+ /** @type {number | undefined} */
23523
+ var before;
23524
+
23525
+ /** @type {Headers} */
23526
+ var requestHeaders = {};
23527
+ var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
23528
+ xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
23529
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23530
+ requestHeaders[header] = value;
23531
+ }
23532
+ return originalSetRequestHeader(header, value);
23533
+ };
23534
+ networkRequest.requestHeaders = requestHeaders;
23535
+
23536
+ var originalSend = xhr.send.bind(xhr);
23537
+ xhr.send = function(/** @type {Body} */ body) {
23538
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23539
+ networkRequest.requestBody = tryReadXHRBody(body);
23540
+ }
23541
+ after = win.performance.now();
23542
+ return originalSend(body);
23543
+ };
23544
+ xhr.addEventListener('readystatechange', function() {
23545
+ if (xhr.readyState !== xhr.DONE) {
23546
+ return;
23547
+ }
23548
+ before = win.performance.now();
23549
+ /** @type {Headers} */
23550
+ var responseHeaders = {};
23551
+ var rawHeaders = xhr.getAllResponseHeaders();
23552
+ if (rawHeaders) {
23553
+ var headers = rawHeaders.trim().split(/[\r\n]+/);
23554
+ headers.forEach(function(line) {
23555
+ if (!line) return;
23556
+ var colonIndex = line.indexOf(': ');
23557
+ if (colonIndex === -1) return;
23558
+ var header = line.substring(0, colonIndex);
23559
+ var value = line.substring(colonIndex + 2);
23560
+ if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
23561
+ responseHeaders[header] = value;
23562
+ }
23563
+ });
23564
+ }
23565
+ networkRequest.responseHeaders = responseHeaders;
23566
+ if (
23567
+ shouldRecordBody('response', options.recordBodyUrls, req.url)
23568
+ ) {
23569
+ networkRequest.responseBody = tryReadXHRBody(xhr.response);
23570
+ }
23571
+ getRequestPerformanceEntry(
23572
+ win,
23573
+ 'xmlhttprequest',
23574
+ req.url,
23575
+ after,
23576
+ before
23577
+ )
23578
+ .then(function(entry) {
23579
+ if (!entry) {
23580
+ logger$4.error('Failed to get performance entry for XHR request to ' + req.url);
23581
+ return;
23582
+ }
23583
+ /** @type {NetworkRequest} */
23584
+ var request = {
23585
+ url: entry.name,
23586
+ method: req.method,
23587
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23588
+ status: xhr.status,
23589
+ startTime: Math.round(entry.startTime),
23590
+ endTime: Math.round(entry.responseEnd),
23591
+ timeOrigin: getTimeOrigin(win),
23592
+ requestHeaders: networkRequest.requestHeaders,
23593
+ requestBody: networkRequest.requestBody,
23594
+ responseHeaders: networkRequest.responseHeaders,
23595
+ responseBody: networkRequest.responseBody,
23596
+ };
23597
+ cb({ requests: [request] });
23598
+ })
23599
+ .catch(function(e) {
23600
+ logger$4.error('Error recording XHR request to ' + req.url + ': ' + String(e));
23601
+ });
23602
+ });
23603
+
23604
+ originalOpen.call(xhr, method, url, async, username, password);
23605
+ };
23606
+ }
23607
+ );
23608
+ return function() {
23609
+ restorePatch();
23610
+ };
23611
+ }
23612
+
23613
+ /**
23614
+ * @param {networkCallback} cb
23615
+ * @param {Window} win
23616
+ * @param {Required<NetworkRecordOptions>} options
23617
+ * @returns {listenerHandler}
23618
+ */
23619
+ function initFetchObserver(cb, win, options) {
23620
+ if (!options.initiatorTypes.includes('fetch')) {
23621
+ return function() {
23622
+ //
23623
+ };
23624
+ }
23625
+
23626
+ var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
23627
+ return function() {
23628
+ var req = new Request(arguments[0], arguments[1]);
23629
+ /** @type {Response | undefined} */
23630
+ var res;
23631
+ /** @type {Partial<NetworkRequest>} */
23632
+ var networkRequest = {};
23633
+ /** @type {number | undefined} */
23634
+ var after;
23635
+ /** @type {number | undefined} */
23636
+ var before;
23637
+
23638
+ var originalFetchPromise;
23639
+ var requestBodyPromise = Promise.resolve(undefined);
23640
+ var responseBodyPromise = Promise.resolve(undefined);
23641
+ try {
23642
+ /** @type {Headers} */
23643
+ var requestHeaders = {};
23644
+ req.headers.forEach(function(value, header) {
23645
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
23646
+ requestHeaders[header] = value;
23647
+ }
23648
+ });
23649
+ networkRequest.requestHeaders = requestHeaders;
23650
+
23651
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
23652
+ requestBodyPromise = tryReadFetchBody(req)
23653
+ .then(function(body) {
23654
+ networkRequest.requestBody = body;
23655
+ });
23656
+ }
23657
+
23658
+ after = win.performance.now();
23659
+ originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
23660
+ res = response;
23661
+ before = win.performance.now();
23662
+
23663
+ /** @type {Headers} */
23664
+ var responseHeaders = {};
23665
+ res.headers.forEach(function(value, header) {
23666
+ if (shouldRecordHeader('response', options.recordHeaders, header)) {
23667
+ responseHeaders[header] = value;
23668
+ }
23669
+ });
23670
+ networkRequest.responseHeaders = responseHeaders;
23671
+
23672
+ if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
23673
+ responseBodyPromise = tryReadFetchBody(res)
23674
+ .then(function(body) {
23675
+ networkRequest.responseBody = body;
23676
+ });
23677
+ }
23678
+
23679
+ return res;
23680
+ });
23681
+ } catch (e) {
23682
+ originalFetchPromise = Promise.reject(e);
23683
+ }
23684
+
23685
+ // await concurrently so we don't delay the fetch response
23686
+ Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
23687
+ .then(function () {
23688
+ return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
23689
+ })
23690
+ .then(function(entry) {
23691
+ if (!entry) {
23692
+ logger$4.error('Failed to get performance entry for fetch request to ' + req.url);
23693
+ return;
23694
+ }
23695
+ /** @type {NetworkRequest} */
23696
+ var request = {
23697
+ url: entry.name,
23698
+ method: req.method,
23699
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
23700
+ status: res ? res.status : undefined,
23701
+ startTime: Math.round(entry.startTime),
23702
+ endTime: Math.round(entry.responseEnd),
23703
+ timeOrigin: getTimeOrigin(win),
23704
+ requestHeaders: networkRequest.requestHeaders,
23705
+ requestBody: networkRequest.requestBody,
23706
+ responseHeaders: networkRequest.responseHeaders,
23707
+ responseBody: networkRequest.responseBody,
23708
+ };
23709
+ cb({ requests: [request] });
23710
+ })
23711
+ .catch(function (e) {
23712
+ logger$4.error('Error recording fetch request to ' + req.url + ': ' + String(e));
23713
+ });
23714
+
23715
+ return originalFetchPromise;
23716
+ };
23717
+ });
23718
+ return function() {
23719
+ restorePatch();
23720
+ };
23721
+ }
23722
+
23723
+ /**
23724
+ * @param {networkCallback} callback
23725
+ * @param {Window} win
23726
+ * @param {NetworkRecordOptions} options
23727
+ * @returns {listenerHandler}
23728
+ */
23729
+ function initNetworkObserver(callback, win, options) {
23730
+ if (!('performance' in win)) {
23731
+ return function() {
23732
+ //
23733
+ };
23734
+ }
23735
+
23736
+ var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
23737
+ var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
23738
+ options = Object.assign({}, options, {
23739
+ recordHeaders: recordHeaders,
23740
+ recordBodyUrls: recordBodyUrls,
23741
+ });
23742
+ var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
23743
+
23744
+ /** @type {networkCallback} */
23745
+ var cb = function(data) {
23746
+ var requests = data.requests.filter(function(request) {
23747
+ var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
23748
+ return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
23749
+ });
23750
+ if (requests.length > 0 || data.isInitial) {
23751
+ callback(Object.assign({}, data, { requests: requests }));
23752
+ }
23753
+ };
23754
+ var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
23755
+ var xhrObserver = initXhrObserver(cb, win, networkOptions);
23756
+ var fetchObserver = initFetchObserver(cb, win, networkOptions);
23757
+ return function() {
23758
+ performanceObserver();
23759
+ xhrObserver();
23760
+ fetchObserver();
23761
+ };
23762
+ }
23763
+
23764
+ // arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
23765
+ // a changed format in the mixpanel product.
23766
+ var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
23767
+
23768
+ /**
23769
+ * @param {NetworkRecordOptions} [options]
23770
+ * @returns {RecordPlugin}
23771
+ */
23772
+ var getRecordNetworkPlugin = function(options) {
23773
+ return {
23774
+ name: NETWORK_PLUGIN_NAME,
23775
+ observer: initNetworkObserver,
23776
+ options: options,
23777
+ };
23778
+ };
23779
+
23780
+ /**
23781
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
23782
+ */
23783
+
23784
+
23785
+ var logger$3 = console_with_prefix('recorder');
23786
+ var CompressionStream = win['CompressionStream'];
23787
+
23788
+ var RECORDER_BATCHER_LIB_CONFIG = {
23789
+ 'batch_size': 1000,
23790
+ 'batch_flush_interval_ms': 10 * 1000,
23791
+ 'batch_request_timeout_ms': 90 * 1000,
23792
+ 'batch_autostart': true
23793
+ };
23794
+
23795
+ var ACTIVE_SOURCES = new Set([
23796
+ IncrementalSource.MouseMove,
23797
+ IncrementalSource.MouseInteraction,
23798
+ IncrementalSource.Scroll,
23799
+ IncrementalSource.ViewportResize,
23800
+ IncrementalSource.Input,
23801
+ IncrementalSource.TouchMove,
23802
+ IncrementalSource.MediaInteraction,
23803
+ IncrementalSource.Drag,
23804
+ IncrementalSource.Selection,
23805
+ ]);
23806
+
23807
+ function isUserEvent(ev) {
23808
+ return ev.type === EventType.IncrementalSnapshot && ACTIVE_SOURCES.has(ev.data.source);
23809
+ }
23810
+
23811
+ /**
23812
+ * @typedef {Object} SerializedRecording
23813
+ * @property {number} idleExpires
23814
+ * @property {number} maxExpires
23815
+ * @property {number} replayStartTime
23816
+ * @property {number} lastEventTimestamp
23817
+ * @property {number} seqNo
23818
+ * @property {string} batchStartUrl
23819
+ * @property {string} replayId
23820
+ * @property {string} tabId
23821
+ * @property {string} replayStartUrl
23822
+ */
23823
+
23824
+ /**
23825
+ * @typedef {Object} SessionRecordingOptions
23826
+ * @property {Object} [options.mixpanelInstance] - reference to the core MixpanelLib
23827
+ * @property {String} [options.replayId] - unique uuid for a single replay
23828
+ * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
23829
+ * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
23830
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
23831
+ * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
23832
+ * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
23833
+ * optional properties for deserialization:
23834
+ * @property {number} idleExpires
23835
+ * @property {number} maxExpires
23836
+ * @property {number} replayStartTime
23837
+ * @property {number} lastEventTimestamp - the unix timestamp of the last recorded event from rrweb
23838
+ * @property {number} seqNo
23839
+ * @property {string} batchStartUrl
23840
+ * @property {string} replayStartUrl
23841
+ */
23842
+
23843
+ /**
23844
+ * @typedef {Object} UserIdInfo
23845
+ * @property {string} distinct_id
23846
+ * @property {string} user_id
23847
+ * @property {string} device_id
23848
+ */
23849
+
23850
+
23851
+ /**
23852
+ * This class encapsulates a single session recording and its lifecycle.
23853
+ * @param {SessionRecordingOptions} options
23854
+ */
23855
+ var SessionRecording = function(options) {
23856
+ this._mixpanel = options.mixpanelInstance;
23857
+ this._onIdleTimeout = options.onIdleTimeout || NOOP_FUNC;
23858
+ this._onMaxLengthReached = options.onMaxLengthReached || NOOP_FUNC;
23859
+ this._onBatchSent = options.onBatchSent || NOOP_FUNC;
23860
+ this._rrwebRecord = options.rrwebRecord || null;
23861
+
23862
+ // internal rrweb stopRecording function
23863
+ this._stopRecording = null;
23864
+ this.replayId = options.replayId;
23865
+
23866
+ this.batchStartUrl = options.batchStartUrl || null;
23867
+ this.replayStartUrl = options.replayStartUrl || null;
23868
+ this.idleExpires = options.idleExpires || null;
23869
+ this.maxExpires = options.maxExpires || null;
23870
+ this.replayStartTime = options.replayStartTime || null;
23871
+ this.lastEventTimestamp = options.lastEventTimestamp || null;
23872
+ this.seqNo = options.seqNo || 0;
23873
+
23874
+ this.idleTimeoutId = null;
23875
+ this.maxTimeoutId = null;
23876
+
23877
+ this.recordMaxMs = MAX_RECORDING_MS;
23878
+ this.recordMinMs = 0;
23879
+
23880
+ // disable persistence if localStorage is not supported
23881
+ // request-queue will automatically disable persistence if indexedDB fails to initialize
23882
+ var usePersistence = localStorageSupported(options.sharedLockStorage, true) && !this.getConfig('disable_persistence');
23883
+
23884
+ // each replay has its own batcher key to avoid conflicts between rrweb events of different recordings
23885
+ this.batcherKey = '__mprec_' + this.getConfig('name') + '_' + this.getConfig('token') + '_' + this.replayId;
23886
+ this.queueStorage = new IDBStorageWrapper(RECORDING_EVENTS_STORE_NAME);
23887
+ this.batcher = new RequestBatcher(this.batcherKey, {
23888
+ errorReporter: this.reportError.bind(this),
23889
+ flushOnlyOnInterval: true,
23890
+ libConfig: RECORDER_BATCHER_LIB_CONFIG,
23891
+ sendRequestFunc: this.flushEventsWithOptOut.bind(this),
23892
+ queueStorage: this.queueStorage,
23893
+ sharedLockStorage: options.sharedLockStorage,
23894
+ usePersistence: usePersistence,
23895
+ stopAllBatchingFunc: this.stopRecording.bind(this),
23896
+
23897
+ // increased throttle and shared lock timeout because recording events are very high frequency.
23898
+ // this will minimize the amount of lock contention between enqueued events.
23899
+ // for session recordings there is a lock for each tab anyway, so there's no risk of deadlock between tabs.
23900
+ enqueueThrottleMs: RECORD_ENQUEUE_THROTTLE_MS,
23901
+ sharedLockTimeoutMS: 10 * 1000,
23902
+ });
23903
+ };
23904
+
23905
+ /**
23906
+ * @returns {UserIdInfo}
23907
+ */
23908
+ SessionRecording.prototype.getUserIdInfo = function () {
23909
+ if (this.finalFlushUserIdInfo) {
23910
+ return this.finalFlushUserIdInfo;
23911
+ }
23912
+
23913
+ var userIdInfo = {
23914
+ 'distinct_id': String(this._mixpanel.get_distinct_id()),
23915
+ };
23916
+
23917
+ // send ID management props if they exist
23918
+ var deviceId = this._mixpanel.get_property('$device_id');
23919
+ if (deviceId) {
23920
+ userIdInfo['$device_id'] = deviceId;
23921
+ }
23922
+ var userId = this._mixpanel.get_property('$user_id');
23923
+ if (userId) {
23924
+ userIdInfo['$user_id'] = userId;
23925
+ }
23926
+ return userIdInfo;
23927
+ };
23928
+
23929
+ SessionRecording.prototype.unloadPersistedData = function () {
23930
+ this.batcher.stop();
23931
+
23932
+ return this.queueStorage.init().catch(function () {
23933
+ this.reportError('Error initializing IndexedDB storage for unloading persisted data.');
23934
+ }.bind(this)).then(function () {
23935
+ // if the recording is too short, just delete any stored events without flushing
23936
+ if (this.getDurationMs() < this._getRecordMinMs()) {
23937
+ return this.queueStorage.removeItem(this.batcherKey);
23938
+ }
23939
+
23940
+ return this.batcher.flush()
23941
+ .then(function () {
23942
+ return this.queueStorage.removeItem(this.batcherKey);
23943
+ }.bind(this));
23944
+ }.bind(this));
23945
+ };
23946
+
23947
+ SessionRecording.prototype.getConfig = function(configVar) {
23948
+ return this._mixpanel.get_config(configVar);
23949
+ };
23950
+
23951
+ // Alias for getConfig, used by the common addOptOutCheckMixpanelLib function which
23224
23952
  // reaches into this class instance and expects the snake case version of the function.
23225
23953
  // eslint-disable-next-line camelcase
23226
23954
  SessionRecording.prototype.get_config = function(configVar) {
@@ -23234,14 +23962,14 @@
23234
23962
  }
23235
23963
 
23236
23964
  if (this._stopRecording !== null) {
23237
- logger$2.log('Recording already in progress, skipping startRecording.');
23965
+ logger$3.log('Recording already in progress, skipping startRecording.');
23238
23966
  return;
23239
23967
  }
23240
23968
 
23241
23969
  this.recordMaxMs = this.getConfig('record_max_ms');
23242
23970
  if (this.recordMaxMs > MAX_RECORDING_MS) {
23243
23971
  this.recordMaxMs = MAX_RECORDING_MS;
23244
- logger$2.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
23972
+ logger$3.critical('record_max_ms cannot be greater than ' + MAX_RECORDING_MS + 'ms. Capping value.');
23245
23973
  }
23246
23974
 
23247
23975
  if (!this.maxExpires) {
@@ -23282,6 +24010,31 @@
23282
24010
 
23283
24011
  var privacyConfig = getPrivacyConfig(this._mixpanel);
23284
24012
 
24013
+ var plugins = [];
24014
+ if (this.getConfig('record_network')) {
24015
+ var options = this.getConfig('record_network_options') || {};
24016
+ // don't track requests to Mixpanel /record API
24017
+ var ignoreRequestUrls = (options.ignoreRequestUrls || []).slice();
24018
+ ignoreRequestUrls.push(this._getApiRoute());
24019
+ options.ignoreRequestUrls = ignoreRequestUrls;
24020
+
24021
+ plugins.push(getRecordNetworkPlugin(options));
24022
+ }
24023
+
24024
+ if (this.getConfig('record_console')) {
24025
+ plugins.push(
24026
+ getRecordConsolePlugin({
24027
+ stringifyOptions: {
24028
+ stringLengthLimit: 1000,
24029
+ numOfKeysLimit: 50,
24030
+ depthOfLimit: 2
24031
+ }
24032
+ })
24033
+ );
24034
+ }
24035
+
24036
+ var validatedOrigins = validateAllowedOrigins(this.getConfig('record_allowed_iframe_origins'), logger$3);
24037
+
23285
24038
  try {
23286
24039
  this._stopRecording = this._rrwebRecord({
23287
24040
  'emit': function (ev) {
@@ -23316,19 +24069,13 @@
23316
24069
  'maskTextSelector': '*',
23317
24070
  'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
23318
24071
  'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
24072
+ 'recordCrossOriginIframes': validatedOrigins.length > 0,
24073
+ 'allowedIframeOrigins': validatedOrigins,
23319
24074
  'recordCanvas': this.getConfig('record_canvas'),
23320
24075
  'sampling': {
23321
24076
  'canvas': 15
23322
24077
  },
23323
- 'plugins': this.getConfig('record_console') ? [
23324
- getRecordConsolePlugin({
23325
- stringifyOptions: {
23326
- stringLengthLimit: 1000,
23327
- numOfKeysLimit: 50,
23328
- depthOfLimit: 2
23329
- }
23330
- })
23331
- ] : []
24078
+ 'plugins': plugins,
23332
24079
  });
23333
24080
  } catch (err) {
23334
24081
  this.reportError('Unexpected error when starting rrweb recording.', err);
@@ -23443,6 +24190,10 @@
23443
24190
  return recording;
23444
24191
  };
23445
24192
 
24193
+ SessionRecording.prototype._getApiRoute = function () {
24194
+ return this.getConfig('api_routes')['record'];
24195
+ };
24196
+
23446
24197
  SessionRecording.prototype._sendRequest = function(currentReplayId, reqParams, reqBody, callback) {
23447
24198
  var onSuccess = function (response, responseBody) {
23448
24199
  // Update batch specific props only if the request was successful to guarantee ordering.
@@ -23462,7 +24213,7 @@
23462
24213
  });
23463
24214
  }.bind(this);
23464
24215
  var apiHost = (this._mixpanel.get_api_host && this._mixpanel.get_api_host('record')) || this.getConfig('api_host');
23465
- win['fetch'](apiHost + '/' + this.getConfig('api_routes')['record'] + '?' + new URLSearchParams(reqParams), {
24216
+ win['fetch'](apiHost + '/' + this._getApiRoute() + '?' + new URLSearchParams(reqParams), {
23466
24217
  'method': 'POST',
23467
24218
  'headers': {
23468
24219
  'Authorization': 'Basic ' + btoa(this.getConfig('token') + ':'),
@@ -23544,14 +24295,14 @@
23544
24295
 
23545
24296
 
23546
24297
  SessionRecording.prototype.reportError = function(msg, err) {
23547
- logger$2.error.apply(logger$2.error, arguments);
24298
+ logger$3.error.apply(logger$3.error, arguments);
23548
24299
  try {
23549
24300
  if (!err && !(msg instanceof Error)) {
23550
24301
  msg = new Error(msg);
23551
24302
  }
23552
24303
  this.getConfig('error_reporter')(msg, err);
23553
24304
  } catch(err) {
23554
- logger$2.error(err);
24305
+ logger$3.error(err);
23555
24306
  }
23556
24307
  };
23557
24308
 
@@ -23580,7 +24331,7 @@
23580
24331
  var configValue = this.getConfig('record_min_ms');
23581
24332
 
23582
24333
  if (configValue > MAX_VALUE_FOR_MIN_RECORDING_MS) {
23583
- logger$2.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
24334
+ logger$3.critical('record_min_ms cannot be greater than ' + MAX_VALUE_FOR_MIN_RECORDING_MS + 'ms. Capping value.');
23584
24335
  return MAX_VALUE_FOR_MIN_RECORDING_MS;
23585
24336
  }
23586
24337
 
@@ -23743,7 +24494,7 @@
23743
24494
  .catch(this.handleError.bind(this));
23744
24495
  };
23745
24496
 
23746
- var logger$1 = console_with_prefix('recorder');
24497
+ var logger$2 = console_with_prefix('recorder');
23747
24498
 
23748
24499
  /**
23749
24500
  * Recorder API: bundles rrweb and and exposes methods to start and stop recordings.
@@ -23759,7 +24510,7 @@
23759
24510
  */
23760
24511
  this.recordingRegistry = new RecordingRegistry({
23761
24512
  mixpanelInstance: this.mixpanelInstance,
23762
- errorReporter: logger$1.error,
24513
+ errorReporter: logger$2.error,
23763
24514
  sharedLockStorage: sharedLockStorage
23764
24515
  });
23765
24516
  this._flushInactivePromise = this.recordingRegistry.flushInactiveRecordings();
@@ -23771,17 +24522,17 @@
23771
24522
  MixpanelRecorder.prototype.startRecording = function(options) {
23772
24523
  options = options || {};
23773
24524
  if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
23774
- logger$1.log('Recording already in progress, skipping startRecording.');
24525
+ logger$2.log('Recording already in progress, skipping startRecording.');
23775
24526
  return;
23776
24527
  }
23777
24528
 
23778
24529
  var onIdleTimeout = function () {
23779
- logger$1.log('Idle timeout reached, restarting recording.');
24530
+ logger$2.log('Idle timeout reached, restarting recording.');
23780
24531
  this.resetRecording();
23781
24532
  }.bind(this);
23782
24533
 
23783
24534
  var onMaxLengthReached = function () {
23784
- logger$1.log('Max recording length reached, stopping recording.');
24535
+ logger$2.log('Max recording length reached, stopping recording.');
23785
24536
  this.resetRecording();
23786
24537
  }.bind(this);
23787
24538
 
@@ -23851,7 +24602,7 @@
23851
24602
  } else if (startNewIfInactive) {
23852
24603
  return this.startRecording({shouldStopBatcher: false});
23853
24604
  } else {
23854
- logger$1.log('No resumable recording found.');
24605
+ logger$2.log('No resumable recording found.');
23855
24606
  return null;
23856
24607
  }
23857
24608
  }.bind(this));
@@ -23863,8 +24614,12 @@
23863
24614
  this.startRecording({shouldStopBatcher: true});
23864
24615
  };
23865
24616
 
24617
+ MixpanelRecorder.prototype.isRecording = function () {
24618
+ return this.activeRecording && !this.activeRecording.isRrwebStopped();
24619
+ };
24620
+
23866
24621
  MixpanelRecorder.prototype.getActiveReplayId = function () {
23867
- if (this.activeRecording && !this.activeRecording.isRrwebStopped()) {
24622
+ if (this.isRecording()) {
23868
24623
  return this.activeRecording.replayId;
23869
24624
  } else {
23870
24625
  return null;
@@ -24368,53 +25123,6 @@
24368
25123
  var logicExports = requireLogic();
24369
25124
  var jsonLogic = /*@__PURE__*/getDefaultExportFromCjs(logicExports);
24370
25125
 
24371
- /**
24372
- * Shared helper to recursively lowercase strings in nested structures
24373
- * @param {*} obj - Value to process
24374
- * @param {boolean} lowercaseKeys - Whether to lowercase object keys
24375
- * @returns {*} Processed value with lowercased strings
24376
- */
24377
- var lowercaseJson = function(obj, lowercaseKeys) {
24378
- if (obj === null || obj === undefined) {
24379
- return obj;
24380
- } else if (typeof obj === 'string') {
24381
- return obj.toLowerCase();
24382
- } else if (Array.isArray(obj)) {
24383
- return obj.map(function(item) {
24384
- return lowercaseJson(item, lowercaseKeys);
24385
- });
24386
- } else if (obj === Object(obj)) {
24387
- var result = {};
24388
- for (var key in obj) {
24389
- if (obj.hasOwnProperty(key)) {
24390
- var newKey = lowercaseKeys && typeof key === 'string' ? key.toLowerCase() : key;
24391
- result[newKey] = lowercaseJson(obj[key], lowercaseKeys);
24392
- }
24393
- }
24394
- return result;
24395
- } else {
24396
- return obj;
24397
- }
24398
- };
24399
-
24400
- /**
24401
- * Lowercase all string keys and values in a nested structure
24402
- * @param {*} val - Value to process
24403
- * @returns {*} Processed value with lowercased strings
24404
- */
24405
- var lowercaseKeysAndValues = function(val) {
24406
- return lowercaseJson(val, true);
24407
- };
24408
-
24409
- /**
24410
- * Lowercase only leaf node string values in a nested structure (keys unchanged)
24411
- * @param {*} val - Value to process
24412
- * @returns {*} Processed value with lowercased leaf strings
24413
- */
24414
- var lowercaseOnlyLeafNodes = function(val) {
24415
- return lowercaseJson(val, false);
24416
- };
24417
-
24418
25126
  /**
24419
25127
  * Check if an event matches the given criteria
24420
25128
  * @param {string} eventName - The name of the event being checked
@@ -24438,13 +25146,8 @@
24438
25146
 
24439
25147
  if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
24440
25148
  try {
24441
- // Lowercase all keys and values in event properties for case-insensitive matching
24442
- var lowercasedProperties = lowercaseKeysAndValues(properties || {});
24443
-
24444
- // Lowercase only leaf nodes in JsonLogic filters (keep operators intact)
24445
- var lowercasedFilters = lowercaseOnlyLeafNodes(propertyFilters);
24446
-
24447
- filtersMatch = jsonLogic.apply(lowercasedFilters, lowercasedProperties);
25149
+ // Use properties as-is for case-sensitive matching
25150
+ filtersMatch = jsonLogic.apply(propertyFilters, properties || {});
24448
25151
  } catch (error) {
24449
25152
  return {
24450
25153
  matches: false,
@@ -24564,7 +25267,7 @@
24564
25267
  observer.observe(shadowRoot, this.observerConfig);
24565
25268
  this.shadowObservers.push(observer);
24566
25269
  } catch (e) {
24567
- logger$3.critical('Error while observing shadow root', e);
25270
+ logger$5.critical('Error while observing shadow root', e);
24568
25271
  }
24569
25272
  };
24570
25273
 
@@ -24575,7 +25278,7 @@
24575
25278
  }
24576
25279
 
24577
25280
  if (!weakSetSupported()) {
24578
- logger$3.critical('Shadow DOM observation unavailable: WeakSet not supported');
25281
+ logger$5.critical('Shadow DOM observation unavailable: WeakSet not supported');
24579
25282
  return;
24580
25283
  }
24581
25284
 
@@ -24591,7 +25294,7 @@
24591
25294
  try {
24592
25295
  this.shadowObservers[i].disconnect();
24593
25296
  } catch (e) {
24594
- logger$3.critical('Error while disconnecting shadow DOM observer', e);
25297
+ logger$5.critical('Error while disconnecting shadow DOM observer', e);
24595
25298
  }
24596
25299
  }
24597
25300
  this.shadowObservers = [];
@@ -24779,7 +25482,7 @@
24779
25482
 
24780
25483
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
24781
25484
  } catch (e) {
24782
- logger$3.critical('Error while setting up mutation observer', e);
25485
+ logger$5.critical('Error while setting up mutation observer', e);
24783
25486
  }
24784
25487
  }
24785
25488
 
@@ -24794,7 +25497,7 @@
24794
25497
  );
24795
25498
  this.shadowDOMObserver.start();
24796
25499
  } catch (e) {
24797
- logger$3.critical('Error while setting up shadow DOM observer', e);
25500
+ logger$5.critical('Error while setting up shadow DOM observer', e);
24798
25501
  this.shadowDOMObserver = null;
24799
25502
  }
24800
25503
  }
@@ -24821,7 +25524,7 @@
24821
25524
  try {
24822
25525
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
24823
25526
  } catch (e) {
24824
- logger$3.critical('Error while removing event listener', e);
25527
+ logger$5.critical('Error while removing event listener', e);
24825
25528
  }
24826
25529
  }
24827
25530
  this.eventListeners = [];
@@ -24830,7 +25533,7 @@
24830
25533
  try {
24831
25534
  this.mutationObserver.disconnect();
24832
25535
  } catch (e) {
24833
- logger$3.critical('Error while disconnecting mutation observer', e);
25536
+ logger$5.critical('Error while disconnecting mutation observer', e);
24834
25537
  }
24835
25538
  this.mutationObserver = null;
24836
25539
  }
@@ -24839,7 +25542,7 @@
24839
25542
  try {
24840
25543
  this.shadowDOMObserver.stop();
24841
25544
  } catch (e) {
24842
- logger$3.critical('Error while stopping shadow DOM observer', e);
25545
+ logger$5.critical('Error while stopping shadow DOM observer', e);
24843
25546
  }
24844
25547
  this.shadowDOMObserver = null;
24845
25548
  }
@@ -24917,7 +25620,7 @@
24917
25620
 
24918
25621
  Autocapture.prototype.init = function() {
24919
25622
  if (!minDOMApisSupported()) {
24920
- logger$3.critical('Autocapture unavailable: missing required DOM APIs');
25623
+ logger$5.critical('Autocapture unavailable: missing required DOM APIs');
24921
25624
  return;
24922
25625
  }
24923
25626
  this.initPageListeners();
@@ -24949,27 +25652,15 @@
24949
25652
  };
24950
25653
 
24951
25654
  Autocapture.prototype.currentUrlBlocked = function() {
24952
- var i;
24953
25655
  var currentUrl = _.info.currentUrl();
24954
25656
 
24955
25657
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
24956
25658
  if (allowUrlRegexes.length) {
24957
25659
  // we're using an allowlist, only track if current URL matches
24958
- var allowed = false;
24959
- for (i = 0; i < allowUrlRegexes.length; i++) {
24960
- var allowRegex = allowUrlRegexes[i];
24961
- try {
24962
- if (currentUrl.match(allowRegex)) {
24963
- allowed = true;
24964
- break;
24965
- }
24966
- } catch (err) {
24967
- logger$3.critical('Error while checking block URL regex: ' + allowRegex, err);
24968
- return true;
24969
- }
24970
- }
24971
- if (!allowed) {
24972
- // wasn't allowed by any regex
25660
+ try {
25661
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
25662
+ } catch (err) {
25663
+ logger$5.critical('Error while checking block URL regexes: ', err);
24973
25664
  return true;
24974
25665
  }
24975
25666
  }
@@ -24979,17 +25670,12 @@
24979
25670
  return false;
24980
25671
  }
24981
25672
 
24982
- for (i = 0; i < blockUrlRegexes.length; i++) {
24983
- try {
24984
- if (currentUrl.match(blockUrlRegexes[i])) {
24985
- return true;
24986
- }
24987
- } catch (err) {
24988
- logger$3.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
24989
- return true;
24990
- }
25673
+ try {
25674
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
25675
+ } catch (err) {
25676
+ logger$5.critical('Error while checking block URL regexes: ', err);
25677
+ return true;
24991
25678
  }
24992
- return false;
24993
25679
  };
24994
25680
 
24995
25681
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -25125,7 +25811,7 @@
25125
25811
  return;
25126
25812
  }
25127
25813
 
25128
- logger$3.log('Initializing scroll depth tracking');
25814
+ logger$5.log('Initializing scroll depth tracking');
25129
25815
 
25130
25816
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
25131
25817
 
@@ -25151,7 +25837,7 @@
25151
25837
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
25152
25838
  return;
25153
25839
  }
25154
- logger$3.log('Initializing click tracking');
25840
+ logger$5.log('Initializing click tracking');
25155
25841
 
25156
25842
  this.listenerClick = function(ev) {
25157
25843
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -25170,7 +25856,7 @@
25170
25856
  return;
25171
25857
  }
25172
25858
 
25173
- logger$3.log('Initializing dead click tracking');
25859
+ logger$5.log('Initializing dead click tracking');
25174
25860
  if (!this._deadClickTracker) {
25175
25861
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
25176
25862
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -25204,7 +25890,7 @@
25204
25890
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
25205
25891
  return;
25206
25892
  }
25207
- logger$3.log('Initializing input tracking');
25893
+ logger$5.log('Initializing input tracking');
25208
25894
 
25209
25895
  this.listenerChange = function(ev) {
25210
25896
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -25221,7 +25907,7 @@
25221
25907
  if (!this.pageviewTrackingConfig()) {
25222
25908
  return;
25223
25909
  }
25224
- logger$3.log('Initializing pageview tracking');
25910
+ logger$5.log('Initializing pageview tracking');
25225
25911
 
25226
25912
  var previousTrackedUrl = '';
25227
25913
  var tracked = false;
@@ -25256,7 +25942,7 @@
25256
25942
  }
25257
25943
  if (didPathChange) {
25258
25944
  this.lastScrollCheckpoint = 0;
25259
- logger$3.log('Path change: re-initializing scroll depth checkpoints');
25945
+ logger$5.log('Path change: re-initializing scroll depth checkpoints');
25260
25946
  }
25261
25947
  }
25262
25948
  }.bind(this));
@@ -25271,7 +25957,7 @@
25271
25957
  return;
25272
25958
  }
25273
25959
 
25274
- logger$3.log('Initializing rage click tracking');
25960
+ logger$5.log('Initializing rage click tracking');
25275
25961
  if (!this._rageClickTracker) {
25276
25962
  this._rageClickTracker = new RageClickTracker();
25277
25963
  }
@@ -25301,7 +25987,7 @@
25301
25987
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
25302
25988
  return;
25303
25989
  }
25304
- logger$3.log('Initializing scroll tracking');
25990
+ logger$5.log('Initializing scroll tracking');
25305
25991
  this.lastScrollCheckpoint = 0;
25306
25992
 
25307
25993
  var scrollTrackFunction = function() {
@@ -25338,7 +26024,7 @@
25338
26024
  }
25339
26025
  }
25340
26026
  } catch (err) {
25341
- logger$3.critical('Error while calculating scroll percentage', err);
26027
+ logger$5.critical('Error while calculating scroll percentage', err);
25342
26028
  }
25343
26029
  if (shouldTrack) {
25344
26030
  this.mp.track(MP_EV_SCROLL, props);
@@ -25356,7 +26042,7 @@
25356
26042
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
25357
26043
  return;
25358
26044
  }
25359
- logger$3.log('Initializing submit tracking');
26045
+ logger$5.log('Initializing submit tracking');
25360
26046
 
25361
26047
  this.listenerSubmit = function(ev) {
25362
26048
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -25378,7 +26064,7 @@
25378
26064
  return;
25379
26065
  }
25380
26066
 
25381
- logger$3.log('Initializing page visibility tracking.');
26067
+ logger$5.log('Initializing page visibility tracking.');
25382
26068
  this._initScrollDepthTracking();
25383
26069
  var previousTrackedUrl = _.info.currentUrl();
25384
26070
 
@@ -25463,7 +26149,7 @@
25463
26149
  return win[TARGETING_GLOBAL_NAME];
25464
26150
  };
25465
26151
 
25466
- var logger = console_with_prefix('flags');
26152
+ var logger$1 = console_with_prefix('flags');
25467
26153
  var FLAGS_CONFIG_KEY = 'flags';
25468
26154
 
25469
26155
  var CONFIG_CONTEXT = 'context';
@@ -25506,7 +26192,7 @@
25506
26192
 
25507
26193
  FeatureFlagManager.prototype.init = function() {
25508
26194
  if (!this.minApisSupported()) {
25509
- logger.critical('Feature Flags unavailable: missing minimum required APIs');
26195
+ logger$1.critical('Feature Flags unavailable: missing minimum required APIs');
25510
26196
  return;
25511
26197
  }
25512
26198
 
@@ -25541,7 +26227,7 @@
25541
26227
 
25542
26228
  FeatureFlagManager.prototype.updateContext = function(newContext, options) {
25543
26229
  if (!this.isSystemEnabled()) {
25544
- logger.critical('Feature Flags not enabled, cannot update context');
26230
+ logger$1.critical('Feature Flags not enabled, cannot update context');
25545
26231
  return Promise.resolve();
25546
26232
  }
25547
26233
 
@@ -25558,7 +26244,7 @@
25558
26244
 
25559
26245
  FeatureFlagManager.prototype.areFlagsReady = function() {
25560
26246
  if (!this.isSystemEnabled()) {
25561
- logger.error('Feature Flags not enabled');
26247
+ logger$1.error('Feature Flags not enabled');
25562
26248
  }
25563
26249
  return !!this.flags;
25564
26250
  };
@@ -25571,7 +26257,7 @@
25571
26257
  var distinctId = this.getMpProperty('distinct_id');
25572
26258
  var deviceId = this.getMpProperty('$device_id');
25573
26259
  var traceparent = generateTraceparent();
25574
- logger.log('Fetching flags for distinct ID: ' + distinctId);
26260
+ logger$1.log('Fetching flags for distinct ID: ' + distinctId);
25575
26261
 
25576
26262
  var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
25577
26263
  var searchParams = new URLSearchParams();
@@ -25670,11 +26356,11 @@
25670
26356
  this._loadTargetingIfNeeded();
25671
26357
  }.bind(this)).catch(function(error) {
25672
26358
  this.markFetchComplete();
25673
- logger.error(error);
26359
+ logger$1.error(error);
25674
26360
  }.bind(this));
25675
26361
  }.bind(this)).catch(function(error) {
25676
26362
  this.markFetchComplete();
25677
- logger.error(error);
26363
+ logger$1.error(error);
25678
26364
  }.bind(this));
25679
26365
 
25680
26366
  return this.fetchPromise;
@@ -25682,7 +26368,7 @@
25682
26368
 
25683
26369
  FeatureFlagManager.prototype.markFetchComplete = function() {
25684
26370
  if (!this._fetchInProgressStartTime) {
25685
- logger.error('Fetch in progress started time not set, cannot mark fetch complete');
26371
+ logger$1.error('Fetch in progress started time not set, cannot mark fetch complete');
25686
26372
  return;
25687
26373
  }
25688
26374
  this._fetchStartTime = this._fetchInProgressStartTime;
@@ -25704,7 +26390,7 @@
25704
26390
 
25705
26391
  if (hasPropertyFilters) {
25706
26392
  this.getTargeting().then(function() {
25707
- logger.log('targeting loaded for property filter evaluation');
26393
+ logger$1.log('targeting loaded for property filter evaluation');
25708
26394
  });
25709
26395
  }
25710
26396
  };
@@ -25719,7 +26405,7 @@
25719
26405
  this.loadExtraBundle.bind(this),
25720
26406
  this.targetingSrc
25721
26407
  ).catch(function(error) {
25722
- logger.error('Failed to load targeting: ' + error);
26408
+ logger$1.error('Failed to load targeting: ' + error);
25723
26409
  }.bind(this));
25724
26410
  };
25725
26411
 
@@ -25773,7 +26459,7 @@
25773
26459
 
25774
26460
  // If no targeting library and event has property filters, skip it
25775
26461
  if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
25776
- logger.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
26462
+ logger$1.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
25777
26463
  return;
25778
26464
  }
25779
26465
 
@@ -25796,7 +26482,7 @@
25796
26482
  }
25797
26483
 
25798
26484
  if (matchResult.error) {
25799
- logger.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
26485
+ logger$1.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
25800
26486
  return;
25801
26487
  }
25802
26488
 
@@ -25804,7 +26490,7 @@
25804
26490
  return;
25805
26491
  }
25806
26492
 
25807
- logger.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
26493
+ logger$1.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
25808
26494
 
25809
26495
  var newVariant = {
25810
26496
  'key': pendingEvent['pending_variant']['variant_key'],
@@ -25845,7 +26531,7 @@
25845
26531
  'first_time_event_hash': firstTimeEventHash
25846
26532
  };
25847
26533
 
25848
- logger.log('Recording first-time event for flag: ' + flagId);
26534
+ logger$1.log('Recording first-time event for flag: ' + flagId);
25849
26535
 
25850
26536
  // Fire-and-forget POST request
25851
26537
  this.fetch.call(win, url, {
@@ -25858,130 +26544,446 @@
25858
26544
  'body': JSON.stringify(payload)
25859
26545
  }).catch(function(error) {
25860
26546
  // Silent failure - cohort sync will catch up
25861
- logger.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26547
+ logger$1.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
26548
+ });
26549
+ };
26550
+
26551
+ FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
26552
+ if (!this.fetchPromise) {
26553
+ return new Promise(function(resolve) {
26554
+ logger$1.critical('Feature Flags not initialized');
26555
+ resolve(fallback);
26556
+ });
26557
+ }
26558
+
26559
+ return this.fetchPromise.then(function() {
26560
+ return this.getVariantSync(featureName, fallback);
26561
+ }.bind(this)).catch(function(error) {
26562
+ logger$1.error(error);
26563
+ return fallback;
26564
+ });
26565
+ };
26566
+
26567
+ FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
26568
+ if (!this.areFlagsReady()) {
26569
+ logger$1.log('Flags not loaded yet');
26570
+ return fallback;
26571
+ }
26572
+ var feature = this.flags.get(featureName);
26573
+ if (!feature) {
26574
+ logger$1.log('No flag found: "' + featureName + '"');
26575
+ return fallback;
26576
+ }
26577
+ this.trackFeatureCheck(featureName, feature);
26578
+ return feature;
26579
+ };
26580
+
26581
+ FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
26582
+ return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
26583
+ return feature['value'];
26584
+ }).catch(function(error) {
26585
+ logger$1.error(error);
26586
+ return fallbackValue;
26587
+ });
26588
+ };
26589
+
26590
+ // TODO remove deprecated method
26591
+ FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
26592
+ 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.');
26593
+ return this.getVariantValue(featureName, fallbackValue);
26594
+ };
26595
+
26596
+ FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
26597
+ return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
26598
+ };
26599
+
26600
+ FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
26601
+ return this.getVariantValue(featureName).then(function() {
26602
+ return this.isEnabledSync(featureName, fallbackValue);
26603
+ }.bind(this)).catch(function(error) {
26604
+ logger$1.error(error);
26605
+ return fallbackValue;
25862
26606
  });
25863
26607
  };
25864
26608
 
25865
- FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
25866
- if (!this.fetchPromise) {
25867
- return new Promise(function(resolve) {
25868
- logger.critical('Feature Flags not initialized');
25869
- resolve(fallback);
25870
- });
26609
+ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
26610
+ fallbackValue = fallbackValue || false;
26611
+ var val = this.getVariantValueSync(featureName, fallbackValue);
26612
+ if (val !== true && val !== false) {
26613
+ logger$1.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
26614
+ val = fallbackValue;
26615
+ }
26616
+ return val;
26617
+ };
26618
+
26619
+ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
26620
+ if (this.trackedFeatures.has(featureName)) {
26621
+ return;
26622
+ }
26623
+ this.trackedFeatures.add(featureName);
26624
+
26625
+ var trackingProperties = {
26626
+ 'Experiment name': featureName,
26627
+ 'Variant name': feature['key'],
26628
+ '$experiment_type': 'feature_flag',
26629
+ 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
26630
+ 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
26631
+ 'Variant fetch latency (ms)': this._fetchLatency,
26632
+ 'Variant fetch traceparent': this._traceparent,
26633
+ };
26634
+
26635
+ if (feature['experiment_id'] !== 'undefined') {
26636
+ trackingProperties['$experiment_id'] = feature['experiment_id'];
26637
+ }
26638
+ if (feature['is_experiment_active'] !== 'undefined') {
26639
+ trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
26640
+ }
26641
+ if (feature['is_qa_tester'] !== 'undefined') {
26642
+ trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
26643
+ }
26644
+
26645
+ this.track('$experiment_started', trackingProperties);
26646
+ };
26647
+
26648
+ FeatureFlagManager.prototype.minApisSupported = function() {
26649
+ return !!this.fetch &&
26650
+ typeof Promise !== 'undefined' &&
26651
+ typeof Map !== 'undefined' &&
26652
+ typeof Set !== 'undefined';
26653
+ };
26654
+
26655
+ safewrapClass(FeatureFlagManager);
26656
+
26657
+ FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
26658
+ FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
26659
+ FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
26660
+ FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
26661
+ FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
26662
+ FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
26663
+ FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
26664
+ FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
26665
+
26666
+ // Deprecated method
26667
+ FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
26668
+
26669
+ // Exports intended only for testing
26670
+ FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26671
+
26672
+ /* eslint camelcase: "off" */
26673
+
26674
+
26675
+ var logger = console_with_prefix('recorder');
26676
+
26677
+ var IFRAME_HANDSHAKE_REQUEST = 'mp_iframe_handshake_request';
26678
+ var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
26679
+
26680
+
26681
+ /**
26682
+ * RecorderManager: manages session recording initialization, lifecycle and state
26683
+ * @constructor
26684
+ */
26685
+ var RecorderManager = function(initOptions) {
26686
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
26687
+ // but ideally we should be able to remove this dependency.
26688
+ this.mixpanelInstance = initOptions.mixpanelInstance;
26689
+
26690
+ this.getMpConfig = initOptions.getConfigFunc;
26691
+ this.getTabId = initOptions.getTabIdFunc;
26692
+ this.reportError = initOptions.reportErrorFunc;
26693
+ this.getDistinctId = initOptions.getDistinctIdFunc;
26694
+ this.loadExtraBundle = initOptions.loadExtraBundle;
26695
+ this.recorderSrc = initOptions.recorderSrc;
26696
+ this.targetingSrc = initOptions.targetingSrc;
26697
+ this.libBasePath = initOptions.libBasePath;
26698
+
26699
+ this._recorder = null;
26700
+ this._parentReplayId = null;
26701
+ this._parentFrameRetryInterval = null;
26702
+ };
26703
+
26704
+ RecorderManager.prototype.shouldLoadRecorder = function() {
26705
+ if (this.getMpConfig('disable_persistence')) {
26706
+ console$1.log('Load recorder check skipped due to disable_persistence config');
26707
+ return PromisePolyfill.resolve(false);
26708
+ }
26709
+
26710
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
26711
+ var tab_id = this.getTabId();
26712
+ return recording_registry_idb.init()
26713
+ .then(function () {
26714
+ return recording_registry_idb.getAll();
26715
+ })
26716
+ .then(function (recordings) {
26717
+ for (var i = 0; i < recordings.length; i++) {
26718
+ // if there are expired recordings in the registry, we should load the recorder to flush them
26719
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
26720
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
26721
+ return true;
26722
+ }
26723
+ }
26724
+ return false;
26725
+ })
26726
+ .catch(_.bind(function (err) {
26727
+ this.reportError('Error checking recording registry', err);
26728
+ return false;
26729
+ }, this));
26730
+ };
26731
+
26732
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
26733
+ if (!win['MutationObserver']) {
26734
+ console$1.critical('Browser does not support MutationObserver; skipping session recording');
26735
+ return PromisePolyfill.resolve();
26736
+ }
26737
+
26738
+ var loadRecorder = _.bind(function(startNewIfInactive) {
26739
+ return new PromisePolyfill(_.bind(function(resolve) {
26740
+ var handleLoadedRecorder = safewrap(_.bind(function() {
26741
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
26742
+ this._recorder['resumeRecording'](startNewIfInactive);
26743
+ resolve();
26744
+ }, this));
26745
+
26746
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
26747
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
26748
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
26749
+ } else {
26750
+ handleLoadedRecorder();
26751
+ }
26752
+ }, this));
26753
+ }, this);
26754
+
26755
+ // Cross-origin iframe handling
26756
+ var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger);
26757
+ var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
26758
+
26759
+ if (isCrossOriginRecordingEnabled) {
26760
+ // listen for handshake requests from their own child iframes (including nested)
26761
+ this._setupParentFrameListener(allowedOrigins);
26762
+
26763
+ if (win.parent !== win) {
26764
+ // also wait for parent's replay ID
26765
+ this._setupChildFrameListener(allowedOrigins, loadRecorder);
26766
+ this._sendParentFrameRequestWithRetry(allowedOrigins);
26767
+ return PromisePolyfill.resolve();
26768
+ }
26769
+ }
26770
+
26771
+ /**
26772
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
26773
+ * 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.
26774
+ */
26775
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
26776
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
26777
+ if (force_start || is_sampled) {
26778
+ return loadRecorder(true);
26779
+ } else {
26780
+ return this.shouldLoadRecorder()
26781
+ .then(_.bind(function (shouldLoad) {
26782
+ if (shouldLoad) {
26783
+ return loadRecorder(false);
26784
+ }
26785
+ return PromisePolyfill.resolve();
26786
+ }, this));
26787
+ }
26788
+ };
26789
+
26790
+ RecorderManager.prototype.isRecording = function() {
26791
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
26792
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
26793
+ return false;
26794
+ }
26795
+ try {
26796
+ return this._recorder['isRecording']();
26797
+ } catch (e) {
26798
+ this.reportError('Error checking if recording is active', e);
26799
+ return false;
25871
26800
  }
26801
+ };
25872
26802
 
25873
- return this.fetchPromise.then(function() {
25874
- return this.getVariantSync(featureName, fallback);
25875
- }.bind(this)).catch(function(error) {
25876
- logger.error(error);
25877
- return fallback;
25878
- });
26803
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
26804
+ var isRecording = this.isRecording();
26805
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
26806
+
26807
+ if (!isRecording && recordingTriggerEvents) {
26808
+ var trigger = recordingTriggerEvents[event_name];
26809
+ if (trigger && typeof trigger['percentage'] === 'number') {
26810
+ var newRate = trigger['percentage'];
26811
+ var propertyFilters = trigger['property_filters'];
26812
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
26813
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
26814
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
26815
+ .then(function(targeting) {
26816
+ try {
26817
+ var result = targeting['eventMatchesCriteria'](
26818
+ event_name,
26819
+ properties,
26820
+ {
26821
+ 'event_name': event_name,
26822
+ 'property_filters': propertyFilters
26823
+ }
26824
+ );
26825
+ if (result['matches']) {
26826
+ this.checkAndStartSessionRecording(false, newRate);
26827
+ }
26828
+ } catch (err) {
26829
+ console$1.critical('Could not parse recording event trigger properties logic:', err);
26830
+ }
26831
+ }.bind(this)).catch(function(err) {
26832
+ console$1.critical('Failed to load targeting library:', err);
26833
+ });
26834
+ } else {
26835
+ this.checkAndStartSessionRecording(false, newRate);
26836
+ }
26837
+ }
26838
+ }
25879
26839
  };
25880
26840
 
25881
- FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
25882
- if (!this.areFlagsReady()) {
25883
- logger.log('Flags not loaded yet');
25884
- return fallback;
26841
+ RecorderManager.prototype.stopSessionRecording = function() {
26842
+ if (this._recorder) {
26843
+ return this._recorder['stopRecording']();
25885
26844
  }
25886
- var feature = this.flags.get(featureName);
25887
- if (!feature) {
25888
- logger.log('No flag found: "' + featureName + '"');
25889
- return fallback;
26845
+ return PromisePolyfill.resolve();
26846
+ };
26847
+
26848
+ RecorderManager.prototype.pauseSessionRecording = function() {
26849
+ if (this._recorder) {
26850
+ return this._recorder['pauseRecording']();
25890
26851
  }
25891
- this.trackFeatureCheck(featureName, feature);
25892
- return feature;
26852
+ return PromisePolyfill.resolve();
25893
26853
  };
25894
26854
 
25895
- FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
25896
- return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
25897
- return feature['value'];
25898
- }).catch(function(error) {
25899
- logger.error(error);
25900
- return fallbackValue;
25901
- });
26855
+ RecorderManager.prototype.resumeSessionRecording = function() {
26856
+ if (this._recorder) {
26857
+ return this._recorder['resumeRecording']();
26858
+ }
26859
+ return PromisePolyfill.resolve();
25902
26860
  };
25903
26861
 
25904
- // TODO remove deprecated method
25905
- FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
25906
- logger.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
25907
- return this.getVariantValue(featureName, fallbackValue);
26862
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
26863
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
25908
26864
  };
25909
26865
 
25910
- FeatureFlagManager.prototype.getVariantValueSync = function(featureName, fallbackValue) {
25911
- return this.getVariantSync(featureName, {'value': fallbackValue})['value'];
26866
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
26867
+ var props = {};
26868
+ var replay_id = this.getSessionReplayId();
26869
+ if (replay_id) {
26870
+ props['$mp_replay_id'] = replay_id;
26871
+ }
26872
+ return props;
25912
26873
  };
25913
26874
 
25914
- FeatureFlagManager.prototype.isEnabled = function(featureName, fallbackValue) {
25915
- return this.getVariantValue(featureName).then(function() {
25916
- return this.isEnabledSync(featureName, fallbackValue);
25917
- }.bind(this)).catch(function(error) {
25918
- logger.error(error);
25919
- return fallbackValue;
25920
- });
26875
+ RecorderManager.prototype.getSessionReplayUrl = function() {
26876
+ var replay_url = null;
26877
+ var replay_id = this.getSessionReplayId();
26878
+ if (replay_id) {
26879
+ var query_params = _.HTTPBuildQuery({
26880
+ 'replay_id': replay_id,
26881
+ 'distinct_id': this.getDistinctId(),
26882
+ 'token': this.getMpConfig('token')
26883
+ });
26884
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
26885
+ }
26886
+ return replay_url;
25921
26887
  };
25922
26888
 
25923
- FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue) {
25924
- fallbackValue = fallbackValue || false;
25925
- var val = this.getVariantValueSync(featureName, fallbackValue);
25926
- if (val !== true && val !== false) {
25927
- logger.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
25928
- val = fallbackValue;
26889
+ RecorderManager.prototype.getSessionReplayId = function() {
26890
+ // Child iframe uses parent's replay ID
26891
+ if (this._parentReplayId) {
26892
+ return this._parentReplayId;
25929
26893
  }
25930
- return val;
26894
+ var replay_id = null;
26895
+ if (this._recorder) {
26896
+ replay_id = this._recorder['replayId'];
26897
+ }
26898
+ return replay_id || null;
25931
26899
  };
25932
26900
 
25933
- FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
25934
- if (this.trackedFeatures.has(featureName)) {
26901
+ // "private" public method to reach into the recorder in test cases
26902
+ RecorderManager.prototype.getRecorder = function() {
26903
+ return this._recorder;
26904
+ };
26905
+
26906
+ RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
26907
+ if (this._childFrameMessageHandler) {
25935
26908
  return;
25936
26909
  }
25937
- this.trackedFeatures.add(featureName);
25938
-
25939
- var trackingProperties = {
25940
- 'Experiment name': featureName,
25941
- 'Variant name': feature['key'],
25942
- '$experiment_type': 'feature_flag',
25943
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
25944
- 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
25945
- 'Variant fetch latency (ms)': this._fetchLatency,
25946
- 'Variant fetch traceparent': this._traceparent,
26910
+ var self = this;
26911
+ this._childFrameMessageHandler = function(event) {
26912
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
26913
+ var data = event.data;
26914
+ if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
26915
+ self._parentReplayId = data['replayId'];
26916
+ if (data['distinctId']) {
26917
+ self.mixpanelInstance['identify'](data['distinctId']);
26918
+ }
26919
+ self._parentFrameRetryActive = false;
26920
+ win.removeEventListener('message', self._childFrameMessageHandler);
26921
+ self._childFrameMessageHandler = null;
26922
+ loadRecorder(true);
26923
+ }
25947
26924
  };
26925
+ win.addEventListener('message', this._childFrameMessageHandler);
26926
+ };
25948
26927
 
25949
- if (feature['experiment_id'] !== 'undefined') {
25950
- trackingProperties['$experiment_id'] = feature['experiment_id'];
25951
- }
25952
- if (feature['is_experiment_active'] !== 'undefined') {
25953
- trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
25954
- }
25955
- if (feature['is_qa_tester'] !== 'undefined') {
25956
- trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
26928
+ RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
26929
+ var message = {};
26930
+ message['type'] = IFRAME_HANDSHAKE_REQUEST;
26931
+ message['token'] = this.getMpConfig('token');
26932
+ for (var i = 0; i < allowedOrigins.length; i++) {
26933
+ try {
26934
+ win.parent.postMessage(message, allowedOrigins[i]);
26935
+ } catch (e) {
26936
+ // origin mismatch - ignore
26937
+ }
25957
26938
  }
25958
-
25959
- this.track('$experiment_started', trackingProperties);
25960
26939
  };
25961
26940
 
25962
- FeatureFlagManager.prototype.minApisSupported = function() {
25963
- return !!this.fetch &&
25964
- typeof Promise !== 'undefined' &&
25965
- typeof Map !== 'undefined' &&
25966
- typeof Set !== 'undefined';
25967
- };
26941
+ RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
26942
+ var self = this;
26943
+ var maxRetries = 10;
26944
+ var retryCount = 0;
26945
+ var delay = 50;
26946
+ this._parentFrameRetryActive = true;
25968
26947
 
25969
- safewrapClass(FeatureFlagManager);
26948
+ this._sendParentFrameRequest(allowedOrigins);
25970
26949
 
25971
- FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
25972
- FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
25973
- FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
25974
- FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
25975
- FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
25976
- FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
25977
- FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
25978
- FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
26950
+ function scheduleRetry() {
26951
+ setTimeout(function() {
26952
+ if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
26953
+ return;
26954
+ }
26955
+ self._sendParentFrameRequest(allowedOrigins);
26956
+ delay *= 2;
26957
+ scheduleRetry();
26958
+ }, delay);
26959
+ }
26960
+ scheduleRetry();
26961
+ };
25979
26962
 
25980
- // Deprecated method
25981
- FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
26963
+ RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
26964
+ if (this._parentFrameMessageHandler) {
26965
+ return;
26966
+ }
26967
+ var self = this;
26968
+ this._parentFrameMessageHandler = function(event) {
26969
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
26970
+ var data = event.data;
26971
+ if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
26972
+ var replayId = self.getSessionReplayId();
26973
+ if (replayId) {
26974
+ var response = {};
26975
+ response['type'] = IFRAME_HANDSHAKE_RESPONSE;
26976
+ response['token'] = self.getMpConfig('token');
26977
+ response['replayId'] = replayId;
26978
+ response['distinctId'] = self.getDistinctId();
26979
+ event.source.postMessage(response, event.origin);
26980
+ }
26981
+ }
26982
+ };
26983
+ win.addEventListener('message', this._parentFrameMessageHandler);
26984
+ };
25982
26985
 
25983
- // Exports intended only for testing
25984
- FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
26986
+ safewrapClass(RecorderManager);
25985
26987
 
25986
26988
  /* eslint camelcase: "off" */
25987
26989
 
@@ -27355,7 +28357,6 @@
27355
28357
  /** @const */ var SETTING_FALLBACK = 'fallback';
27356
28358
  /** @const */ var SETTING_DISABLED = 'disabled';
27357
28359
 
27358
-
27359
28360
  /*
27360
28361
  * Dynamic... constants? Is that an oxymoron?
27361
28362
  */
@@ -27440,19 +28441,24 @@
27440
28441
  'batch_request_timeout_ms': 90000,
27441
28442
  'batch_autostart': true,
27442
28443
  'hooks': {},
28444
+ 'record_allowed_iframe_origins': [],
27443
28445
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
27444
28446
  'record_block_selector': 'img, video, audio',
27445
28447
  'record_canvas': false,
27446
28448
  'record_collect_fonts': false,
27447
28449
  'record_console': true,
27448
28450
  'record_heatmap_data': false,
28451
+ 'recording_event_triggers': {},
27449
28452
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
27450
28453
  'record_mask_inputs': true,
27451
28454
  'record_max_ms': MAX_RECORDING_MS,
27452
28455
  'record_min_ms': 0,
28456
+ 'record_network': false,
28457
+ 'record_network_options': {},
27453
28458
  'record_sessions_percent': 0,
27454
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
27455
- 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
28459
+ 'recorder_src': null,
28460
+ 'targeting_src': null,
28461
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
27456
28462
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
27457
28463
  };
27458
28464
 
@@ -27606,6 +28612,19 @@
27606
28612
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
27607
28613
  }));
27608
28614
 
28615
+ this.recorderManager = new RecorderManager({
28616
+ mixpanelInstance: this,
28617
+ getConfigFunc: _.bind(this.get_config, this),
28618
+ setConfigFunc: _.bind(this.set_config, this),
28619
+ getTabIdFunc: _.bind(this.get_tab_id, this),
28620
+ reportErrorFunc: _.bind(this.report_error, this),
28621
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
28622
+ recorderSrc: this.get_config('recorder_src'),
28623
+ targetingSrc: this.get_config('targeting_src'),
28624
+ libBasePath: this.get_config('lib_base_path'),
28625
+ loadExtraBundle: load_extra_bundle
28626
+ });
28627
+
27609
28628
  this['_jsc'] = NOOP_FUNC;
27610
28629
 
27611
28630
  this.__dom_loaded_queue = [];
@@ -27684,7 +28703,7 @@
27684
28703
  getPropertyFunc: _.bind(this.get_property, this),
27685
28704
  trackingFunc: _.bind(this.track, this),
27686
28705
  loadExtraBundle: load_extra_bundle,
27687
- targetingSrc: this.get_config('targeting_src')
28706
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
27688
28707
  });
27689
28708
  this.flags.init();
27690
28709
  this['flags'] = this.flags;
@@ -27697,11 +28716,11 @@
27697
28716
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
27698
28717
  var mode = this.get_config('remote_settings_mode');
27699
28718
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
27700
- this._fetch_remote_settings(mode).then(_.bind(function() {
27701
- this._check_and_start_session_recording();
28719
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
28720
+ return this._check_and_start_session_recording();
27702
28721
  }, this));
27703
28722
  } else {
27704
- this._check_and_start_session_recording();
28723
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
27705
28724
  }
27706
28725
  };
27707
28726
 
@@ -27745,132 +28764,50 @@
27745
28764
  return this.tab_id || null;
27746
28765
  };
27747
28766
 
27748
- MixpanelLib.prototype._should_load_recorder = function () {
27749
- if (this.get_config('disable_persistence')) {
27750
- console$1.log('Load recorder check skipped due to disable_persistence config');
27751
- return Promise.resolve(false);
27752
- }
27753
-
27754
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
27755
- var tab_id = this.get_tab_id();
27756
- return recording_registry_idb.init()
27757
- .then(function () {
27758
- return recording_registry_idb.getAll();
27759
- })
27760
- .then(function (recordings) {
27761
- for (var i = 0; i < recordings.length; i++) {
27762
- // if there are expired recordings in the registry, we should load the recorder to flush them
27763
- // if there's a recording for this tab id, we should load the recorder to continue the recording
27764
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
27765
- return true;
27766
- }
27767
- }
27768
- return false;
27769
- })
27770
- .catch(_.bind(function (err) {
27771
- this.report_error('Error checking recording registry', err);
27772
- }, this));
27773
- };
27774
-
27775
28767
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
27776
- if (!win['MutationObserver']) {
27777
- console$1.critical('Browser does not support MutationObserver; skipping session recording');
27778
- return;
27779
- }
27780
-
27781
- var loadRecorder = _.bind(function(startNewIfInactive) {
27782
- var handleLoadedRecorder = _.bind(function() {
27783
- this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
27784
- this._recorder['resumeRecording'](startNewIfInactive);
27785
- }, this);
27786
-
27787
- if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
27788
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
27789
- } else {
27790
- handleLoadedRecorder();
27791
- }
27792
- }, this);
27793
-
27794
- /**
27795
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
27796
- * 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.
27797
- */
27798
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
27799
- if (force_start || is_sampled) {
27800
- loadRecorder(true);
27801
- } else {
27802
- this._should_load_recorder()
27803
- .then(function (shouldLoad) {
27804
- if (shouldLoad) {
27805
- loadRecorder(false);
27806
- }
27807
- });
27808
- }
28768
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
27809
28769
  });
27810
28770
 
28771
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
28772
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
28773
+ };
28774
+
27811
28775
  MixpanelLib.prototype.start_session_recording = function () {
27812
- this._check_and_start_session_recording(true);
28776
+ return this._check_and_start_session_recording(true);
27813
28777
  };
27814
28778
 
27815
28779
  MixpanelLib.prototype.stop_session_recording = function () {
27816
- if (this._recorder) {
27817
- return this._recorder['stopRecording']();
27818
- }
27819
- return Promise.resolve();
28780
+ return this.recorderManager.stopSessionRecording();
27820
28781
  };
27821
28782
 
27822
28783
  MixpanelLib.prototype.pause_session_recording = function () {
27823
- if (this._recorder) {
27824
- return this._recorder['pauseRecording']();
27825
- }
27826
- return Promise.resolve();
28784
+ return this.recorderManager.pauseSessionRecording();
27827
28785
  };
27828
28786
 
27829
28787
  MixpanelLib.prototype.resume_session_recording = function () {
27830
- if (this._recorder) {
27831
- return this._recorder['resumeRecording']();
27832
- }
27833
- return Promise.resolve();
28788
+ return this.recorderManager.resumeSessionRecording();
27834
28789
  };
27835
28790
 
27836
28791
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
27837
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
28792
+ return this.recorderManager.isRecordingHeatmapData();
27838
28793
  };
27839
28794
 
27840
28795
  MixpanelLib.prototype.get_session_recording_properties = function () {
27841
- var props = {};
27842
- var replay_id = this._get_session_replay_id();
27843
- if (replay_id) {
27844
- props['$mp_replay_id'] = replay_id;
27845
- }
27846
- return props;
28796
+ return this.recorderManager.getSessionRecordingProperties();
27847
28797
  };
27848
28798
 
27849
28799
  MixpanelLib.prototype.get_session_replay_url = function () {
27850
- var replay_url = null;
27851
- var replay_id = this._get_session_replay_id();
27852
- if (replay_id) {
27853
- var query_params = _.HTTPBuildQuery({
27854
- 'replay_id': replay_id,
27855
- 'distinct_id': this.get_distinct_id(),
27856
- 'token': this.get_config('token')
27857
- });
27858
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
27859
- }
27860
- return replay_url;
27861
- };
27862
-
27863
- MixpanelLib.prototype._get_session_replay_id = function () {
27864
- var replay_id = null;
27865
- if (this._recorder) {
27866
- replay_id = this._recorder['replayId'];
27867
- }
27868
- return replay_id || null;
28800
+ return this.recorderManager.getSessionReplayUrl();
27869
28801
  };
27870
28802
 
27871
28803
  // "private" public method to reach into the recorder in test cases
27872
28804
  MixpanelLib.prototype.__get_recorder = function () {
27873
- return this._recorder;
28805
+ return this.recorderManager.getRecorder();
28806
+ };
28807
+
28808
+ // "private" public method to get session recording init promise in test cases
28809
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
28810
+ return this.__session_recording_init_promise;
27874
28811
  };
27875
28812
 
27876
28813
  // Private methods
@@ -28128,6 +29065,7 @@
28128
29065
  };
28129
29066
 
28130
29067
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
29068
+ var self = this;
28131
29069
  var disableRecordingIfStrict = function() {
28132
29070
  if (mode === 'strict') {
28133
29071
  self.set_config({'record_sessions_percent': 0});
@@ -28148,7 +29086,6 @@
28148
29086
  };
28149
29087
  var query_string = _.HTTPBuildQuery(request_params);
28150
29088
  var full_url = settings_endpoint + '?' + query_string;
28151
- var self = this;
28152
29089
 
28153
29090
  var abortController = new AbortController();
28154
29091
  var timeout_id = setTimeout(function() {
@@ -28340,6 +29277,34 @@
28340
29277
  this._execute_array([item]);
28341
29278
  };
28342
29279
 
29280
+ /**
29281
+ * Enables events on the Mixpanel object. If passed no arguments,
29282
+ * this function enable tracking of all events. If passed an
29283
+ * array of event names, those events will be enabled, but other
29284
+ * existing disabled events will continue to be not tracked.
29285
+ *
29286
+ * @param {Array} [events] An array of event names to enable
29287
+ */
29288
+ MixpanelLib.prototype.enable = function(events) {
29289
+ var keys, new_disabled_events, i, j;
29290
+
29291
+ if (typeof(events) === 'undefined') {
29292
+ this._flags.disable_all_events = false;
29293
+ } else {
29294
+ keys = {};
29295
+ new_disabled_events = [];
29296
+ for (i = 0; i < events.length; i++) {
29297
+ keys[events[i]] = true;
29298
+ }
29299
+ for (j = 0; j < this.__disabled_events.length; j++) {
29300
+ if (!keys[this.__disabled_events[j]]) {
29301
+ new_disabled_events.push(this.__disabled_events[j]);
29302
+ }
29303
+ }
29304
+ this.__disabled_events = new_disabled_events;
29305
+ }
29306
+ };
29307
+
28343
29308
  /**
28344
29309
  * Disable events on the Mixpanel object. If passed no arguments,
28345
29310
  * this function disables tracking of any event. If passed an
@@ -28513,6 +29478,8 @@
28513
29478
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
28514
29479
  }
28515
29480
 
29481
+ this._start_recording_on_event(event_name, properties);
29482
+
28516
29483
  var data = {
28517
29484
  'event': event_name,
28518
29485
  'properties': properties
@@ -29721,6 +30688,7 @@
29721
30688
  // MixpanelLib Exports
29722
30689
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
29723
30690
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
30691
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
29724
30692
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
29725
30693
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
29726
30694
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -29764,6 +30732,7 @@
29764
30732
 
29765
30733
  // Exports intended only for testing
29766
30734
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
30735
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
29767
30736
 
29768
30737
  // MixpanelPersistence Exports
29769
30738
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;