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
@@ -3,9 +3,17 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.75.0'
6
+ LIB_VERSION: '2.77.0'
7
7
  };
8
8
 
9
+ // Window global names for async modules
10
+ var TARGETING_GLOBAL_NAME = '__mp_targeting';
11
+ var RECORDER_GLOBAL_NAME = '__mp_recorder';
12
+
13
+ // Constants that are injected at build-time for the names of async modules.
14
+ var RECORDER_FILENAME = 'mixpanel-recorder-DLKbUIEE.js';
15
+ var TARGETING_FILENAME = 'mixpanel-targeting-CmVvUyFM.js';
16
+
9
17
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
10
18
  var win;
11
19
  if (typeof(window) === 'undefined') {
@@ -2122,6 +2130,17 @@
2122
2130
 
2123
2131
  var NOOP_FUNC = function () {};
2124
2132
 
2133
+ var urlMatchesRegexList = function (url, regexList) {
2134
+ var matches = false;
2135
+ for (var i = 0; i < regexList.length; i++) {
2136
+ if (url.match(regexList[i])) {
2137
+ matches = true;
2138
+ break;
2139
+ }
2140
+ }
2141
+ return matches;
2142
+ };
2143
+
2125
2144
  var JSONStringify = null, JSONParse = null;
2126
2145
  if (typeof JSON !== 'undefined') {
2127
2146
  JSONStringify = JSON.stringify;
@@ -2144,25 +2163,6 @@
2144
2163
  _['toArray'] = _.toArray;
2145
2164
  _['NPO'] = NpoPromise;
2146
2165
 
2147
- /**
2148
- * @param {import('./session-recording').SerializedRecording} serializedRecording
2149
- * @returns {boolean}
2150
- */
2151
- var isRecordingExpired = function(serializedRecording) {
2152
- var now = Date.now();
2153
- return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
2154
- };
2155
-
2156
- /**
2157
- * Shared global window property names used across modules
2158
- */
2159
-
2160
- // Targeting library global (used by flags and targeting modules)
2161
- var TARGETING_GLOBAL_NAME = '__mp_targeting';
2162
-
2163
- // Recorder library global (used by recorder and mixpanel-core)
2164
- var RECORDER_GLOBAL_NAME = '__mp_recorder';
2165
-
2166
2166
  // stateless utils
2167
2167
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
2168
2168
 
@@ -2268,7 +2268,7 @@
2268
2268
 
2269
2269
  var MAX_DEPTH = 5;
2270
2270
 
2271
- var logger$4 = console_with_prefix('autocapture');
2271
+ var logger$5 = console_with_prefix('autocapture');
2272
2272
 
2273
2273
 
2274
2274
  function getClasses(el) {
@@ -2532,7 +2532,7 @@
2532
2532
  return false;
2533
2533
  }
2534
2534
  } catch (err) {
2535
- logger$4.critical('Error while checking element in allowElementCallback', err);
2535
+ logger$5.critical('Error while checking element in allowElementCallback', err);
2536
2536
  return false;
2537
2537
  }
2538
2538
  }
@@ -2549,7 +2549,7 @@
2549
2549
  return true;
2550
2550
  }
2551
2551
  } catch (err) {
2552
- logger$4.critical('Error while checking selector: ' + sel, err);
2552
+ logger$5.critical('Error while checking selector: ' + sel, err);
2553
2553
  }
2554
2554
  }
2555
2555
  return false;
@@ -2564,7 +2564,7 @@
2564
2564
  return true;
2565
2565
  }
2566
2566
  } catch (err) {
2567
- logger$4.critical('Error while checking element in blockElementCallback', err);
2567
+ logger$5.critical('Error while checking element in blockElementCallback', err);
2568
2568
  return true;
2569
2569
  }
2570
2570
  }
@@ -2578,7 +2578,7 @@
2578
2578
  return true;
2579
2579
  }
2580
2580
  } catch (err) {
2581
- logger$4.critical('Error while checking selector: ' + sel, err);
2581
+ logger$5.critical('Error while checking selector: ' + sel, err);
2582
2582
  }
2583
2583
  }
2584
2584
  }
@@ -3042,7 +3042,7 @@
3042
3042
  observer.observe(shadowRoot, this.observerConfig);
3043
3043
  this.shadowObservers.push(observer);
3044
3044
  } catch (e) {
3045
- logger$4.critical('Error while observing shadow root', e);
3045
+ logger$5.critical('Error while observing shadow root', e);
3046
3046
  }
3047
3047
  };
3048
3048
 
@@ -3053,7 +3053,7 @@
3053
3053
  }
3054
3054
 
3055
3055
  if (!weakSetSupported()) {
3056
- logger$4.critical('Shadow DOM observation unavailable: WeakSet not supported');
3056
+ logger$5.critical('Shadow DOM observation unavailable: WeakSet not supported');
3057
3057
  return;
3058
3058
  }
3059
3059
 
@@ -3069,7 +3069,7 @@
3069
3069
  try {
3070
3070
  this.shadowObservers[i].disconnect();
3071
3071
  } catch (e) {
3072
- logger$4.critical('Error while disconnecting shadow DOM observer', e);
3072
+ logger$5.critical('Error while disconnecting shadow DOM observer', e);
3073
3073
  }
3074
3074
  }
3075
3075
  this.shadowObservers = [];
@@ -3257,7 +3257,7 @@
3257
3257
 
3258
3258
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
3259
3259
  } catch (e) {
3260
- logger$4.critical('Error while setting up mutation observer', e);
3260
+ logger$5.critical('Error while setting up mutation observer', e);
3261
3261
  }
3262
3262
  }
3263
3263
 
@@ -3272,7 +3272,7 @@
3272
3272
  );
3273
3273
  this.shadowDOMObserver.start();
3274
3274
  } catch (e) {
3275
- logger$4.critical('Error while setting up shadow DOM observer', e);
3275
+ logger$5.critical('Error while setting up shadow DOM observer', e);
3276
3276
  this.shadowDOMObserver = null;
3277
3277
  }
3278
3278
  }
@@ -3299,7 +3299,7 @@
3299
3299
  try {
3300
3300
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
3301
3301
  } catch (e) {
3302
- logger$4.critical('Error while removing event listener', e);
3302
+ logger$5.critical('Error while removing event listener', e);
3303
3303
  }
3304
3304
  }
3305
3305
  this.eventListeners = [];
@@ -3308,7 +3308,7 @@
3308
3308
  try {
3309
3309
  this.mutationObserver.disconnect();
3310
3310
  } catch (e) {
3311
- logger$4.critical('Error while disconnecting mutation observer', e);
3311
+ logger$5.critical('Error while disconnecting mutation observer', e);
3312
3312
  }
3313
3313
  this.mutationObserver = null;
3314
3314
  }
@@ -3317,7 +3317,7 @@
3317
3317
  try {
3318
3318
  this.shadowDOMObserver.stop();
3319
3319
  } catch (e) {
3320
- logger$4.critical('Error while stopping shadow DOM observer', e);
3320
+ logger$5.critical('Error while stopping shadow DOM observer', e);
3321
3321
  }
3322
3322
  this.shadowDOMObserver = null;
3323
3323
  }
@@ -3395,7 +3395,7 @@
3395
3395
 
3396
3396
  Autocapture.prototype.init = function() {
3397
3397
  if (!minDOMApisSupported()) {
3398
- logger$4.critical('Autocapture unavailable: missing required DOM APIs');
3398
+ logger$5.critical('Autocapture unavailable: missing required DOM APIs');
3399
3399
  return;
3400
3400
  }
3401
3401
  this.initPageListeners();
@@ -3427,27 +3427,15 @@
3427
3427
  };
3428
3428
 
