mixpanel-browser 2.78.0 → 2.79.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 (66) hide show
  1. package/.claude/settings.local.json +6 -11
  2. package/.eslintrc.json +12 -0
  3. package/.github/workflows/openfeature-provider-tests.yml +31 -0
  4. package/CHANGELOG.md +8 -1
  5. package/build.sh +2 -2
  6. package/dist/async-modules/{mixpanel-recorder-BjSlYaNJ.min.js → mixpanel-recorder-D5HJyV2E.min.js} +2 -2
  7. package/dist/async-modules/mixpanel-recorder-D5HJyV2E.min.js.map +1 -0
  8. package/dist/async-modules/{mixpanel-recorder-zMBXIyeG.js → mixpanel-recorder-P6SEnnPV.js} +57 -33
  9. package/dist/async-modules/mixpanel-targeting-1L9FyetZ.min.js +2 -0
  10. package/dist/async-modules/mixpanel-targeting-1L9FyetZ.min.js.map +1 -0
  11. package/dist/async-modules/{mixpanel-targeting-UHf4eBfC.js → mixpanel-targeting-BBMVbgJF.js} +24 -13
  12. package/dist/mixpanel-core.cjs.d.ts +45 -1
  13. package/dist/mixpanel-core.cjs.js +565 -197
  14. package/dist/mixpanel-recorder.js +57 -33
  15. package/dist/mixpanel-recorder.min.js +1 -1
  16. package/dist/mixpanel-recorder.min.js.map +1 -1
  17. package/dist/mixpanel-targeting.js +24 -13
  18. package/dist/mixpanel-targeting.min.js +1 -1
  19. package/dist/mixpanel-targeting.min.js.map +1 -1
  20. package/dist/mixpanel-with-async-modules.cjs.d.ts +45 -1
  21. package/dist/mixpanel-with-async-modules.cjs.js +567 -199
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +45 -1
  23. package/dist/mixpanel-with-async-recorder.cjs.js +567 -199
  24. package/dist/mixpanel-with-recorder.d.ts +45 -1
  25. package/dist/mixpanel-with-recorder.js +490 -122
  26. package/dist/mixpanel-with-recorder.min.d.ts +45 -1
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +45 -1
  29. package/dist/mixpanel.amd.js +490 -122
  30. package/dist/mixpanel.cjs.d.ts +45 -1
  31. package/dist/mixpanel.cjs.js +490 -122
  32. package/dist/mixpanel.globals.js +567 -199
  33. package/dist/mixpanel.min.js +199 -189
  34. package/dist/mixpanel.module.d.ts +45 -1
  35. package/dist/mixpanel.module.js +490 -122
  36. package/dist/mixpanel.umd.d.ts +45 -1
  37. package/dist/mixpanel.umd.js +490 -122
  38. package/package.json +1 -1
  39. package/packages/openfeature-web-provider/README.md +357 -0
  40. package/packages/openfeature-web-provider/package-lock.json +1636 -0
  41. package/packages/openfeature-web-provider/package.json +51 -0
  42. package/packages/openfeature-web-provider/rollup.config.browser.mjs +26 -0
  43. package/packages/openfeature-web-provider/src/MixpanelProvider.ts +302 -0
  44. package/packages/openfeature-web-provider/src/index.ts +1 -0
  45. package/packages/openfeature-web-provider/src/types.ts +72 -0
  46. package/packages/openfeature-web-provider/test/MixpanelProvider.spec.ts +484 -0
  47. package/packages/openfeature-web-provider/tsconfig.json +15 -0
  48. package/src/autocapture/index.js +7 -2
  49. package/src/config.js +1 -1
  50. package/src/flags/flags-persistence.js +176 -0
  51. package/src/flags/index.js +174 -23
  52. package/src/index.d.ts +45 -1
  53. package/src/mixpanel-core.js +24 -7
  54. package/src/recorder/idb-config.js +16 -0
  55. package/src/recorder/recording-registry.js +7 -2
  56. package/src/recorder/session-recording.js +9 -4
  57. package/src/recorder-manager.js +7 -2
  58. package/src/request-queue.js +1 -2
  59. package/src/shared-lock.js +2 -3
  60. package/src/storage/indexed-db.js +16 -15
  61. package/src/storage/local-storage.js +5 -3
  62. package/src/utils.js +25 -12
  63. package/tsconfig.base.json +9 -0
  64. package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js.map +0 -1
  65. package/dist/async-modules/mixpanel-targeting-BSHal4N9.min.js +0 -2
  66. package/dist/async-modules/mixpanel-targeting-BSHal4N9.min.js.map +0 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  var Config = {
4
4
  DEBUG: false,
5
- LIB_VERSION: '2.78.0'
5
+ LIB_VERSION: '2.79.0'
6
6
  };
7
7
 
8
8
  // Window global names for async modules
@@ -10,8 +10,8 @@ var TARGETING_GLOBAL_NAME = '__mp_targeting';
10
10
  var RECORDER_GLOBAL_NAME = '__mp_recorder';
11
11
 
12
12
  // Constants that are injected at build-time for the names of async modules.
13
- var RECORDER_FILENAME = 'mixpanel-recorder-zMBXIyeG.js';
14
- var TARGETING_FILENAME = 'mixpanel-targeting-UHf4eBfC.js';
13
+ var RECORDER_FILENAME = 'mixpanel-recorder-P6SEnnPV.js';
14
+ var TARGETING_FILENAME = 'mixpanel-targeting-BBMVbgJF.js';
15
15
 
16
16
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
17
17
  var win;