3429
3429
  Autocapture.prototype.currentUrlBlocked = function() {
3430
- var i;
3431
3430
  var currentUrl = _.info.currentUrl();
3432
3431
 
3433
3432
  var allowUrlRegexes = this.getConfig(CONFIG_ALLOW_URL_REGEXES) || [];
3434
3433
  if (allowUrlRegexes.length) {
3435
3434
  // we're using an allowlist, only track if current URL matches
3436
- var allowed = false;
3437
- for (i = 0; i < allowUrlRegexes.length; i++) {
3438
- var allowRegex = allowUrlRegexes[i];
3439
- try {
3440
- if (currentUrl.match(allowRegex)) {
3441
- allowed = true;
3442
- break;
3443
- }
3444
- } catch (err) {
3445
- logger$4.critical('Error while checking block URL regex: ' + allowRegex, err);
3446
- return true;
3447
- }
3448
- }
3449
- if (!allowed) {
3450
- // wasn't allowed by any regex
3435
+ try {
3436
+ return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
3437
+ } catch (err) {
3438
+ logger$5.critical('Error while checking block URL regexes: ', err);
3451
3439
  return true;
3452
3440
  }
3453
3441
  }
@@ -3457,17 +3445,12 @@
3457
3445
  return false;
3458
3446
  }
3459
3447
 
3460
- for (i = 0; i < blockUrlRegexes.length; i++) {
3461
- try {
3462
- if (currentUrl.match(blockUrlRegexes[i])) {
3463
- return true;
3464
- }
3465
- } catch (err) {
3466
- logger$4.critical('Error while checking block URL regex: ' + blockUrlRegexes[i], err);
3467
- return true;
3468
- }
3448
+ try {
3449
+ return urlMatchesRegexList(currentUrl, blockUrlRegexes);
3450
+ } catch (err) {
3451
+ logger$5.critical('Error while checking block URL regexes: ', err);
3452
+ return true;
3469
3453
  }
3470
- return false;
3471
3454
  };
3472
3455
 
3473
3456
  Autocapture.prototype.pageviewTrackingConfig = function() {
@@ -3603,7 +3586,7 @@
3603
3586
  return;
3604
3587
  }
3605
3588
 
3606
- logger$4.log('Initializing scroll depth tracking');
3589
+ logger$5.log('Initializing scroll depth tracking');
3607
3590
 
3608
3591
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3609
3592
 
@@ -3629,7 +3612,7 @@
3629
3612
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
3630
3613
  return;
3631
3614
  }
3632
- logger$4.log('Initializing click tracking');
3615
+ logger$5.log('Initializing click tracking');
3633
3616
 
3634
3617
  this.listenerClick = function(ev) {
3635
3618
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -3648,7 +3631,7 @@
3648
3631
  return;
3649
3632
  }
3650
3633
 
3651
- logger$4.log('Initializing dead click tracking');
3634
+ logger$5.log('Initializing dead click tracking');
3652
3635
  if (!this._deadClickTracker) {
3653
3636
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
3654
3637
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -3682,7 +3665,7 @@
3682
3665
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
3683
3666
  return;
3684
3667
  }
3685
- logger$4.log('Initializing input tracking');
3668
+ logger$5.log('Initializing input tracking');
3686
3669
 
3687
3670
  this.listenerChange = function(ev) {
3688
3671
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -3699,7 +3682,7 @@
3699
3682
  if (!this.pageviewTrackingConfig()) {
3700
3683
  return;
3701
3684
  }
3702
- logger$4.log('Initializing pageview tracking');
3685
+ logger$5.log('Initializing pageview tracking');
3703
3686
 
3704
3687
  var previousTrackedUrl = '';
3705
3688
  var tracked = false;
@@ -3734,7 +3717,7 @@
3734
3717
  }
3735
3718
  if (didPathChange) {
3736
3719
  this.lastScrollCheckpoint = 0;
3737
- logger$4.log('Path change: re-initializing scroll depth checkpoints');
3720
+ logger$5.log('Path change: re-initializing scroll depth checkpoints');
3738
3721
  }
3739
3722
  }
3740
3723
  }.bind(this));
@@ -3749,7 +3732,7 @@
3749
3732
  return;
3750
3733
  }
3751
3734
 
3752
- logger$4.log('Initializing rage click tracking');
3735
+ logger$5.log('Initializing rage click tracking');
3753
3736
  if (!this._rageClickTracker) {
3754
3737
  this._rageClickTracker = new RageClickTracker();
3755
3738
  }
@@ -3779,7 +3762,7 @@
3779
3762
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3780
3763
  return;
3781
3764
  }
3782
- logger$4.log('Initializing scroll tracking');
3765
+ logger$5.log('Initializing scroll tracking');
3783
3766
  this.lastScrollCheckpoint = 0;
3784
3767
 
3785
3768
  var scrollTrackFunction = function() {
@@ -3816,7 +3799,7 @@
3816
3799
  }
3817
3800
  }
3818
3801
  } catch (err) {
3819
- logger$4.critical('Error while calculating scroll percentage', err);
3802
+ logger$5.critical('Error while calculating scroll percentage', err);
3820
3803
  }
3821
3804
  if (shouldTrack) {
3822
3805
  this.mp.track(MP_EV_SCROLL, props);
@@ -3834,7 +3817,7 @@
3834
3817
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
3835
3818
  return;
3836
3819
  }
3837
- logger$4.log('Initializing submit tracking');
3820
+ logger$5.log('Initializing submit tracking');
3838
3821
 
3839
3822
  this.listenerSubmit = function(ev) {
3840
3823
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -3856,7 +3839,7 @@
3856
3839
  return;
3857
3840
  }
3858
3841
 
3859
- logger$4.log('Initializing page visibility tracking.');
3842
+ logger$5.log('Initializing page visibility tracking.');
3860
3843
  this._initScrollDepthTracking();
3861
3844
  var previousTrackedUrl = _.info.currentUrl();
3862
3845
 
@@ -3941,7 +3924,7 @@
3941
3924
  return win[TARGETING_GLOBAL_NAME];
3942
3925
  };
3943
3926
 
3944
- var logger$3 = console_with_prefix('flags');
3927
+ var logger$4 = console_with_prefix('flags');
3945
3928
  var FLAGS_CONFIG_KEY = 'flags';
3946
3929
 
3947
3930
  var CONFIG_CONTEXT = 'context';
@@ -3984,7 +3967,7 @@
3984
3967
 
3985
3968
  FeatureFlagManager.prototype.init = function() {
3986
3969
  if (!this.minApisSupported()) {
3987
- logger$3.critical('Feature Flags unavailable: missing minimum required APIs');
3970
+ logger$4.critical('Feature Flags unavailable: missing minimum required APIs');
3988
3971
  return;
3989
3972
  }
3990
3973
 
@@ -4019,7 +4002,7 @@
4019
4002
 
4020
4003
  FeatureFlagManager.prototype.updateContext = function(newContext, options) {
4021
4004
  if (!this.isSystemEnabled()) {
4022
- logger$3.critical('Feature Flags not enabled, cannot update context');
4005
+ logger$4.critical('Feature Flags not enabled, cannot update context');
4023
4006
  return Promise.resolve();
4024
4007
  }
4025
4008
 
@@ -4036,7 +4019,7 @@
4036
4019
 
4037
4020
  FeatureFlagManager.prototype.areFlagsReady = function() {
4038
4021
  if (!this.isSystemEnabled()) {
4039
- logger$3.error('Feature Flags not enabled');
4022
+ logger$4.error('Feature Flags not enabled');
4040
4023
  }
4041
4024
  return !!this.flags;
4042
4025
  };
@@ -4049,7 +4032,7 @@
4049
4032
  var distinctId = this.getMpProperty('distinct_id');
4050
4033
  var deviceId = this.getMpProperty('$device_id');
4051
4034
  var traceparent = generateTraceparent();
4052
- logger$3.log('Fetching flags for distinct ID: ' + distinctId);
4035
+ logger$4.log('Fetching flags for distinct ID: ' + distinctId);
4053
4036
 
4054
4037
  var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
4055
4038
  var searchParams = new URLSearchParams();
@@ -4148,11 +4131,11 @@
4148
4131
  this._loadTargetingIfNeeded();
4149
4132
  }.bind(this)).catch(function(error) {
4150
4133
  this.markFetchComplete();
4151
- logger$3.error(error);
4134
+ logger$4.error(error);
4152
4135
  }.bind(this));
4153
4136
  }.bind(this)).catch(function(error) {
4154
4137
  this.markFetchComplete();
4155
- logger$3.error(error);
4138
+ logger$4.error(error);
4156
4139
  }.bind(this));
4157
4140
 
4158
4141
  return this.fetchPromise;
@@ -4160,7 +4143,7 @@
4160
4143
 
4161
4144
  FeatureFlagManager.prototype.markFetchComplete = function() {
4162
4145
  if (!this._fetchInProgressStartTime) {
4163
- logger$3.error('Fetch in progress started time not set, cannot mark fetch complete');
4146
+ logger$4.error('Fetch in progress started time not set, cannot mark fetch complete');
4164
4147
  return;
4165
4148
  }
4166
4149
  this._fetchStartTime = this._fetchInProgressStartTime;
@@ -4182,7 +4165,7 @@
4182
4165
 
4183
4166
  if (hasPropertyFilters) {
4184
4167
  this.getTargeting().then(function() {
4185
- logger$3.log('targeting loaded for property filter evaluation');
4168
+ logger$4.log('targeting loaded for property filter evaluation');
4186
4169
  });
4187
4170
  }
4188
4171
  };
@@ -4197,7 +4180,7 @@
4197
4180
  this.loadExtraBundle.bind(this),
4198
4181
  this.targetingSrc
4199
4182
  ).catch(function(error) {
4200
- logger$3.error('Failed to load targeting: ' + error);
4183
+ logger$4.error('Failed to load targeting: ' + error);
4201
4184
  }.bind(this));
4202
4185
  };
4203
4186
 
@@ -4251,7 +4234,7 @@
4251
4234
 
4252
4235
  // If no targeting library and event has property filters, skip it
4253
4236
  if (!targeting && pendingEvent['property_filters'] && !_.isEmptyObject(pendingEvent['property_filters'])) {
4254
- logger$3.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
4237
+ logger$4.warn('Skipping event check for "' + flagKey + '" - property filters require targeting library');
4255
4238
  return;
4256
4239
  }
4257
4240
 
@@ -4274,7 +4257,7 @@
4274
4257
  }
4275
4258
 
4276
4259
  if (matchResult.error) {
4277
- logger$3.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
4260
+ logger$4.error('Error checking first-time event for flag "' + flagKey + '": ' + matchResult.error);
4278
4261
  return;
4279
4262
  }
4280
4263
 
@@ -4282,7 +4265,7 @@
4282
4265
  return;
4283
4266
  }
4284
4267
 
4285
- logger$3.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
4268
+ logger$4.log('First-time event matched for flag "' + flagKey + '": ' + eventName);
4286
4269
 
4287
4270
  var newVariant = {
4288
4271
  'key': pendingEvent['pending_variant']['variant_key'],
@@ -4323,7 +4306,7 @@
4323
4306
  'first_time_event_hash': firstTimeEventHash
4324
4307
  };
4325
4308
 
4326
- logger$3.log('Recording first-time event for flag: ' + flagId);
4309
+ logger$4.log('Recording first-time event for flag: ' + flagId);
4327
4310
 
4328
4311
  // Fire-and-forget POST request
4329
4312
  this.fetch.call(win, url, {
@@ -4336,14 +4319,14 @@
4336
4319
  'body': JSON.stringify(payload)
4337
4320
  }).catch(function(error) {
4338
4321
  // Silent failure - cohort sync will catch up
4339
- logger$3.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
4322
+ logger$4.error('Failed to record first-time event for flag ' + flagId + ': ' + error);
4340
4323
  });
4341
4324
  };
4342
4325
 
4343
4326
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
4344
4327
  if (!this.fetchPromise) {
4345
4328
  return new Promise(function(resolve) {
4346
- logger$3.critical('Feature Flags not initialized');
4329
+ logger$4.critical('Feature Flags not initialized');
4347
4330
  resolve(fallback);
4348
4331
  });
4349
4332
  }
@@ -4351,19 +4334,19 @@
4351
4334
  return this.fetchPromise.then(function() {
4352
4335
  return this.getVariantSync(featureName, fallback);
4353
4336
  }.bind(this)).catch(function(error) {
4354
- logger$3.error(error);
4337
+ logger$4.error(error);
4355
4338
  return fallback;
4356
4339
  });
4357
4340
  };
4358
4341
 
4359
4342
  FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
4360
4343
  if (!this.areFlagsReady()) {
4361
- logger$3.log('Flags not loaded yet');
4344
+ logger$4.log('Flags not loaded yet');
4362
4345
  return fallback;
4363
4346
  }
4364
4347
  var feature = this.flags.get(featureName);
4365
4348
  if (!feature) {
4366
- logger$3.log('No flag found: "' + featureName + '"');
4349
+ logger$4.log('No flag found: "' + featureName + '"');
4367
4350
  return fallback;
4368
4351
  }
4369
4352
  this.trackFeatureCheck(featureName, feature);
@@ -4374,14 +4357,14 @@
4374
4357
  return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
4375
4358
  return feature['value'];
4376
4359
  }).catch(function(error) {
4377
- logger$3.error(error);
4360
+ logger$4.error(error);
4378
4361
  return fallbackValue;
4379
4362
  });
4380
4363
  };
4381
4364
 
4382
4365
  // TODO remove deprecated method
4383
4366
  FeatureFlagManager.prototype.getFeatureData = function(featureName, fallbackValue) {
4384
- logger$3.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
4367
+ logger$4.critical('mixpanel.flags.get_feature_data() is deprecated and will be removed in a future release. Use mixpanel.flags.get_variant_value() instead.');
4385
4368
  return this.getVariantValue(featureName, fallbackValue);
4386
4369
  };
4387
4370
 
@@ -4393,7 +4376,7 @@
4393
4376
  return this.getVariantValue(featureName).then(function() {
4394
4377
  return this.isEnabledSync(featureName, fallbackValue);
4395
4378
  }.bind(this)).catch(function(error) {
4396
- logger$3.error(error);
4379
+ logger$4.error(error);
4397
4380
  return fallbackValue;
4398
4381
  });
4399
4382
  };
@@ -4402,7 +4385,7 @@
4402
4385
  fallbackValue = fallbackValue || false;
4403
4386
  var val = this.getVariantValueSync(featureName, fallbackValue);
4404
4387
  if (val !== true && val !== false) {
4405
- logger$3.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
4388
+ logger$4.error('Feature flag "' + featureName + '" value: ' + val + ' is not a boolean; returning fallback value: ' + fallbackValue);
4406
4389
  val = fallbackValue;
4407
4390
  }
4408
4391
  return val;
@@ -4461,143 +4444,618 @@
4461
4444
  // Exports intended only for testing
4462
4445
  FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
4463
4446
 
4464
- /* eslint camelcase: "off" */
4447
+ var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
4448
+
4449
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
4450
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
4465
4451
 
4452
+ // note: increment the version number when adding new object stores
4453
+ var DB_VERSION = 1;
4454
+ var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
4466
4455
 
4467
4456
  /**
4468
- * DomTracker Object
4469
- * @constructor
4457
+ * @type {import('./wrapper').StorageWrapper}
4470
4458
  */