@@ -501,6 +501,7 @@ var log_func_with_prefix = function(func, prefix) {
501
501
  var console_with_prefix = function(prefix) {
502
502
  return {
503
503
  log: log_func_with_prefix(console.log, prefix),
504
+ warn: log_func_with_prefix(console.warn, prefix),
504
505
  error: log_func_with_prefix(console.error, prefix),
505
506
  critical: log_func_with_prefix(console.critical, prefix)
506
507
  };
@@ -1447,7 +1448,8 @@ var localStorageSupported = function(storage, forceCheck) {
1447
1448
  if (_localStorageSupported !== null && !forceCheck) {
1448
1449
  return _localStorageSupported;
1449
1450
  }
1450
- return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
1451
+
1452
+ return _localStorageSupported = _testStorageSupported(storage);
1451
1453
  };
1452
1454
 
1453
1455
  var _sessionStorageSupported = null;
@@ -1455,7 +1457,8 @@ var sessionStorageSupported = function(storage, forceCheck) {
1455
1457
  if (_sessionStorageSupported !== null && !forceCheck) {
1456
1458
  return _sessionStorageSupported;
1457
1459
  }
1458
- return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
1460
+
1461
+ return _sessionStorageSupported = _testStorageSupported(storage);
1459
1462
  };
1460
1463
 
1461
1464
  function _storageWrapper(storage, name, is_supported_fn) {
@@ -1505,17 +1508,26 @@ function _storageWrapper(storage, name, is_supported_fn) {
1505
1508
  };
1506
1509
  }
1507
1510
 
1508
- // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1509
- // so create dummy storage wrappers that silently fail as a fallback.
1510
- var windowLocalStorage = null, windowSessionStorage = null;
1511
- try {
1512
- windowLocalStorage = win.localStorage;
1513
- windowSessionStorage = win.sessionStorage;
1514
- // eslint-disable-next-line no-empty
1515
- } catch (_err) {}
1511
+ // Safari and other browsers may error out accessing localStorage/sessionStorage
1512
+ // when cookies are disabled, so wrap access in a try-catch.
1513
+ var getLocalStorage = function() {
1514
+ try {
1515
+ return win.localStorage; // eslint-disable-line no-restricted-properties
1516
+ } catch (_err) {
1517
+ return null;
1518
+ }
1519
+ };
1516
1520
 
1517
- _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1518
- _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1521
+ var getSessionStorage = function() {
1522
+ try {
1523
+ return win.sessionStorage; // eslint-disable-line no-restricted-properties
1524
+ } catch (_err) {
1525
+ return null;
1526
+ }
1527
+ };
1528
+
1529
+ _.localStorage = _storageWrapper(getLocalStorage(), 'localStorage', localStorageSupported);
1530
+ _.sessionStorage = _storageWrapper(getSessionStorage(), 'sessionStorage', sessionStorageSupported);
1519
1531
 
1520
1532
  _.register_event = (function() {
1521
1533
  // written by Dean Edwards, 2005
@@ -2267,7 +2279,7 @@ var EVENT_HANDLER_ATTRIBUTES = [
2267
2279
 
2268
2280
  var MAX_DEPTH = 5;
2269
2281
 
2270
- var logger$5 = console_with_prefix('autocapture');
2282
+ var logger$6 = console_with_prefix('autocapture');
2271
2283
 
2272
2284
 
2273
2285
  function getClasses(el) {
@@ -2531,7 +2543,7 @@ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
2531
2543
  return false;
2532
2544
  }
2533
2545
  } catch (err) {
2534
- logger$5.critical('Error while checking element in allowElementCallback', err);
2546
+ logger$6.critical('Error while checking element in allowElementCallback', err);
2535
2547
  return false;
2536
2548
  }
2537
2549
  }
@@ -2548,7 +2560,7 @@ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
2548
2560
  return true;
2549
2561
  }
2550
2562
  } catch (err) {
2551
- logger$5.critical('Error while checking selector: ' + sel, err);
2563
+ logger$6.critical('Error while checking selector: ' + sel, err);
2552
2564
  }
2553
2565
  }
2554
2566
  return false;
@@ -2563,7 +2575,7 @@ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
2563
2575
  return true;
2564
2576
  }
2565
2577
  } catch (err) {
2566
- logger$5.critical('Error while checking element in blockElementCallback', err);
2578
+ logger$6.critical('Error while checking element in blockElementCallback', err);
2567
2579
  return true;
2568
2580
  }
2569
2581
  }
@@ -2577,7 +2589,7 @@ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
2577
2589
  return true;
2578
2590
  }
2579
2591
  } catch (err) {
2580
- logger$5.critical('Error while checking selector: ' + sel, err);
2592
+ logger$6.critical('Error while checking selector: ' + sel, err);
2581
2593
  }
2582
2594
  }
2583
2595
  }
@@ -3041,7 +3053,7 @@ ShadowDOMObserver.prototype.observeShadowRoot = function(shadowRoot) {
3041
3053
  observer.observe(shadowRoot, this.observerConfig);
3042
3054
  this.shadowObservers.push(observer);
3043
3055
  } catch (e) {
3044
- logger$5.critical('Error while observing shadow root', e);
3056
+ logger$6.critical('Error while observing shadow root', e);
3045
3057
  }
3046
3058
  };
3047
3059
 
@@ -3052,7 +3064,7 @@ ShadowDOMObserver.prototype.start = function() {
3052
3064
  }
3053
3065
 
3054
3066
  if (!weakSetSupported()) {
3055
- logger$5.critical('Shadow DOM observation unavailable: WeakSet not supported');
3067
+ logger$6.critical('Shadow DOM observation unavailable: WeakSet not supported');
3056
3068
  return;
3057
3069
  }
3058
3070
 
@@ -3068,7 +3080,7 @@ ShadowDOMObserver.prototype.stop = function() {
3068
3080
  try {
3069
3081
  this.shadowObservers[i].disconnect();
3070
3082
  } catch (e) {
3071
- logger$5.critical('Error while disconnecting shadow DOM observer', e);
3083
+ logger$6.critical('Error while disconnecting shadow DOM observer', e);
3072
3084
  }
3073
3085
  }
3074
3086
  this.shadowObservers = [];
@@ -3256,7 +3268,7 @@ DeadClickTracker.prototype.startTracking = function() {
3256
3268
 
3257
3269
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
3258
3270
  } catch (e) {
3259
- logger$5.critical('Error while setting up mutation observer', e);
3271
+ logger$6.critical('Error while setting up mutation observer', e);
3260
3272
  }
3261
3273
  }
3262
3274
 
@@ -3271,7 +3283,7 @@ DeadClickTracker.prototype.startTracking = function() {
3271
3283
  );
3272
3284
  this.shadowDOMObserver.start();
3273
3285
  } catch (e) {
3274
- logger$5.critical('Error while setting up shadow DOM observer', e);
3286
+ logger$6.critical('Error while setting up shadow DOM observer', e);
3275
3287
  this.shadowDOMObserver = null;
3276
3288
  }
3277
3289
  }
@@ -3298,7 +3310,7 @@ DeadClickTracker.prototype.stopTracking = function() {
3298
3310
  try {
3299
3311
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
3300
3312
  } catch (e) {
3301
- logger$5.critical('Error while removing event listener', e);
3313
+ logger$6.critical('Error while removing event listener', e);
3302
3314
  }
3303
3315
  }
3304
3316
  this.eventListeners = [];
@@ -3307,7 +3319,7 @@ DeadClickTracker.prototype.stopTracking = function() {
3307
3319
  try {
3308
3320
  this.mutationObserver.disconnect();
3309
3321
  } catch (e) {
3310
- logger$5.critical('Error while disconnecting mutation observer', e);
3322
+ logger$6.critical('Error while disconnecting mutation observer', e);
3311
3323
  }
3312
3324
  this.mutationObserver = null;
3313
3325
  }
@@ -3316,7 +3328,7 @@ DeadClickTracker.prototype.stopTracking = function() {
3316
3328
  try {
3317
3329
  this.shadowDOMObserver.stop();
3318
3330
  } catch (e) {
3319
- logger$5.critical('Error while stopping shadow DOM observer', e);
3331
+ logger$6.critical('Error while stopping shadow DOM observer', e);
3320
3332
  }
3321
3333
  this.shadowDOMObserver = null;
3322
3334
  }