4471
- var DomTracker = function() {};
4459
+ var IDBStorageWrapper = function (storeName) {
4460
+ /**
4461
+ * @type {Promise<IDBDatabase>|null}
4462
+ */
4463
+ this.dbPromise = null;
4464
+ this.storeName = storeName;
4465
+ };
4466
+
4467
+ IDBStorageWrapper.prototype._openDb = function () {
4468
+ return new PromisePolyfill(function (resolve, reject) {
4469
+ var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
4470
+ openRequest['onerror'] = function () {
4471
+ reject(openRequest.error);
4472
+ };
4472
4473
 
4474
+ openRequest['onsuccess'] = function () {
4475
+ resolve(openRequest.result);
4476
+ };
4473
4477
 
4474
- // interface
4475
- DomTracker.prototype.create_properties = function() {};
4476
- DomTracker.prototype.event_handler = function() {};
4477
- DomTracker.prototype.after_track_handler = function() {};
4478
+ openRequest['onupgradeneeded'] = function (ev) {
4479
+ var db = ev.target.result;
4478
4480
 
4479
- DomTracker.prototype.init = function(mixpanel_instance) {
4480
- this.mp = mixpanel_instance;
4481
- return this;
4481
+ OBJECT_STORES.forEach(function (storeName) {
4482
+ db.createObjectStore(storeName);
4483
+ });
4484
+ };
4485
+ });
4482
4486
  };
4483
4487
 
4484
- /**
4485
- * @param {Object|string} query
4486
- * @param {string} event_name
4487
- * @param {Object=} properties
4488
- * @param {function=} user_callback
4489
- */
4490
- DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
4491
- var that = this;
4492
- var elements = _.dom_query(query);
4493
-
4494
- if (elements.length === 0) {
4495
- console.error('The DOM query (' + query + ') returned 0 elements');
4496
- return;
4488
+ IDBStorageWrapper.prototype.init = function () {
4489
+ if (!win.indexedDB) {
4490
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
4497
4491
  }
4498
4492
 
4499
- _.each(elements, function(element) {
4500
- _.register_event(element, this.override_event, function(e) {
4501
- var options = {};
4502
- var props = that.create_properties(properties, this);
4503
- var timeout = that.mp.get_config('track_links_timeout');
4504
-
4505
- that.event_handler(e, this, options);
4506
-
4507
- // in case the mixpanel servers don't get back to us in time
4508
- window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
4493
+ if (!this.dbPromise) {
4494
+ this.dbPromise = this._openDb();
4495
+ }
4509
4496
 
4510
- // fire the tracking event
4511
- that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
4497
+ return this.dbPromise
4498
+ .then(function (dbOrError) {
4499
+ if (dbOrError instanceof win['IDBDatabase']) {
4500
+ return PromisePolyfill.resolve();
4501
+ } else {
4502
+ return PromisePolyfill.reject(dbOrError);
4503
+ }
4512
4504
  });
4513
- }, this);
4505
+ };
4514
4506
 
4515
- return true;
4507
+ IDBStorageWrapper.prototype.isInitialized = function () {
4508
+ return !!this.dbPromise;
4516
4509
  };
4517
4510
 
4518
4511
  /**
4519
- * @param {function} user_callback
4520
- * @param {Object} props
4521
- * @param {boolean=} timeout_occured
4512
+ * @param {IDBTransactionMode} mode
4513
+ * @param {function(IDBObjectStore): void} storeCb
4522
4514
  */
4523
- DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
4524
- timeout_occured = timeout_occured || false;
4525
- var that = this;
4515
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
4516
+ var storeName = this.storeName;
4517
+ var doTransaction = function (db) {
4518
+ return new PromisePolyfill(function (resolve, reject) {
4519
+ var transaction = db.transaction(storeName, mode);
4520
+ transaction.oncomplete = function () {
4521
+ resolve(transaction);
4522
+ };
4523
+ transaction.onabort = transaction.onerror = function () {
4524
+ reject(transaction.error);
4525
+ };
4526
4526
 
4527
- return function() {
4528
- // options is referenced from both callbacks, so we can have
4529
- // a 'lock' of sorts to ensure only one fires
4530
- if (options.callback_fired) { return; }
4531
- options.callback_fired = true;
4527
+ storeCb(transaction.objectStore(storeName));
4528
+ });
4529
+ };
4532
4530
 
4533
- if (user_callback && user_callback(timeout_occured, props) === false) {
4534
- // user can prevent the default functionality by
4535
- // returning false from their callback
4536
- return;
4537
- }
4531
+ return this.dbPromise
4532
+ .then(doTransaction)
4533
+ .catch(function (err) {
4534
+ if (err && err['name'] === 'InvalidStateError') {
4535
+ // try reopening the DB if the connection is closed
4536
+ this.dbPromise = this._openDb();
4537
+ return this.dbPromise.then(doTransaction);
4538
+ } else {
4539
+ return PromisePolyfill.reject(err);
4540
+ }
4541
+ }.bind(this));
4542
+ };
4538
4543
 
4539
- that.after_track_handler(props, options, timeout_occured);
4540
- };
4544
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
4545
+ return this.makeTransaction('readwrite', function (objectStore) {
4546
+ objectStore.put(value, key);
4547
+ });
4541
4548
  };
4542
4549
 
4543
- DomTracker.prototype.create_properties = function(properties, element) {
4544
- var props;
4550
+ IDBStorageWrapper.prototype.getItem = function (key) {
4551
+ var req;
4552
+ return this.makeTransaction('readonly', function (objectStore) {
4553
+ req = objectStore.get(key);
4554
+ }).then(function () {
4555
+ return req.result;
4556
+ });
4557
+ };
4545
4558
 
4546
- if (typeof(properties) === 'function') {
4547
- props = properties(element);
4548
- } else {
4549
- props = _.extend({}, properties);
4550
- }
4559
+ IDBStorageWrapper.prototype.removeItem = function (key) {
4560
+ return this.makeTransaction('readwrite', function (objectStore) {
4561
+ objectStore.delete(key);
4562
+ });
4563
+ };
4551
4564
 
4552
- return props;
4565
+ IDBStorageWrapper.prototype.getAll = function () {
4566
+ var req;
4567
+ return this.makeTransaction('readonly', function (objectStore) {
4568
+ req = objectStore.getAll();
4569
+ }).then(function () {
4570
+ return req.result;
4571
+ });
4553
4572
  };
4554
4573
 
4555
4574
  /**
4556
- * LinkTracker Object
4557
- * @constructor
4558
- * @extends DomTracker
4575
+ * @param {import('./session-recording').SerializedRecording} serializedRecording
4576
+ * @returns {boolean}
4559
4577
  */
4560
- var LinkTracker = function() {
4561
- this.override_event = 'click';
4578
+ var isRecordingExpired = function(serializedRecording) {
4579
+ var now = Date.now();
4580
+ return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
4562
4581
  };
4563
- _.inherit(LinkTracker, DomTracker);
4564
-
4565
- LinkTracker.prototype.create_properties = function(properties, element) {
4566
- var props = LinkTracker.superclass.create_properties.apply(this, arguments);
4567
-
4568
- if (element.href) { props['url'] = element.href; }
4569
4582
 
4570
- return props;
4583
+ var validateAllowedOrigins = function(origins, logger) {
4584
+ if (!_.isArray(origins)) {
4585
+ if (origins) {
4586
+ logger.critical('record_allowed_iframe_origins must be an array of origin strings, cross-origin recording will be disabled.');
4587
+ }
4588
+ return [];
4589
+ }
4590
+ var valid = [];
4591
+ for (var i = 0; i < origins.length; i++) {
4592
+ try {
4593
+ var origin = new URL(origins[i]).origin;
4594
+ if (origin === 'null') {
4595
+ logger.critical(origins[i] + ' has an opaque origin. Skipping this entry.');
4596
+ continue;
4597
+ }
4598
+ valid.push(origin);
4599
+ } catch (e) {
4600
+ logger.critical(origins[i] + ' is not a valid origin URL. Skipping this entry.');
4601
+ }
4602
+ }
4603
+ return valid;
4571
4604
  };
4572
4605
 
4573
- LinkTracker.prototype.event_handler = function(evt, element, options) {
4574
- options.new_tab = (
4575
- evt.which === 2 ||
4576
- evt.metaKey ||
4577
- evt.ctrlKey ||
4578
- element.target === '_blank'
4579
- );
4580
- options.href = element.href;
4606
+ /* eslint camelcase: "off" */
4581
4607
 
4582
- if (!options.new_tab) {
4583
- evt.preventDefault();
4584
- }
4585
- };
4586
4608
 
4587
- LinkTracker.prototype.after_track_handler = function(props, options) {
4588
- if (options.new_tab) { return; }
4609
+ var logger$3 = console_with_prefix('recorder');
4610
+
4611
+ var IFRAME_HANDSHAKE_REQUEST = 'mp_iframe_handshake_request';
4612
+ var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
4589
4613
 
4590
- setTimeout(function() {
4591
- window.location = options.href;
4592
- }, 0);
4593
- };
4594
4614
 
4595
4615
  /**
4596
- * FormTracker Object
4616
+ * RecorderManager: manages session recording initialization, lifecycle and state
4597
4617
  * @constructor
4598
- * @extends DomTracker
4599
4618
  */
4600
- var FormTracker = function() {
4619
+ var RecorderManager = function(initOptions) {
4620
+ // TODO - Passing in mixpanel instance as it is still needed for recorder creation
4621
+ // but ideally we should be able to remove this dependency.
4622
+ this.mixpanelInstance = initOptions.mixpanelInstance;
4623
+
4624
+ this.getMpConfig = initOptions.getConfigFunc;
4625
+ this.getTabId = initOptions.getTabIdFunc;
4626
+ this.reportError = initOptions.reportErrorFunc;
4627
+ this.getDistinctId = initOptions.getDistinctIdFunc;
4628
+ this.loadExtraBundle = initOptions.loadExtraBundle;
4629
+ this.recorderSrc = initOptions.recorderSrc;
4630
+ this.targetingSrc = initOptions.targetingSrc;
4631
+ this.libBasePath = initOptions.libBasePath;
4632
+
4633
+ this._recorder = null;
4634
+ this._parentReplayId = null;
4635
+ this._parentFrameRetryInterval = null;
4636
+ };
4637
+
4638
+ RecorderManager.prototype.shouldLoadRecorder = function() {
4639
+ if (this.getMpConfig('disable_persistence')) {
4640
+ console.log('Load recorder check skipped due to disable_persistence config');
4641
+ return PromisePolyfill.resolve(false);
4642
+ }
4643
+
4644
+ var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
4645
+ var tab_id = this.getTabId();
4646
+ return recording_registry_idb.init()
4647
+ .then(function () {
4648
+ return recording_registry_idb.getAll();
4649
+ })
4650
+ .then(function (recordings) {
4651
+ for (var i = 0; i < recordings.length; i++) {
4652
+ // if there are expired recordings in the registry, we should load the recorder to flush them
4653
+ // if there's a recording for this tab id, we should load the recorder to continue the recording
4654
+ if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
4655
+ return true;
4656
+ }
4657
+ }
4658
+ return false;
4659
+ })
4660
+ .catch(_.bind(function (err) {
4661
+ this.reportError('Error checking recording registry', err);
4662
+ return false;
4663
+ }, this));
4664
+ };
4665
+
4666
+ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start, rate) {
4667
+ if (!win['MutationObserver']) {
4668
+ console.critical('Browser does not support MutationObserver; skipping session recording');
4669
+ return PromisePolyfill.resolve();
4670
+ }
4671
+
4672
+ var loadRecorder = _.bind(function(startNewIfInactive) {
4673
+ return new PromisePolyfill(_.bind(function(resolve) {
4674
+ var handleLoadedRecorder = safewrap(_.bind(function() {
4675
+ this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this.mixpanelInstance);
4676
+ this._recorder['resumeRecording'](startNewIfInactive);
4677
+ resolve();
4678
+ }, this));
4679
+
4680
+ if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
4681
+ var recorderSrc = this.recorderSrc || (this.libBasePath + RECORDER_FILENAME);
4682
+ this.loadExtraBundle(recorderSrc, handleLoadedRecorder);
4683
+ } else {
4684
+ handleLoadedRecorder();
4685
+ }
4686
+ }, this));
4687
+ }, this);
4688
+
4689
+ // Cross-origin iframe handling
4690
+ var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger$3);
4691
+ var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
4692
+
4693
+ if (isCrossOriginRecordingEnabled) {
4694
+ // listen for handshake requests from their own child iframes (including nested)
4695
+ this._setupParentFrameListener(allowedOrigins);
4696
+
4697
+ if (win.parent !== win) {
4698
+ // also wait for parent's replay ID
4699
+ this._setupChildFrameListener(allowedOrigins, loadRecorder);
4700
+ this._sendParentFrameRequestWithRetry(allowedOrigins);
4701
+ return PromisePolyfill.resolve();
4702
+ }
4703
+ }
4704
+
4705
+ /**
4706
+ * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
4707
+ * 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.
4708
+ */
4709
+ var effective_rate = _.isUndefined(rate) ? this.getMpConfig('record_sessions_percent') : rate;
4710
+ var is_sampled = effective_rate > 0 && Math.random() * 100 <= effective_rate;
4711
+ if (force_start || is_sampled) {
4712
+ return loadRecorder(true);
4713
+ } else {
4714
+ return this.shouldLoadRecorder()
4715
+ .then(_.bind(function (shouldLoad) {
4716
+ if (shouldLoad) {
4717
+ return loadRecorder(false);
4718
+ }
4719
+ return PromisePolyfill.resolve();
4720
+ }, this));
4721
+ }
4722
+ };
4723
+
4724
+ RecorderManager.prototype.isRecording = function() {
4725
+ // Safety check: ensure isRecording method exists (older CDN builds may not have it)
4726
+ if (!this._recorder || !_.isFunction(this._recorder['isRecording'])) {
4727
+ return false;
4728
+ }
4729
+ try {
4730
+ return this._recorder['isRecording']();
4731
+ } catch (e) {
4732
+ this.reportError('Error checking if recording is active', e);
4733
+ return false;
4734
+ }
4735
+ };
4736
+
4737
+ RecorderManager.prototype.startRecordingOnEvent = function(event_name, properties) {
4738
+ var isRecording = this.isRecording();
4739
+ var recordingTriggerEvents = this.getMpConfig('recording_event_triggers');
4740
+
4741
+ if (!isRecording && recordingTriggerEvents) {
4742
+ var trigger = recordingTriggerEvents[event_name];
4743
+ if (trigger && typeof trigger['percentage'] === 'number') {
4744
+ var newRate = trigger['percentage'];
4745
+ var propertyFilters = trigger['property_filters'];
4746
+ if (propertyFilters && !_.isEmptyObject(propertyFilters)) {
4747
+ var targetingSrc = this.targetingSrc || (this.libBasePath + TARGETING_FILENAME);
4748
+ getTargetingPromise(this.loadExtraBundle, targetingSrc)
4749
+ .then(function(targeting) {
4750
+ try {
4751
+ var result = targeting['eventMatchesCriteria'](
4752
+ event_name,
4753
+ properties,
4754
+ {
4755
+ 'event_name': event_name,
4756
+ 'property_filters': propertyFilters
4757
+ }
4758
+ );
4759
+ if (result['matches']) {
4760
+ this.checkAndStartSessionRecording(false, newRate);
4761
+ }
4762
+ } catch (err) {
4763
+ console.critical('Could not parse recording event trigger properties logic:', err);
4764
+ }
4765
+ }.bind(this)).catch(function(err) {
4766
+ console.critical('Failed to load targeting library:', err);
4767
+ });
4768
+ } else {
4769
+ this.checkAndStartSessionRecording(false, newRate);
4770
+ }
4771
+ }
4772
+ }
4773
+ };
4774
+
4775
+ RecorderManager.prototype.stopSessionRecording = function() {
4776
+ if (this._recorder) {
4777
+ return this._recorder['stopRecording']();
4778
+ }
4779
+ return PromisePolyfill.resolve();
4780
+ };
4781
+
4782
+ RecorderManager.prototype.pauseSessionRecording = function() {
4783
+ if (this._recorder) {
4784
+ return this._recorder['pauseRecording']();
4785
+ }
4786
+ return PromisePolyfill.resolve();
4787
+ };
4788
+
4789
+ RecorderManager.prototype.resumeSessionRecording = function() {
4790
+ if (this._recorder) {
4791
+ return this._recorder['resumeRecording']();
4792
+ }
4793
+ return PromisePolyfill.resolve();
4794
+ };
4795
+
4796
+ RecorderManager.prototype.isRecordingHeatmapData = function() {
4797
+ return this.getSessionReplayId() && this.getMpConfig('record_heatmap_data');
4798
+ };
4799
+
4800
+ RecorderManager.prototype.getSessionRecordingProperties = function() {
4801
+ var props = {};
4802
+ var replay_id = this.getSessionReplayId();
4803
+ if (replay_id) {
4804
+ props['$mp_replay_id'] = replay_id;
4805
+ }
4806
+ return props;
4807
+ };
4808
+
4809
+ RecorderManager.prototype.getSessionReplayUrl = function() {
4810
+ var replay_url = null;
4811
+ var replay_id = this.getSessionReplayId();
4812
+ if (replay_id) {
4813
+ var query_params = _.HTTPBuildQuery({
4814
+ 'replay_id': replay_id,
4815
+ 'distinct_id': this.getDistinctId(),
4816
+ 'token': this.getMpConfig('token')
4817
+ });
4818
+ replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
4819
+ }
4820
+ return replay_url;
4821
+ };
4822
+
4823
+ RecorderManager.prototype.getSessionReplayId = function() {
4824
+ // Child iframe uses parent's replay ID
4825
+ if (this._parentReplayId) {
4826
+ return this._parentReplayId;
4827
+ }
4828
+ var replay_id = null;
4829
+ if (this._recorder) {
4830
+ replay_id = this._recorder['replayId'];
4831
+ }
4832
+ return replay_id || null;
4833
+ };
4834
+
4835
+ // "private" public method to reach into the recorder in test cases
4836
+ RecorderManager.prototype.getRecorder = function() {
4837
+ return this._recorder;
4838
+ };
4839
+
4840
+ RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
4841
+ if (this._childFrameMessageHandler) {
4842
+ return;
4843
+ }
4844
+ var self = this;
4845
+ this._childFrameMessageHandler = function(event) {
4846
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
4847
+ var data = event.data;
4848
+ if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
4849
+ self._parentReplayId = data['replayId'];
4850
+ if (data['distinctId']) {
4851
+ self.mixpanelInstance['identify'](data['distinctId']);
4852
+ }
4853
+ self._parentFrameRetryActive = false;
4854
+ win.removeEventListener('message', self._childFrameMessageHandler);
4855
+ self._childFrameMessageHandler = null;
4856
+ loadRecorder(true);
4857
+ }
4858
+ };
4859
+ win.addEventListener('message', this._childFrameMessageHandler);
4860
+ };
4861
+
4862
+ RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
4863
+ var message = {};
4864
+ message['type'] = IFRAME_HANDSHAKE_REQUEST;
4865
+ message['token'] = this.getMpConfig('token');
4866
+ for (var i = 0; i < allowedOrigins.length; i++) {
4867
+ try {
4868
+ win.parent.postMessage(message, allowedOrigins[i]);
4869
+ } catch (e) {
4870
+ // origin mismatch - ignore
4871
+ }
4872
+ }
4873
+ };
4874
+
4875
+ RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
4876
+ var self = this;
4877
+ var maxRetries = 10;
4878
+ var retryCount = 0;
4879
+ var delay = 50;
4880
+ this._parentFrameRetryActive = true;
4881
+
4882
+ this._sendParentFrameRequest(allowedOrigins);
4883
+
4884
+ function scheduleRetry() {
4885
+ setTimeout(function() {
4886
+ if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
4887
+ return;
4888
+ }
4889
+ self._sendParentFrameRequest(allowedOrigins);
4890
+ delay *= 2;
4891
+ scheduleRetry();
4892
+ }, delay);
4893
+ }
4894
+ scheduleRetry();
4895
+ };
4896
+
4897
+ RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
4898
+ if (this._parentFrameMessageHandler) {
4899
+ return;
4900
+ }
4901
+ var self = this;
4902
+ this._parentFrameMessageHandler = function(event) {
4903
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
4904
+ var data = event.data;
4905
+ if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
4906
+ var replayId = self.getSessionReplayId();
4907
+ if (replayId) {
4908
+ var response = {};
4909
+ response['type'] = IFRAME_HANDSHAKE_RESPONSE;
4910
+ response['token'] = self.getMpConfig('token');
4911
+ response['replayId'] = replayId;
4912
+ response['distinctId'] = self.getDistinctId();
4913
+ event.source.postMessage(response, event.origin);
4914
+ }
4915
+ }
4916
+ };
4917
+ win.addEventListener('message', this._parentFrameMessageHandler);
4918
+ };
4919
+
4920
+ safewrapClass(RecorderManager);
4921
+
4922
+ /* eslint camelcase: "off" */
4923
+
4924
+
4925
+ /**
4926
+ * DomTracker Object
4927
+ * @constructor
4928
+ */
4929
+ var DomTracker = function() {};
4930
+
4931
+
4932
+ // interface
4933
+ DomTracker.prototype.create_properties = function() {};
4934
+ DomTracker.prototype.event_handler = function() {};
4935
+ DomTracker.prototype.after_track_handler = function() {};
4936
+
4937
+ DomTracker.prototype.init = function(mixpanel_instance) {
4938
+ this.mp = mixpanel_instance;
4939
+ return this;
4940
+ };
4941
+
4942
+ /**
4943
+ * @param {Object|string} query
4944
+ * @param {string} event_name
4945
+ * @param {Object=} properties
4946
+ * @param {function=} user_callback
4947
+ */
4948
+ DomTracker.prototype.track = function(query, event_name, properties, user_callback) {
4949
+ var that = this;
4950
+ var elements = _.dom_query(query);
4951
+
4952
+ if (elements.length === 0) {
4953
+ console.error('The DOM query (' + query + ') returned 0 elements');
4954
+ return;
4955
+ }
4956
+
4957
+ _.each(elements, function(element) {
4958
+ _.register_event(element, this.override_event, function(e) {
4959
+ var options = {};
4960
+ var props = that.create_properties(properties, this);
4961
+ var timeout = that.mp.get_config('track_links_timeout');
4962
+
4963
+ that.event_handler(e, this, options);
4964
+
4965
+ // in case the mixpanel servers don't get back to us in time
4966
+ window.setTimeout(that.track_callback(user_callback, props, options, true), timeout);
4967
+
4968
+ // fire the tracking event
4969
+ that.mp.track(event_name, props, that.track_callback(user_callback, props, options));
4970
+ });
4971
+ }, this);
4972
+
4973
+ return true;
4974
+ };
4975
+
4976
+ /**
4977
+ * @param {function} user_callback
4978
+ * @param {Object} props
4979
+ * @param {boolean=} timeout_occured
4980
+ */
4981
+ DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) {
4982
+ timeout_occured = timeout_occured || false;
4983
+ var that = this;
4984
+
4985
+ return function() {
4986
+ // options is referenced from both callbacks, so we can have
4987
+ // a 'lock' of sorts to ensure only one fires
4988
+ if (options.callback_fired) { return; }
4989
+ options.callback_fired = true;
4990
+
4991
+ if (user_callback && user_callback(timeout_occured, props) === false) {
4992
+ // user can prevent the default functionality by
4993
+ // returning false from their callback
4994
+ return;
4995
+ }
4996
+
4997
+ that.after_track_handler(props, options, timeout_occured);
4998
+ };
4999
+ };
5000
+
5001
+ DomTracker.prototype.create_properties = function(properties, element) {
5002
+ var props;
5003
+
5004
+ if (typeof(properties) === 'function') {
5005
+ props = properties(element);
5006
+ } else {
5007
+ props = _.extend({}, properties);
5008
+ }
5009
+
5010
+ return props;
5011
+ };
5012
+
5013
+ /**
5014
+ * LinkTracker Object
5015
+ * @constructor
5016
+ * @extends DomTracker
5017
+ */
5018
+ var LinkTracker = function() {
5019
+ this.override_event = 'click';
5020
+ };
5021
+ _.inherit(LinkTracker, DomTracker);
5022
+
5023
+ LinkTracker.prototype.create_properties = function(properties, element) {
5024
+ var props = LinkTracker.superclass.create_properties.apply(this, arguments);
5025
+
5026
+ if (element.href) { props['url'] = element.href; }
5027
+
5028
+ return props;
5029
+ };
5030
+
5031
+ LinkTracker.prototype.event_handler = function(evt, element, options) {
5032
+ options.new_tab = (
5033
+ evt.which === 2 ||
5034
+ evt.metaKey ||
5035
+ evt.ctrlKey ||
5036
+ element.target === '_blank'
5037
+ );
5038
+ options.href = element.href;
5039
+
5040
+ if (!options.new_tab) {
5041
+ evt.preventDefault();
5042
+ }
5043
+ };
5044
+
5045
+ LinkTracker.prototype.after_track_handler = function(props, options) {
5046
+ if (options.new_tab) { return; }
5047
+
5048
+ setTimeout(function() {
5049
+ window.location = options.href;
5050
+ }, 0);
5051
+ };
5052
+
5053
+ /**
5054
+ * FormTracker Object
5055
+ * @constructor
5056
+ * @extends DomTracker
5057
+ */
5058
+ var FormTracker = function() {
4601
5059
  this.override_event = 'submit';
4602
5060
  };
4603
5061
  _.inherit(FormTracker, DomTracker);
@@ -6982,133 +7440,6 @@
6982
7440
  return timestamp;
6983
7441
  };
6984
7442
 
6985
- var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
6986
-
6987
- var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
6988
- var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
6989
-
6990
- // note: increment the version number when adding new object stores
6991
- var DB_VERSION = 1;
6992
- var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
6993
-
6994
- /**
6995
- * @type {import('./wrapper').StorageWrapper}
6996
- */
6997
- var IDBStorageWrapper = function (storeName) {
6998
- /**
6999
- * @type {Promise<IDBDatabase>|null}
7000
- */
7001
- this.dbPromise = null;
7002
- this.storeName = storeName;
7003
- };
7004
-
7005
- IDBStorageWrapper.prototype._openDb = function () {
7006
- return new PromisePolyfill(function (resolve, reject) {
7007
- var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
7008
- openRequest['onerror'] = function () {
7009
- reject(openRequest.error);
7010
- };
7011
-
7012
- openRequest['onsuccess'] = function () {
7013
- resolve(openRequest.result);
7014
- };
7015
-
7016
- openRequest['onupgradeneeded'] = function (ev) {
7017
- var db = ev.target.result;
7018
-
7019
- OBJECT_STORES.forEach(function (storeName) {
7020
- db.createObjectStore(storeName);
7021
- });
7022
- };
7023
- });
7024
- };
7025
-
7026
- IDBStorageWrapper.prototype.init = function () {
7027
- if (!win.indexedDB) {
7028
- return PromisePolyfill.reject('indexedDB is not supported in this browser');
7029
- }
7030
-
7031
- if (!this.dbPromise) {
7032
- this.dbPromise = this._openDb();
7033
- }
7034
-
7035
- return this.dbPromise
7036
- .then(function (dbOrError) {
7037
- if (dbOrError instanceof win['IDBDatabase']) {
7038
- return PromisePolyfill.resolve();
7039
- } else {
7040
- return PromisePolyfill.reject(dbOrError);
7041
- }
7042
- });
7043
- };
7044
-
7045
- IDBStorageWrapper.prototype.isInitialized = function () {
7046
- return !!this.dbPromise;
7047
- };
7048
-
7049
- /**
7050
- * @param {IDBTransactionMode} mode
7051
- * @param {function(IDBObjectStore): void} storeCb
7052
- */
7053
- IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
7054
- var storeName = this.storeName;
7055
- var doTransaction = function (db) {
7056
- return new PromisePolyfill(function (resolve, reject) {
7057
- var transaction = db.transaction(storeName, mode);
7058
- transaction.oncomplete = function () {
7059
- resolve(transaction);
7060
- };
7061
- transaction.onabort = transaction.onerror = function () {
7062
- reject(transaction.error);
7063
- };
7064
-
7065
- storeCb(transaction.objectStore(storeName));
7066
- });
7067
- };
7068
-
7069
- return this.dbPromise
7070
- .then(doTransaction)
7071
- .catch(function (err) {
7072
- if (err && err['name'] === 'InvalidStateError') {
7073
- // try reopening the DB if the connection is closed
7074
- this.dbPromise = this._openDb();
7075
- return this.dbPromise.then(doTransaction);
7076
- } else {
7077
- return PromisePolyfill.reject(err);
7078
- }
7079
- }.bind(this));
7080
- };
7081
-
7082
- IDBStorageWrapper.prototype.setItem = function (key, value) {
7083
- return this.makeTransaction('readwrite', function (objectStore) {
7084
- objectStore.put(value, key);
7085
- });
7086
- };
7087
-
7088
- IDBStorageWrapper.prototype.getItem = function (key) {
7089
- var req;
7090
- return this.makeTransaction('readonly', function (objectStore) {
7091
- req = objectStore.get(key);
7092
- }).then(function () {
7093
- return req.result;
7094
- });
7095
- };
7096
-
7097
- IDBStorageWrapper.prototype.removeItem = function (key) {
7098
- return this.makeTransaction('readwrite', function (objectStore) {
7099
- objectStore.delete(key);
7100
- });
7101
- };
7102
-
7103
- IDBStorageWrapper.prototype.getAll = function () {
7104
- var req;
7105
- return this.makeTransaction('readonly', function (objectStore) {
7106
- req = objectStore.getAll();
7107
- }).then(function () {
7108
- return req.result;
7109
- });
7110
- };
7111
-
7112
7443
  /* eslint camelcase: "off" */