@@ -3394,7 +3406,7 @@ var Autocapture = function(mp) {
3394
3406
 
3395
3407
  Autocapture.prototype.init = function() {
3396
3408
  if (!minDOMApisSupported()) {
3397
- logger$5.critical('Autocapture unavailable: missing required DOM APIs');
3409
+ logger$6.critical('Autocapture unavailable: missing required DOM APIs');
3398
3410
  return;
3399
3411
  }
3400
3412
  this.initPageListeners();
@@ -3434,7 +3446,7 @@ Autocapture.prototype.currentUrlBlocked = function() {
3434
3446
  try {
3435
3447
  return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
3436
3448
  } catch (err) {
3437
- logger$5.critical('Error while checking block URL regexes: ', err);
3449
+ logger$6.critical('Error while checking block URL regexes: ', err);
3438
3450
  return true;
3439
3451
  }
3440
3452
  }
@@ -3447,7 +3459,7 @@ Autocapture.prototype.currentUrlBlocked = function() {
3447
3459
  try {
3448
3460
  return urlMatchesRegexList(currentUrl, blockUrlRegexes);
3449
3461
  } catch (err) {
3450
- logger$5.critical('Error while checking block URL regexes: ', err);
3462
+ logger$6.critical('Error while checking block URL regexes: ', err);
3451
3463
  return true;
3452
3464
  }
3453
3465
  };
@@ -3585,7 +3597,7 @@ Autocapture.prototype._initScrollDepthTracking = function() {
3585
3597
  return;
3586
3598
  }
3587
3599
 
3588
- logger$5.log('Initializing scroll depth tracking');
3600
+ logger$6.log('Initializing scroll depth tracking');
3589
3601
 
3590
3602
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3591
3603
 
@@ -3611,7 +3623,7 @@ Autocapture.prototype.initClickTracking = function() {
3611
3623
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
3612
3624
  return;
3613
3625
  }
3614
- logger$5.log('Initializing click tracking');
3626
+ logger$6.log('Initializing click tracking');
3615
3627
 
3616
3628
  this.listenerClick = function(ev) {
3617
3629
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -3630,7 +3642,7 @@ Autocapture.prototype.initDeadClickTracking = function() {
3630
3642
  return;
3631
3643
  }
3632
3644
 
3633
- logger$5.log('Initializing dead click tracking');
3645
+ logger$6.log('Initializing dead click tracking');
3634
3646
  if (!this._deadClickTracker) {
3635
3647
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
3636
3648
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -3664,7 +3676,7 @@ Autocapture.prototype.initInputTracking = function() {
3664
3676
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
3665
3677
  return;
3666
3678
  }
3667
- logger$5.log('Initializing input tracking');
3679
+ logger$6.log('Initializing input tracking');
3668
3680
 
3669
3681
  this.listenerChange = function(ev) {
3670
3682
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -3678,14 +3690,15 @@ Autocapture.prototype.initInputTracking = function() {
3678
3690
  Autocapture.prototype.initPageviewTracking = function() {
3679
3691
  win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
3680
3692
 
3681
- if (!this.pageviewTrackingConfig()) {
3693
+ if (!this.pageviewTrackingConfig() && !this.mp.get_config('record_heatmap_data')) {
3682
3694
  return;
3683
3695
  }
3684
- logger$5.log('Initializing pageview tracking');
3696
+ logger$6.log('Initializing pageview tracking');
3685
3697
 
3686
3698
  var previousTrackedUrl = '';
3687
3699
  var tracked = false;
3688
- if (!this.currentUrlBlocked()) {
3700
+ // Track initial pageview if pageview tracking enabled OR heatmap recording is active
3701
+ if ((this.pageviewTrackingConfig() || this.mp.is_recording_heatmap_data()) && !this.currentUrlBlocked()) {
3689
3702
  tracked = this.mp.track_pageview(DEFAULT_PROPS);
3690
3703
  }
3691
3704
  if (tracked) {
@@ -3701,6 +3714,10 @@ Autocapture.prototype.initPageviewTracking = function() {
3701
3714
  var shouldTrack = false;
3702
3715
  var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
3703
3716
  var trackPageviewOption = this.pageviewTrackingConfig();
3717
+ if (!trackPageviewOption && this.mp.is_recording_heatmap_data()) {
3718
+ trackPageviewOption = PAGEVIEW_OPTION_FULL_URL;
3719
+ }
3720
+
3704
3721
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
3705
3722
  shouldTrack = currentUrl !== previousTrackedUrl;
3706
3723
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
@@ -3716,7 +3733,7 @@ Autocapture.prototype.initPageviewTracking = function() {
3716
3733
  }
3717
3734
  if (didPathChange) {
3718
3735
  this.lastScrollCheckpoint = 0;
3719
- logger$5.log('Path change: re-initializing scroll depth checkpoints');
3736
+ logger$6.log('Path change: re-initializing scroll depth checkpoints');
3720
3737
  }
3721
3738
  }
3722
3739
  }.bind(this));
@@ -3731,7 +3748,7 @@ Autocapture.prototype.initRageClickTracking = function() {
3731
3748
  return;
3732
3749
  }
3733
3750
 
3734
- logger$5.log('Initializing rage click tracking');
3751
+ logger$6.log('Initializing rage click tracking');
3735
3752
  if (!this._rageClickTracker) {
3736
3753
  this._rageClickTracker = new RageClickTracker();
3737
3754
  }
@@ -3761,7 +3778,7 @@ Autocapture.prototype.initScrollTracking = function() {
3761
3778
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3762
3779
  return;
3763
3780
  }
3764
- logger$5.log('Initializing scroll tracking');
3781
+ logger$6.log('Initializing scroll tracking');
3765
3782
  this.lastScrollCheckpoint = 0;
3766
3783
 
3767
3784
  var scrollTrackFunction = function() {
@@ -3798,7 +3815,7 @@ Autocapture.prototype.initScrollTracking = function() {
3798
3815
  }
3799
3816
  }
3800
3817
  } catch (err) {
3801
- logger$5.critical('Error while calculating scroll percentage', err);
3818
+ logger$6.critical('Error while calculating scroll percentage', err);
3802
3819
  }
3803
3820
  if (shouldTrack) {
3804
3821
  this.mp.track(MP_EV_SCROLL, props);
@@ -3816,7 +3833,7 @@ Autocapture.prototype.initSubmitTracking = function() {
3816
3833
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
3817
3834
  return;
3818
3835
  }
3819
- logger$5.log('Initializing submit tracking');
3836
+ logger$6.log('Initializing submit tracking');
3820
3837
 
3821
3838
  this.listenerSubmit = function(ev) {
3822
3839
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -3838,7 +3855,7 @@ Autocapture.prototype.initPageLeaveTracking = function() {
3838
3855
  return;
3839
3856
  }
3840
3857
 
3841
- logger$5.log('Initializing page visibility tracking.');
3858
+ logger$6.log('Initializing page visibility tracking.');
3842
3859
  this._initScrollDepthTracking();
3843
3860
  var previousTrackedUrl = _.info.currentUrl();
3844
3861
 
@@ -3923,10 +3940,309 @@ var getTargetingPromise = function(loadExtraBundle, targetingSrc) {
3923
3940
  return win[TARGETING_GLOBAL_NAME];
3924
3941
  };
3925
3942
 
3943
+ /**
3944
+ * @type {import('./wrapper').StorageWrapper}
3945
+ */
3946
+ var IDBStorageWrapper = function (dbName, storeName, versionData) {
3947
+ this.dbName = dbName;
3948
+ this.storeName = storeName;
3949
+ this.version = versionData.version;
3950
+ this.storeNamesInDb = versionData.storeNames;
3951
+ /**
3952
+ * @type {Promise<IDBDatabase>|null}
3953
+ */
3954
+ this.dbPromise = null;
3955
+ };
3956
+
3957
+ IDBStorageWrapper.prototype._openDb = function () {
3958
+ var dbName = this.dbName;
3959
+ var version = this.version;
3960
+ var storeNamesInDb = this.storeNamesInDb;
3961
+ return new PromisePolyfill(function (resolve, reject) {
3962
+ var openRequest = win.indexedDB.open(dbName, version);
3963
+ openRequest['onerror'] = function () {
3964
+ reject(openRequest.error);
3965
+ };
3966
+
3967
+ openRequest['onsuccess'] = function () {
3968
+ resolve(openRequest.result);
3969
+ };
3970
+
3971
+ openRequest['onupgradeneeded'] = function (ev) {
3972
+ var db = ev.target.result;
3973
+
3974
+ storeNamesInDb.forEach(function (storeName) {
3975
+ if (!db.objectStoreNames.contains(storeName)) {
3976
+ db.createObjectStore(storeName);
3977
+ }
3978
+ });
3979
+ };
3980
+ });
3981
+ };
3982
+
3983
+ IDBStorageWrapper.prototype.init = function () {
3984
+ if (!win.indexedDB) {
3985
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
3986
+ }
3987
+
3988
+ if (!this.dbPromise) {
3989
+ this.dbPromise = this._openDb();
3990
+ }
3991
+
3992
+ return this.dbPromise
3993
+ .then(function (dbOrError) {
3994
+ if (dbOrError instanceof win['IDBDatabase']) {
3995
+ return PromisePolyfill.resolve();
3996
+ } else {
3997
+ return PromisePolyfill.reject(dbOrError);
3998
+ }
3999
+ });
4000
+ };
4001
+
4002
+ IDBStorageWrapper.prototype.isInitialized = function () {
4003
+ return !!this.dbPromise;
4004
+ };
4005
+
4006
+ /**
4007
+ * @param {IDBTransactionMode} mode
4008
+ * @param {function(IDBObjectStore): void} storeCb
4009
+ */
4010
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
4011
+ var storeName = this.storeName;
4012
+ var doTransaction = function (db) {
4013
+ return new PromisePolyfill(function (resolve, reject) {
4014
+ var transaction = db.transaction(storeName, mode);
4015
+ transaction.oncomplete = function () {
4016
+ resolve(transaction);
4017
+ };
4018
+ transaction.onabort = transaction.onerror = function () {
4019
+ reject(transaction.error);
4020
+ };
4021
+
4022
+ storeCb(transaction.objectStore(storeName));
4023
+ });
4024
+ };
4025
+
4026
+ return this.dbPromise
4027
+ .then(doTransaction)
4028
+ .catch(function (err) {
4029
+ if (err && err['name'] === 'InvalidStateError') {
4030
+ // try reopening the DB if the connection is closed
4031
+ this.dbPromise = this._openDb();
4032
+ return this.dbPromise.then(doTransaction);
4033
+ } else {
4034
+ return PromisePolyfill.reject(err);
4035
+ }
4036
+ }.bind(this));
4037
+ };
4038
+
4039
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
4040
+ return this.makeTransaction('readwrite', function (objectStore) {
4041
+ objectStore.put(value, key);
4042
+ });
4043
+ };
4044
+
4045
+ IDBStorageWrapper.prototype.getItem = function (key) {
4046
+ var req;
4047
+ return this.makeTransaction('readonly', function (objectStore) {
4048
+ req = objectStore.get(key);
4049
+ }).then(function () {
4050
+ return req.result;
4051
+ });
4052
+ };
4053
+
4054
+ IDBStorageWrapper.prototype.removeItem = function (key) {
4055
+ return this.makeTransaction('readwrite', function (objectStore) {
4056
+ objectStore.delete(key);
4057
+ });
4058
+ };
4059
+
4060
+ IDBStorageWrapper.prototype.getAll = function () {
4061
+ var req;
4062
+ return this.makeTransaction('readonly', function (objectStore) {
4063
+ req = objectStore.getAll();
4064
+ }).then(function () {
4065
+ return req.result;
4066
+ });
4067
+ };
4068
+
4069
+ var logger$5 = console_with_prefix('flags');
4070
+
4071
+ var MIXPANEL_FLAGS_DB_NAME = 'mixpanelFlagsDb';
4072
+ var FLAGS_STORE_NAME = 'mixpanelFlags';
4073
+
4074
+ // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
4075
+ var FLAGS_VERSION_DATA = { version: 1, storeNames: [FLAGS_STORE_NAME] };
4076
+
4077
+ var PERSISTED_VARIANTS_KEY_PREFIX = 'persisted_variants_for_';
4078
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
4079
+
4080
+ var VariantLookupPolicy = Object.freeze({
4081
+ NETWORK_ONLY: 'networkOnly',
4082
+ NETWORK_FIRST: 'networkFirst',
4083
+ PERSISTENCE_UNTIL_NETWORK_SUCCESS: 'persistenceUntilNetworkSuccess'
4084
+ });
4085
+
4086
+ var VALID_POLICIES = [
4087
+ VariantLookupPolicy.NETWORK_ONLY,
4088
+ VariantLookupPolicy.NETWORK_FIRST,
4089
+ VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS
4090
+ ];
4091
+
4092
+ /**
4093
+ * Module for handling the storage and retrieval of persisted feature flag variants.
4094
+ */
4095
+ var FeatureFlagPersistence = function(persistenceConfig, token, isGloballyDisabled) {
4096
+ this.idb = new IDBStorageWrapper(MIXPANEL_FLAGS_DB_NAME, FLAGS_STORE_NAME, FLAGS_VERSION_DATA);
4097
+ this.persistenceConfig = persistenceConfig;
4098
+ this.persistedVariantsKey = PERSISTED_VARIANTS_KEY_PREFIX + token;
4099
+ this.isGloballyDisabled = isGloballyDisabled || function() { return false; };
4100
+ };
4101
+
4102
+ FeatureFlagPersistence.prototype.getPolicy = function() {
4103
+ if (this.isGloballyDisabled() || !this._isConfigValid()) {
4104
+ return VariantLookupPolicy.NETWORK_ONLY;
4105
+ }
4106
+ return this.persistenceConfig['variantLookupPolicy'];
4107
+ };
4108
+
4109
+ FeatureFlagPersistence.prototype.getTtlMs = function() {
4110
+ if (!this._isConfigValid()) {
4111
+ return DEFAULT_TTL_MS;
4112
+ }
4113
+ var configuredTtl = this.persistenceConfig['persistenceTtlMs'];
4114
+ return (configuredTtl === undefined || configuredTtl === null) ? DEFAULT_TTL_MS : configuredTtl;
4115
+ };
4116
+
4117
+ FeatureFlagPersistence.prototype._isConfigValid = function() {
4118
+ var config = this.persistenceConfig;
4119
+ if (!config) {
4120
+ return false;
4121
+ }
4122
+
4123
+ if (VALID_POLICIES.indexOf(config['variantLookupPolicy']) === -1) {
4124
+ logger$5.error('Invalid variantLookupPolicy:', config['variantLookupPolicy']);
4125
+ return false;
4126
+ }
4127
+
4128
+ if (config['persistenceTtlMs'] !== undefined &&
4129
+ config['persistenceTtlMs'] !== null &&
4130
+ config['persistenceTtlMs'] <= 0) {
4131
+ logger$5.error('If provided, persistenceTtlMs must be a positive number. Provided value:', config['persistenceTtlMs']);
4132
+ return false;
4133
+ }
4134
+
4135
+ return true;
4136
+ };
4137
+
4138
+ FeatureFlagPersistence.prototype.loadFlagsFromStorage = function(context) {
4139
+ var clearAndReturnNull = _.bind(function() {
4140
+ return this.clear().then(function() { return null; }).catch(function() { return null; });
4141
+ }, this);
4142
+
4143
+ if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
4144
+ return clearAndReturnNull();
4145
+ }
4146
+
4147
+ var ttlMs = this.getTtlMs();
4148
+
4149
+ return this.idb.init().then(_.bind(function() {
4150
+ return this.idb.getItem(this.persistedVariantsKey);
4151
+ }, this)).then(_.bind(function(data) {
4152
+ if (!data) {
4153
+ logger$5.log('No persisted variants found in IndexedDB');
4154
+ return null;
4155
+ }
4156
+
4157
+ if (ttlMs && Date.now() - data['persistedAt'] >= ttlMs) {
4158
+ logger$5.log('Persisted variants are expiring');
4159
+ return null;
4160
+ }
4161
+
4162
+ if (!context || data['distinctId'] !== context['distinct_id']) {
4163
+ logger$5.log('Persisted variants found, but for a different distinct_id so clearing.');
4164
+ return clearAndReturnNull();
4165
+ }
4166
+
4167
+ var persistedFlags = new Map();
4168
+ _.each(data['flagVariants'], function(variantData, key) {
4169
+ persistedFlags.set(key, {
4170
+ 'key': variantData['variant_key'],
4171
+ 'value': variantData['variant_value'],
4172
+ 'experiment_id': variantData['experiment_id'],
4173
+ 'is_experiment_active': variantData['is_experiment_active'],
4174
+ 'is_qa_tester': variantData['is_qa_tester'],
4175
+ 'variant_source': 'persistence',
4176
+ 'persisted_at_in_ms': data['persistedAt'],
4177
+ 'ttl_in_ms': ttlMs
4178
+ });
4179
+ });
4180
+
4181
+ logger$5.log('Loaded', persistedFlags.size, 'variants from IndexedDB for distinct_id', data['distinctId']);
4182
+
4183
+ return {
4184
+ flags: persistedFlags,
4185
+ pendingFirstTimeEvents: data['pendingFirstTimeEvents'] || {},
4186
+ persistedAtMs: data['persistedAt'],
4187
+ ttlMs: ttlMs
4188
+ };
4189
+ }, this)).catch(_.bind(function(error) {
4190
+ logger$5.error('Failed to load persisted variants from IndexedDB, so clearing', error);
4191
+ return clearAndReturnNull();
4192
+ }, this));
4193
+ };
4194
+
4195
+ FeatureFlagPersistence.prototype.save = function(context, flagsMap, pendingFirstTimeEvents) {
4196
+ if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
4197
+ return Promise.resolve();
4198
+ }
4199
+
4200
+ var flagVariants = {};
4201
+ flagsMap.forEach(function(variant, key) {
4202
+ flagVariants[key] = {
4203
+ 'variant_key': variant['key'],
4204
+ 'variant_value': variant['value'],
4205
+ 'experiment_id': variant['experiment_id'],
4206
+ 'is_experiment_active': variant['is_experiment_active'],
4207
+ 'is_qa_tester': variant['is_qa_tester']
4208
+ };
4209
+ });
4210
+
4211
+ var data = {
4212
+ 'persistedAt': Date.now(),
4213
+ 'distinctId': context && context['distinct_id'],
4214
+ 'context': context,
4215
+ 'flagVariants': flagVariants,
4216
+ 'pendingFirstTimeEvents': pendingFirstTimeEvents || {}
4217
+ };
4218
+
4219
+ return this.idb.init().then(_.bind(function() {
4220
+ return this.idb.setItem(this.persistedVariantsKey, data);
4221
+ }, this)).then(function() {
4222
+ logger$5.log('Saved', flagsMap.size, 'variants to IndexedDB for distinct_id', data['distinctId']);
4223
+ }).catch(function(error) {
4224
+ logger$5.error('Failed to persist variants to IndexedDB:', error);
4225
+ });
4226
+ };
4227
+
4228
+ FeatureFlagPersistence.prototype.clear = function() {
4229
+ if (this.isGloballyDisabled()) {
4230
+ return Promise.resolve();
4231
+ }
4232
+ return this.idb.init().then(_.bind(function() {
4233
+ return this.idb.removeItem(this.persistedVariantsKey);
4234
+ }, this)).then(function() {
4235
+ logger$5.log('Cleared persisted variants from IndexedDB');
4236
+ }).catch(function(error) {
4237
+ logger$5.error('Failed to clear persisted variants from IndexedDB:', error);
4238
+ });
4239
+ };
4240
+
3926
4241
  var logger$4 = console_with_prefix('flags');
3927
4242
  var FLAGS_CONFIG_KEY = 'flags';
3928
4243
 
3929
4244
  var CONFIG_CONTEXT = 'context';
4245
+ var CONFIG_PERSISTENCE = 'persistence';
3930
4246
  var CONFIG_DEFAULTS = {};
3931
4247
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
3932
4248
 
@@ -3949,6 +4265,13 @@ var getFlagKeyFromPendingEventKey = function(eventKey) {
3949
4265
  return eventKey.split(':')[0];
3950
4266
  };
3951
4267
 
4268
+ var withFallbackSource = function(fallback) {
4269
+ if (_.isObject(fallback)) {
4270
+ return _.extend({}, fallback, {'variant_source': 'fallback'});
4271
+ }
4272
+ return {'value': fallback, 'variant_source': 'fallback'};
4273
+ };
4274
+
3952
4275
  /**
3953
4276
  * FeatureFlagManager: support for Mixpanel's feature flagging product
3954
4277
  * @constructor
@@ -3971,13 +4294,63 @@ FeatureFlagManager.prototype.init = function() {
3971
4294
  }
3972
4295
 
3973
4296
  this.flags = null;
3974
- this.fetchFlags().catch(function() {
3975
- logger$4.error('Error fetching flags during init');
3976
- });
3977
-
3978
4297
  this.trackedFeatures = new Set();
3979
4298
  this.pendingFirstTimeEvents = {};
3980
4299
  this.activatedFirstTimeEvents = {};
4300
+ this._loadedPersistedAtMs = null;
4301
+ this._loadedTtlMs = null;
4302
+
4303
+ this.persistence = new FeatureFlagPersistence(
4304
+ this.getConfig(CONFIG_PERSISTENCE),
4305
+ this.getMpConfig('token'),
4306
+ _.bind(function() { return this.getMpConfig('disable_persistence'); }, this)
4307
+ );
4308
+
4309
+ this.persistenceLoadedPromise = this.persistence.loadFlagsFromStorage(this._buildContext())
4310
+ .then(_.bind(function(loaded) {
4311
+ if (loaded) {
4312
+ this.flags = loaded.flags;
4313
+ this.pendingFirstTimeEvents = loaded.pendingFirstTimeEvents;
4314
+ this._loadedPersistedAtMs = loaded.persistedAtMs;
4315
+ this._loadedTtlMs = loaded.ttlMs;
4316
+ }
4317
+ }, this));
4318
+
4319
+ return this.persistenceLoadedPromise
4320
+ .then(_.bind(function() {
4321
+ return this.fetchFlags();
4322
+ }, this))
4323
+ .catch(function() {
4324
+ logger$4.error('Error initializing feature flags');
4325
+ });
4326
+ };
4327
+
4328
+ FeatureFlagManager.prototype._buildContext = function() {
4329
+ return _.extend(
4330
+ {'distinct_id': this.getMpProperty('distinct_id'), 'device_id': this.getMpProperty('$device_id')},
4331
+ this.getConfig(CONFIG_CONTEXT)
4332
+ );
4333
+ };
4334
+
4335
+ FeatureFlagManager.prototype.reset = function() {
4336
+ if (!this.persistence) {
4337
+ return Promise.resolve();
4338
+ }
4339
+
4340
+ this.flags = null;
4341
+ this.pendingFirstTimeEvents = {};
4342
+ this.activatedFirstTimeEvents = {};
4343
+ this.trackedFeatures = new Set();
4344
+ this.fetchPromise = null;
4345
+ this._fetchInProgressStartTime = null;
4346
+ this._loadedPersistedAtMs = null;
4347
+ this._loadedTtlMs = null;
4348
+
4349
+ return this.persistence.clear().then(_.bind(function() {
4350
+ return this.fetchFlags();
4351
+ }, this)).catch(function() {
4352
+ logger$4.error('Error during flags reset');
4353
+ });
3981
4354
  };
3982
4355
 
3983
4356
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -4034,12 +4407,11 @@ FeatureFlagManager.prototype.fetchFlags = function() {
4034
4407
  return Promise.resolve();
4035
4408
  }
4036
4409
 
4037
- var distinctId = this.getMpProperty('distinct_id');
4038
- var deviceId = this.getMpProperty('$device_id');
4410
+ var context = this._buildContext();
4411
+ var distinctId = context['distinct_id'];
4039
4412
  var traceparent = generateTraceparent();
4040
4413
  logger$4.log('Fetching flags for distinct ID: ' + distinctId);
4041
4414
 
4042
- var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
4043
4415
  var searchParams = new URLSearchParams();
4044
4416
  searchParams.set('context', JSON.stringify(context));
4045
4417
  searchParams.set('token', this.getMpConfig('token'));
@@ -4089,7 +4461,8 @@ FeatureFlagManager.prototype.fetchFlags = function() {
4089
4461
  'value': data['variant_value'],
4090
4462
  'experiment_id': data['experiment_id'],
4091
4463
  'is_experiment_active': data['is_experiment_active'],
4092
- 'is_qa_tester': data['is_qa_tester']
4464
+ 'is_qa_tester': data['is_qa_tester'],
4465
+ 'variant_source': 'network'
4093
4466
  });
4094
4467
  }
4095
4468
  }, this);
@@ -4131,10 +4504,15 @@ FeatureFlagManager.prototype.fetchFlags = function() {
4131
4504
  }
4132
4505
 
4133
4506
  this.flags = flags;
4507
+ this.trackedFeatures = new Set();
4134
4508
  this.pendingFirstTimeEvents = pendingFirstTimeEvents;
4509
+ this._loadedPersistedAtMs = null;
4510
+ this._loadedTtlMs = null;
4135
4511
  this._traceparent = traceparent;
4136
4512
 
4137
4513
  this._loadTargetingIfNeeded();
4514
+
4515
+ this.persistence.save(context, this.flags, this.pendingFirstTimeEvents);
4138
4516
  }.bind(this)).catch(function(error) {
4139
4517
  if (this._fetchInProgressStartTime) {
4140
4518
  this.markFetchComplete();
@@ -4294,6 +4672,7 @@ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, p
4294
4672
  };
4295
4673
 
4296
4674
  this.flags.set(flagKey, newVariant);
4675
+ this.trackedFeatures.delete(flagKey);
4297
4676
  this.activatedFirstTimeEvents[eventKey] = true;
4298
4677
 
4299
4678
  this.recordFirstTimeEvent(
@@ -4343,35 +4722,106 @@ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId,
4343
4722
  };
4344
4723
 
4345
4724
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
4346
- if (!this.fetchPromise) {
4725
+ if (!this.persistenceLoadedPromise) {
4347
4726
  return new Promise(function(resolve) {
4348
4727
  logger$4.critical('Feature Flags not initialized');
4349
- resolve(fallback);
4728
+ resolve(withFallbackSource(fallback));
4350
4729
  });
4351
4730
  }
4352
4731
 
4353
- return this.fetchPromise.then(function() {
4354
- return this.getVariantSync(featureName, fallback);
4355
- }.bind(this)).catch(function(error) {
4356
- logger$4.error(error);
4357
- return fallback;
4358
- });
4732
+ var policy = this.persistence.getPolicy();
4733
+
4734
+ return this.persistenceLoadedPromise.then(_.bind(function() {
4735
+ // Serve from persistence until the network completes a successful fetch. If a non-expired cached value is available, return it without waiting on the in-flight fetch.
4736
+ if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
4737
+ if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
4738
+ return this.getVariantSync(featureName, fallback);
4739
+ }
4740
+ if (!this.fetchPromise) {
4741
+ return withFallbackSource(fallback);
4742
+ }
4743
+ return this.fetchPromise.then(_.bind(function() {
4744
+ return this.getVariantSync(featureName, fallback);
4745
+ }, this)).catch(function(error) {
4746
+ logger$4.error(error);
4747
+ return withFallbackSource(fallback);
4748
+ });
4749
+ }
4750
+
4751
+ var serve = _.bind(function() { return this.getVariantSync(featureName, fallback); }, this);
4752
+ if (!this.fetchPromise) {
4753
+ return withFallbackSource(fallback);
4754
+ }
4755
+ return this.fetchPromise.then(serve).catch(serve);
4756
+ }, this));
4757
+ };
4758
+
4759
+ FeatureFlagManager.prototype._loadedPersistenceIsStale = function() {
4760
+ if (!this._loadedPersistedAtMs || !this._loadedTtlMs) {
4761
+ return false;
4762
+ }
4763
+ return Date.now() - this._loadedPersistedAtMs >= this._loadedTtlMs;
4359
4764
  };
4360
4765
 
4361
4766
  FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
4767
+ if (this._loadedPersistenceIsStale()) {
4768
+ logger$4.log('Loaded persisted variants are past TTL so returning fallback for "' + featureName + '"');
4769
+ return withFallbackSource(fallback);
4770
+ }
4362
4771
  if (!this.areFlagsReady()) {
4363
4772
  logger$4.log('Flags not loaded yet');
4364
- return fallback;
4773
+ return withFallbackSource(fallback);
4365
4774
  }
4366
4775
  var feature = this.flags.get(featureName);
4367
4776
  if (!feature) {
4368
4777
  logger$4.log('No flag found: "' + featureName + '"');
4369
- return fallback;
4778
+ return withFallbackSource(fallback);
4370
4779
  }
4371
4780
  this.trackFeatureCheck(featureName, feature);
4372
4781
  return feature;
4373
4782
  };
4374
4783
 
4784
+ FeatureFlagManager.prototype.getAllVariants = function() {
4785
+ if (!this.persistenceLoadedPromise) {
4786
+ logger$4.critical('Feature Flags not initialized');
4787
+ return Promise.resolve(new Map());
4788
+ }
4789
+
4790
+ var policy = this.persistence.getPolicy();
4791
+
4792
+ return this.persistenceLoadedPromise.then(_.bind(function() {
4793
+ // Serve from persistence until the network completes a successful fetch. If a non-expired cached value is available, return it without waiting on the in-flight fetch.
4794
+ if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
4795
+ if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
4796
+ return this.getAllVariantsSync();
4797
+ }
4798
+ if (!this.fetchPromise) {
4799
+ return new Map();
4800
+ }
4801
+ return this.fetchPromise.then(_.bind(function() {
4802
+ return this.getAllVariantsSync();
4803
+ }, this)).catch(function(error) {
4804
+ logger$4.error(error);
4805
+ return new Map();
4806
+ });
4807
+ }
4808
+
4809
+ var serve = _.bind(this.getAllVariantsSync, this);
4810
+ if (!this.fetchPromise) {
4811
+ return new Map();
4812
+ }
4813
+ return this.fetchPromise.then(serve).catch(serve);
4814
+ }, this));
4815
+ };
4816
+
4817
+ FeatureFlagManager.prototype.getAllVariantsSync = function() {
4818
+ if (this._loadedPersistenceIsStale()) {
4819
+ logger$4.log('Loaded persisted variants are past TTL so returning empty Map');
4820
+ return new Map();
4821
+ }
4822
+ return this.flags || new Map();
4823
+ };
4824
+
4375
4825
  FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
4376
4826
  return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
4377
4827
  return feature['value'];
@@ -4410,6 +4860,10 @@ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue
4410
4860
  return val;
4411
4861
  };
4412
4862
 
4863
+ function isPresent(v) {
4864
+ return v !== undefined && v !== null;
4865
+ }
4866
+
4413
4867
  FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
4414
4868
  if (this.trackedFeatures.has(featureName)) {
4415
4869
  return;
@@ -4420,21 +4874,30 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
4420
4874
  'Experiment name': featureName,
4421
4875
  'Variant name': feature['key'],
4422
4876
  '$experiment_type': 'feature_flag',
4423
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
4424
- 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
4877
+ 'Variant fetch start time': isPresent(this._fetchStartTime) ? new Date(this._fetchStartTime).toISOString() : null,
4878
+ 'Variant fetch complete time': isPresent(this._fetchCompleteTime) ? new Date(this._fetchCompleteTime).toISOString() : null,
4425
4879
  'Variant fetch latency (ms)': this._fetchLatency,
4426
4880
  'Variant fetch traceparent': this._traceparent,
4427
4881
  };
4428
4882
 
4429
- if (feature['experiment_id'] !== 'undefined') {
4883
+ if (isPresent(feature['experiment_id'])) {
4430
4884
  trackingProperties['$experiment_id'] = feature['experiment_id'];
4431
4885
  }
4432
- if (feature['is_experiment_active'] !== 'undefined') {
4886
+ if (isPresent(feature['is_experiment_active'])) {
4433
4887
  trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
4434
4888
  }
4435
- if (feature['is_qa_tester'] !== 'undefined') {
4889
+ if (isPresent(feature['is_qa_tester'])) {
4436
4890
  trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
4437
4891
  }
4892
+ if (isPresent(feature['variant_source'])) {
4893
+ trackingProperties['$variant_source'] = feature['variant_source'];
4894
+ }
4895
+ if (isPresent(feature['persisted_at_in_ms'])) {
4896
+ trackingProperties['$persisted_at_in_ms'] = feature['persisted_at_in_ms'];
4897
+ }
4898
+ if (isPresent(feature['ttl_in_ms'])) {
4899
+ trackingProperties['$ttl_in_ms'] = feature['ttl_in_ms'];
4900
+ }
4438
4901
 
4439
4902
  this.track('$experiment_started', trackingProperties);
4440
4903
  };
@@ -4458,6 +4921,8 @@ safewrapClass(FeatureFlagManager);
4458
4921
  FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
4459
4922
  FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
4460
4923
  FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
4924
+ FeatureFlagManager.prototype['get_all_variants'] = FeatureFlagManager.prototype.getAllVariants;
4925
+ FeatureFlagManager.prototype['get_all_variants_sync'] = FeatureFlagManager.prototype.getAllVariantsSync;
4461
4926
  FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
4462
4927
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
4463
4928
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
@@ -4472,131 +4937,14 @@ FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.
4472
4937
  // Exports intended only for testing
4473
4938
  FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
4474
4939
 
4475
- var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
4476
-
4940
+ var MIXPANEL_BROWSER_DB_NAME = 'mixpanelBrowserDb';
4477
4941
  var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
4478
4942
  var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
4479
4943
 
4480
- // note: increment the version number when adding new object stores
4481
- var DB_VERSION = 1;
4482
- var OBJECT_STORES = [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME];
4483
-
4484
- /**
4485
- * @type {import('./wrapper').StorageWrapper}
4486
- */
4487
- var IDBStorageWrapper = function (storeName) {
4488
- /**
4489
- * @type {Promise<IDBDatabase>|null}
4490
- */
4491
- this.dbPromise = null;
4492
- this.storeName = storeName;
4493
- };
4494
-
4495
- IDBStorageWrapper.prototype._openDb = function () {
4496
- return new PromisePolyfill(function (resolve, reject) {
4497
- var openRequest = win.indexedDB.open(MIXPANEL_DB_NAME, DB_VERSION);
4498
- openRequest['onerror'] = function () {
4499
- reject(openRequest.error);
4500
- };
4501
-
4502
- openRequest['onsuccess'] = function () {
4503
- resolve(openRequest.result);
4504
- };
4505
-
4506
- openRequest['onupgradeneeded'] = function (ev) {
4507
- var db = ev.target.result;
4508
-
4509
- OBJECT_STORES.forEach(function (storeName) {
4510
- db.createObjectStore(storeName);
4511
- });
4512
- };
4513
- });
4514
- };
4515
-
4516
- IDBStorageWrapper.prototype.init = function () {
4517
- if (!win.indexedDB) {
4518
- return PromisePolyfill.reject('indexedDB is not supported in this browser');
4519
- }
4520
-
4521
- if (!this.dbPromise) {
4522
- this.dbPromise = this._openDb();
4523
- }
4524
-
4525
- return this.dbPromise
4526
- .then(function (dbOrError) {
4527
- if (dbOrError instanceof win['IDBDatabase']) {
4528
- return PromisePolyfill.resolve();
4529
- } else {
4530
- return PromisePolyfill.reject(dbOrError);
4531
- }
4532
- });
4533
- };
4534
-
4535
- IDBStorageWrapper.prototype.isInitialized = function () {
4536
- return !!this.dbPromise;
4537
- };
4538
-
4539
- /**
4540
- * @param {IDBTransactionMode} mode
4541
- * @param {function(IDBObjectStore): void} storeCb
4542
- */
4543
- IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
4544
- var storeName = this.storeName;
4545
- var doTransaction = function (db) {
4546
- return new PromisePolyfill(function (resolve, reject) {
4547
- var transaction = db.transaction(storeName, mode);
4548
- transaction.oncomplete = function () {
4549
- resolve(transaction);
4550
- };
4551
- transaction.onabort = transaction.onerror = function () {
4552
- reject(transaction.error);
4553
- };
4554
-
4555
- storeCb(transaction.objectStore(storeName));
4556
- });
4557
- };
4558
-
4559
- return this.dbPromise
4560
- .then(doTransaction)
4561
- .catch(function (err) {
4562
- if (err && err['name'] === 'InvalidStateError') {
4563
- // try reopening the DB if the connection is closed
4564
- this.dbPromise = this._openDb();
4565
- return this.dbPromise.then(doTransaction);
4566
- } else {
4567
- return PromisePolyfill.reject(err);
4568
- }
4569
- }.bind(this));
4570
- };
4571
-
4572
- IDBStorageWrapper.prototype.setItem = function (key, value) {
4573
- return this.makeTransaction('readwrite', function (objectStore) {
4574
- objectStore.put(value, key);
4575
- });
4576
- };
4577
-
4578
- IDBStorageWrapper.prototype.getItem = function (key) {
4579
- var req;
4580
- return this.makeTransaction('readonly', function (objectStore) {
4581
- req = objectStore.get(key);
4582
- }).then(function () {
4583
- return req.result;
4584
- });
4585
- };
4586
-
4587
- IDBStorageWrapper.prototype.removeItem = function (key) {
4588
- return this.makeTransaction('readwrite', function (objectStore) {
4589
- objectStore.delete(key);
4590
- });
4591
- };
4592
-
4593
- IDBStorageWrapper.prototype.getAll = function () {
4594
- var req;
4595
- return this.makeTransaction('readonly', function (objectStore) {
4596
- req = objectStore.getAll();
4597
- }).then(function () {
4598
- return req.result;
4599
- });
4944
+ // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
4945
+ var RECORDER_VERSION_DATA = {
4946
+ version: 1,
4947
+ storeNames: [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME]
4600
4948
  };
4601
4949
 
4602
4950
  /**
@@ -4669,7 +5017,7 @@ RecorderManager.prototype.shouldLoadRecorder = function() {
4669
5017
  return PromisePolyfill.resolve(false);
4670
5018
  }
4671
5019
 
4672
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
5020
+ var recording_registry_idb = new IDBStorageWrapper(MIXPANEL_BROWSER_DB_NAME, RECORDING_REGISTRY_STORE_NAME, RECORDER_VERSION_DATA);
4673
5021
  var tab_id = this.getTabId();
4674
5022
  return recording_registry_idb.init()
4675
5023
  .then(function () {
@@ -5125,7 +5473,7 @@ var SharedLock = function(key, options) {
5125
5473
  options = options || {};
5126
5474
 
5127
5475
  this.storageKey = key;
5128
- this.storage = options.storage || win.localStorage;
5476
+ this.storage = options.storage || getLocalStorage();
5129
5477
  this.pollIntervalMS = options.pollIntervalMS || 100;
5130
5478
  this.timeoutMS = options.timeoutMS || 2000;
5131
5479
 
@@ -5253,10 +5601,13 @@ SharedLock.prototype.withLock = function(lockedCB, pid) {
5253
5601
  * @type {import('./wrapper').StorageWrapper}
5254
5602
  */
5255
5603
  var LocalStorageWrapper = function (storageOverride) {
5256
- this.storage = storageOverride || win.localStorage;
5604
+ this.storage = storageOverride || getLocalStorage();
5257
5605
  };
5258
5606
 
5259
5607
  LocalStorageWrapper.prototype.init = function () {
5608
+ if (!this.storage) {
5609
+ return PromisePolyfill.reject(new Error('localStorage is not available'));
5610
+ }
5260
5611
  return PromisePolyfill.resolve();
5261
5612
  };
5262
5613
 
@@ -5323,7 +5674,7 @@ var RequestQueue = function (storageKey, options) {
5323
5674
  if (this.usePersistence) {
5324
5675
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
5325
5676
  this.lock = new SharedLock(storageKey, {
5326
- storage: options.sharedLockStorage || win.localStorage,
5677
+ storage: options.sharedLockStorage,
5327
5678
  timeoutMS: options.sharedLockTimeoutMS,
5328
5679
  });
5329
5680
  }
@@ -7788,6 +8139,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
7788
8139
  'disable_all_events': false,
7789
8140
  'identify_called': false
7790
8141
  };
8142
+ this._remote_settings_strict_disabled = false;
7791
8143
 
7792
8144
  // set up request queueing/batching
7793
8145
  this.request_batchers = {};
@@ -7862,9 +8214,6 @@ MixpanelLib.prototype._init = function(token, config, name) {
7862
8214
  this.flags.init();
7863
8215
  this['flags'] = this.flags;
7864
8216
 
7865
- this.autocapture = new Autocapture(this);
7866
- this.autocapture.init();
7867
-
7868
8217
  this._init_tab_id();
7869
8218
 
7870
8219
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
@@ -7876,6 +8225,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
7876
8225
  } else {
7877
8226
  this.__session_recording_init_promise = this._check_and_start_session_recording();
7878
8227
  }
8228
+
8229
+ this.autocapture = new Autocapture(this);
8230
+ this.autocapture.init();
7879
8231
  };
7880
8232
 
7881
8233
  /**
@@ -7922,9 +8274,19 @@ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpane
7922
8274
  return this.recorderManager.checkAndStartSessionRecording(force_start);
7923
8275
  });
7924
8276
 
7925
- MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
7926
- return this.recorderManager.startRecordingOnEvent(event_name, properties);
7927
- };
8277
+ MixpanelLib.prototype._start_recording_on_event = safewrap(function(event_name, properties) {
8278
+ // Wait for recording init to complete before evaluating event triggers.
8279
+ // This ensures recording_event_triggers config is fully loaded when remote settings are used.
8280
+ if (this.__session_recording_init_promise) {
8281
+ this.__session_recording_init_promise.then(_.bind(function() {
8282
+ // In strict mode, skip recording if remote settings failed
8283
+ if (this._remote_settings_strict_disabled) {
8284
+ return;
8285
+ }
8286
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
8287
+ }, this));
8288
+ }
8289
+ });
7928
8290
 
7929
8291
  MixpanelLib.prototype.start_session_recording = function () {
7930
8292
  return this._check_and_start_session_recording(true);
@@ -8223,6 +8585,7 @@ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
8223
8585
  var disableRecordingIfStrict = function() {
8224
8586
  if (mode === 'strict') {
8225
8587
  self.set_config({'record_sessions_percent': 0});
8588
+ self._remote_settings_strict_disabled = true;
8226
8589
  }
8227
8590
  };
8228
8591
 
@@ -8848,6 +9211,10 @@ MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(proper
8848
9211
  properties
8849
9212
  );
8850
9213
 
9214
+ if (this.is_recording_heatmap_data()) {
9215
+ event_properties['$captured_for_heatmap'] = true;
9216
+ }
9217
+
8851
9218
  return this.track(event_name, event_properties);
8852
9219
  });
8853
9220
 
@@ -9191,6 +9558,7 @@ MixpanelLib.prototype.reset = function() {
9191
9558
  '$device_id': uuid
9192
9559
  }, '');
9193
9560
  this._check_and_start_session_recording();
9561
+ this.flags.reset();
9194
9562
  };
9195
9563
 
9196
9564
  /**