7113
7444
 
7114
7445
  /*
@@ -7152,7 +7483,6 @@
7152
7483
  /** @const */ var SETTING_FALLBACK = 'fallback';
7153
7484
  /** @const */ var SETTING_DISABLED = 'disabled';
7154
7485
 
7155
-
7156
7486
  /*
7157
7487
  * Dynamic... constants? Is that an oxymoron?
7158
7488
  */
@@ -7237,19 +7567,24 @@
7237
7567
  'batch_request_timeout_ms': 90000,
7238
7568
  'batch_autostart': true,
7239
7569
  'hooks': {},
7570
+ 'record_allowed_iframe_origins': [],
7240
7571
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
7241
7572
  'record_block_selector': 'img, video, audio',
7242
7573
  'record_canvas': false,
7243
7574
  'record_collect_fonts': false,
7244
7575
  'record_console': true,
7245
7576
  'record_heatmap_data': false,
7577
+ 'recording_event_triggers': {},
7246
7578
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
7247
7579
  'record_mask_inputs': true,
7248
7580
  'record_max_ms': MAX_RECORDING_MS,
7249
7581
  'record_min_ms': 0,
7582
+ 'record_network': false,
7583
+ 'record_network_options': {},
7250
7584
  'record_sessions_percent': 0,
7251
- 'recorder_src': 'https://cdn.mxpnl.com/libs/mixpanel-recorder.min.js',
7252
- 'targeting_src': 'https://cdn.mxpnl.com/libs/mixpanel-targeting.min.js',
7585
+ 'recorder_src': null,
7586
+ 'targeting_src': null,
7587
+ 'lib_base_path': 'https://cdn.mxpnl.com/libs/',
7253
7588
  'remote_settings_mode': SETTING_DISABLED // 'strict', 'fallback', 'disabled'
7254
7589
  };
7255
7590
 
@@ -7403,6 +7738,19 @@
7403
7738
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
7404
7739
  }));
7405
7740
 
7741
+ this.recorderManager = new RecorderManager({
7742
+ mixpanelInstance: this,
7743
+ getConfigFunc: _.bind(this.get_config, this),
7744
+ setConfigFunc: _.bind(this.set_config, this),
7745
+ getTabIdFunc: _.bind(this.get_tab_id, this),
7746
+ reportErrorFunc: _.bind(this.report_error, this),
7747
+ getDistinctIdFunc: _.bind(this.get_distinct_id, this),
7748
+ recorderSrc: this.get_config('recorder_src'),
7749
+ targetingSrc: this.get_config('targeting_src'),
7750
+ libBasePath: this.get_config('lib_base_path'),
7751
+ loadExtraBundle: load_extra_bundle
7752
+ });
7753
+
7406
7754
  this['_jsc'] = NOOP_FUNC;
7407
7755
 
7408
7756
  this.__dom_loaded_queue = [];
@@ -7481,7 +7829,7 @@
7481
7829
  getPropertyFunc: _.bind(this.get_property, this),
7482
7830
  trackingFunc: _.bind(this.track, this),
7483
7831
  loadExtraBundle: load_extra_bundle,
7484
- targetingSrc: this.get_config('targeting_src')
7832
+ targetingSrc: this.get_config('targeting_src') || (this.get_config('lib_base_path') + TARGETING_FILENAME)
7485
7833
  });
7486
7834
  this.flags.init();
7487
7835
  this['flags'] = this.flags;
@@ -7494,11 +7842,11 @@
7494
7842
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
7495
7843
  var mode = this.get_config('remote_settings_mode');
7496
7844
  if (mode === SETTING_STRICT || mode === SETTING_FALLBACK) {
7497
- this._fetch_remote_settings(mode).then(_.bind(function() {
7498
- this._check_and_start_session_recording();
7845
+ this.__session_recording_init_promise = this._fetch_remote_settings(mode).then(_.bind(function() {
7846
+ return this._check_and_start_session_recording();
7499
7847
  }, this));
7500
7848
  } else {
7501
- this._check_and_start_session_recording();
7849
+ this.__session_recording_init_promise = this._check_and_start_session_recording();
7502
7850
  }
7503
7851
  };
7504
7852
 
@@ -7542,132 +7890,50 @@
7542
7890
  return this.tab_id || null;
7543
7891
  };
7544
7892
 
7545
- MixpanelLib.prototype._should_load_recorder = function () {
7546
- if (this.get_config('disable_persistence')) {
7547
- console.log('Load recorder check skipped due to disable_persistence config');
7548
- return Promise.resolve(false);
7549
- }
7550
-
7551
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
7552
- var tab_id = this.get_tab_id();
7553
- return recording_registry_idb.init()
7554
- .then(function () {
7555
- return recording_registry_idb.getAll();
7556
- })
7557
- .then(function (recordings) {
7558
- for (var i = 0; i < recordings.length; i++) {
7559
- // if there are expired recordings in the registry, we should load the recorder to flush them
7560
- // if there's a recording for this tab id, we should load the recorder to continue the recording
7561
- if (isRecordingExpired(recordings[i]) || recordings[i]['tabId'] === tab_id) {
7562
- return true;
7563
- }
7564
- }
7565
- return false;
7566
- })
7567
- .catch(_.bind(function (err) {
7568
- this.report_error('Error checking recording registry', err);
7569
- }, this));
7570
- };
7571
-
7572
7893
  MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpanelLib(function(force_start) {
7573
- if (!win['MutationObserver']) {
7574
- console.critical('Browser does not support MutationObserver; skipping session recording');
7575
- return;
7576
- }
7577
-
7578
- var loadRecorder = _.bind(function(startNewIfInactive) {
7579
- var handleLoadedRecorder = _.bind(function() {
7580
- this._recorder = this._recorder || new win[RECORDER_GLOBAL_NAME](this);
7581
- this._recorder['resumeRecording'](startNewIfInactive);
7582
- }, this);
7583
-
7584
- if (_.isUndefined(win[RECORDER_GLOBAL_NAME])) {
7585
- load_extra_bundle(this.get_config('recorder_src'), handleLoadedRecorder);
7586
- } else {
7587
- handleLoadedRecorder();
7588
- }
7589
- }, this);
7590
-
7591
- /**
7592
- * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
7593
- * 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.
7594
- */
7595
- var is_sampled = this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent');
7596
- if (force_start || is_sampled) {
7597
- loadRecorder(true);
7598
- } else {
7599
- this._should_load_recorder()
7600
- .then(function (shouldLoad) {
7601
- if (shouldLoad) {
7602
- loadRecorder(false);
7603
- }
7604
- });
7605
- }
7894
+ return this.recorderManager.checkAndStartSessionRecording(force_start);
7606
7895
  });
7607
7896
 
7897
+ MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
7898
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
7899
+ };
7900
+
7608
7901
  MixpanelLib.prototype.start_session_recording = function () {
7609
- this._check_and_start_session_recording(true);
7902
+ return this._check_and_start_session_recording(true);
7610
7903
  };
7611
7904
 
7612
7905
  MixpanelLib.prototype.stop_session_recording = function () {
7613
- if (this._recorder) {
7614
- return this._recorder['stopRecording']();
7615
- }
7616
- return Promise.resolve();
7906
+ return this.recorderManager.stopSessionRecording();
7617
7907
  };
7618
7908
 
7619
7909
  MixpanelLib.prototype.pause_session_recording = function () {
7620
- if (this._recorder) {
7621
- return this._recorder['pauseRecording']();
7622
- }
7623
- return Promise.resolve();
7910
+ return this.recorderManager.pauseSessionRecording();
7624
7911
  };
7625
7912
 
7626
7913
  MixpanelLib.prototype.resume_session_recording = function () {
7627
- if (this._recorder) {
7628
- return this._recorder['resumeRecording']();
7629
- }
7630
- return Promise.resolve();
7914
+ return this.recorderManager.resumeSessionRecording();
7631
7915
  };
7632
7916
 
7633
7917
  MixpanelLib.prototype.is_recording_heatmap_data = function () {
7634
- return this._get_session_replay_id() && this.get_config('record_heatmap_data');
7918
+ return this.recorderManager.isRecordingHeatmapData();
7635
7919
  };
7636
7920
 
7637
7921
  MixpanelLib.prototype.get_session_recording_properties = function () {
7638
- var props = {};
7639
- var replay_id = this._get_session_replay_id();
7640
- if (replay_id) {
7641
- props['$mp_replay_id'] = replay_id;
7642
- }
7643
- return props;
7922
+ return this.recorderManager.getSessionRecordingProperties();
7644
7923
  };
7645
7924
 
7646
7925
  MixpanelLib.prototype.get_session_replay_url = function () {
7647
- var replay_url = null;
7648
- var replay_id = this._get_session_replay_id();
7649
- if (replay_id) {
7650
- var query_params = _.HTTPBuildQuery({
7651
- 'replay_id': replay_id,
7652
- 'distinct_id': this.get_distinct_id(),
7653
- 'token': this.get_config('token')
7654
- });
7655
- replay_url = 'https://mixpanel.com/projects/replay-redirect?' + query_params;
7656
- }
7657
- return replay_url;
7658
- };
7659
-
7660
- MixpanelLib.prototype._get_session_replay_id = function () {
7661
- var replay_id = null;
7662
- if (this._recorder) {
7663
- replay_id = this._recorder['replayId'];
7664
- }
7665
- return replay_id || null;
7926
+ return this.recorderManager.getSessionReplayUrl();
7666
7927
  };
7667
7928
 
7668
7929
  // "private" public method to reach into the recorder in test cases
7669
7930
  MixpanelLib.prototype.__get_recorder = function () {
7670
- return this._recorder;
7931
+ return this.recorderManager.getRecorder();
7932
+ };
7933
+
7934
+ // "private" public method to get session recording init promise in test cases
7935
+ MixpanelLib.prototype.__get_recording_init_promise = function () {
7936
+ return this.__session_recording_init_promise;
7671
7937
  };
7672
7938
 
7673
7939
  // Private methods
@@ -7925,6 +8191,7 @@
7925
8191
  };
7926
8192
 
7927
8193
  MixpanelLib.prototype._fetch_remote_settings = function(mode) {
8194
+ var self = this;
7928
8195
  var disableRecordingIfStrict = function() {
7929
8196
  if (mode === 'strict') {
7930
8197
  self.set_config({'record_sessions_percent': 0});
@@ -7945,7 +8212,6 @@
7945
8212
  };
7946
8213
  var query_string = _.HTTPBuildQuery(request_params);
7947
8214
  var full_url = settings_endpoint + '?' + query_string;
7948
- var self = this;
7949
8215
 
7950
8216
  var abortController = new AbortController();
7951
8217
  var timeout_id = setTimeout(function() {
@@ -8137,6 +8403,34 @@
8137
8403
  this._execute_array([item]);
8138
8404
  };
8139
8405
 
8406
+ /**
8407
+ * Enables events on the Mixpanel object. If passed no arguments,
8408
+ * this function enable tracking of all events. If passed an
8409
+ * array of event names, those events will be enabled, but other
8410
+ * existing disabled events will continue to be not tracked.
8411
+ *
8412
+ * @param {Array} [events] An array of event names to enable
8413
+ */
8414
+ MixpanelLib.prototype.enable = function(events) {
8415
+ var keys, new_disabled_events, i, j;
8416
+
8417
+ if (typeof(events) === 'undefined') {
8418
+ this._flags.disable_all_events = false;
8419
+ } else {
8420
+ keys = {};
8421
+ new_disabled_events = [];
8422
+ for (i = 0; i < events.length; i++) {
8423
+ keys[events[i]] = true;
8424
+ }
8425
+ for (j = 0; j < this.__disabled_events.length; j++) {
8426
+ if (!keys[this.__disabled_events[j]]) {
8427
+ new_disabled_events.push(this.__disabled_events[j]);
8428
+ }
8429
+ }
8430
+ this.__disabled_events = new_disabled_events;
8431
+ }
8432
+ };
8433
+
8140
8434
  /**
8141
8435
  * Disable events on the Mixpanel object. If passed no arguments,
8142
8436
  * this function disables tracking of any event. If passed an
@@ -8310,6 +8604,8 @@
8310
8604
  this.report_error('Invalid value for property_blacklist config: ' + property_blacklist);
8311
8605
  }
8312
8606
 
8607
+ this._start_recording_on_event(event_name, properties);
8608
+
8313
8609
  var data = {
8314
8610
  'event': event_name,
8315
8611
  'properties': properties
@@ -9518,6 +9814,7 @@
9518
9814
  // MixpanelLib Exports
9519
9815
  MixpanelLib.prototype['init'] = MixpanelLib.prototype.init;
9520
9816
  MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset;
9817
+ MixpanelLib.prototype['enable'] = MixpanelLib.prototype.enable;
9521
9818
  MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable;
9522
9819
  MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event;
9523
9820
  MixpanelLib.prototype['track'] = MixpanelLib.prototype.track;
@@ -9561,6 +9858,7 @@
9561
9858
 
9562
9859
  // Exports intended only for testing
9563
9860
  MixpanelLib.prototype['__get_recorder'] = MixpanelLib.prototype.__get_recorder;
9861
+ MixpanelLib.prototype['__get_recording_init_promise'] = MixpanelLib.prototype.__get_recording_init_promise;
9564
9862
 
9565
9863
  // MixpanelPersistence Exports
9566
9864
  MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties;