mixpanel-browser 2.77.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 (68) hide show
  1. package/.claude/settings.local.json +6 -9
  2. package/.eslintrc.json +12 -0
  3. package/.github/workflows/openfeature-provider-tests.yml +31 -0
  4. package/CHANGELOG.md +11 -0
  5. package/build.sh +2 -2
  6. package/dist/async-modules/{mixpanel-recorder-wIWnMDLA.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-DLKbUIEE.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-CmVvUyFM.js → mixpanel-targeting-BBMVbgJF.js} +24 -13
  12. package/dist/mixpanel-core.cjs.d.ts +46 -1
  13. package/dist/mixpanel-core.cjs.js +671 -272
  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 +46 -1
  21. package/dist/mixpanel-with-async-modules.cjs.js +673 -274
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +46 -1
  23. package/dist/mixpanel-with-async-recorder.cjs.js +673 -274
  24. package/dist/mixpanel-with-recorder.d.ts +46 -1
  25. package/dist/mixpanel-with-recorder.js +596 -197
  26. package/dist/mixpanel-with-recorder.min.d.ts +46 -1
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +46 -1
  29. package/dist/mixpanel.amd.js +596 -197
  30. package/dist/mixpanel.cjs.d.ts +46 -1
  31. package/dist/mixpanel.cjs.js +596 -197
  32. package/dist/mixpanel.globals.js +673 -274
  33. package/dist/mixpanel.min.js +200 -189
  34. package/dist/mixpanel.module.d.ts +46 -1
  35. package/dist/mixpanel.module.js +596 -197
  36. package/dist/mixpanel.umd.d.ts +46 -1
  37. package/dist/mixpanel.umd.js +596 -197
  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/CLAUDE.md +24 -0
  51. package/src/flags/flags-persistence.js +176 -0
  52. package/src/flags/index.js +278 -98
  53. package/src/index.d.ts +46 -1
  54. package/src/mixpanel-core.js +27 -8
  55. package/src/recorder/idb-config.js +16 -0
  56. package/src/recorder/recording-registry.js +7 -2
  57. package/src/recorder/session-recording.js +9 -4
  58. package/src/recorder-manager.js +7 -2
  59. package/src/request-queue.js +1 -2
  60. package/src/shared-lock.js +2 -3
  61. package/src/storage/indexed-db.js +16 -15
  62. package/src/storage/local-storage.js +5 -3
  63. package/src/utils.js +25 -12
  64. package/testServer.js +2 -0
  65. package/tsconfig.base.json +9 -0
  66. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +0 -1
  67. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +0 -2
  68. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +0 -1
@@ -3,7 +3,7 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.77.0'
6
+ LIB_VERSION: '2.79.0'
7
7
  };
8
8
 
9
9
  // Window global names for async modules
@@ -11,8 +11,8 @@
11
11
  var RECORDER_GLOBAL_NAME = '__mp_recorder';
12
12
 
13
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';
14
+ var RECORDER_FILENAME = 'mixpanel-recorder-P6SEnnPV.js';
15
+ var TARGETING_FILENAME = 'mixpanel-targeting-BBMVbgJF.js';
16
16
 
17
17
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
18
18
  var win;
@@ -502,6 +502,7 @@
502
502
  var console_with_prefix = function(prefix) {
503
503
  return {
504
504
  log: log_func_with_prefix(console.log, prefix),
505
+ warn: log_func_with_prefix(console.warn, prefix),
505
506
  error: log_func_with_prefix(console.error, prefix),
506
507
  critical: log_func_with_prefix(console.critical, prefix)
507
508
  };
@@ -1448,7 +1449,8 @@
1448
1449
  if (_localStorageSupported !== null && !forceCheck) {
1449
1450
  return _localStorageSupported;
1450
1451
  }
1451
- return _localStorageSupported = _testStorageSupported(storage || win.localStorage);
1452
+
1453
+ return _localStorageSupported = _testStorageSupported(storage);
1452
1454
  };
1453
1455
 
1454
1456
  var _sessionStorageSupported = null;
@@ -1456,7 +1458,8 @@
1456
1458
  if (_sessionStorageSupported !== null && !forceCheck) {
1457
1459
  return _sessionStorageSupported;
1458
1460
  }
1459
- return _sessionStorageSupported = _testStorageSupported(storage || win.sessionStorage);
1461
+
1462
+ return _sessionStorageSupported = _testStorageSupported(storage);
1460
1463
  };
1461
1464
 
1462
1465
  function _storageWrapper(storage, name, is_supported_fn) {
@@ -1506,17 +1509,26 @@
1506
1509
  };
1507
1510
  }
1508
1511
 
1509
- // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1510
- // so create dummy storage wrappers that silently fail as a fallback.
1511
- var windowLocalStorage = null, windowSessionStorage = null;
1512
- try {
1513
- windowLocalStorage = win.localStorage;
1514
- windowSessionStorage = win.sessionStorage;
1515
- // eslint-disable-next-line no-empty
1516
- } catch (_err) {}
1512
+ // Safari and other browsers may error out accessing localStorage/sessionStorage
1513
+ // when cookies are disabled, so wrap access in a try-catch.
1514
+ var getLocalStorage = function() {
1515
+ try {
1516
+ return win.localStorage; // eslint-disable-line no-restricted-properties
1517
+ } catch (_err) {
1518
+ return null;
1519
+ }
1520
+ };
1517
1521
 
1518
- _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1519
- _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1522
+ var getSessionStorage = function() {
1523
+ try {
1524
+ return win.sessionStorage; // eslint-disable-line no-restricted-properties
1525
+ } catch (_err) {
1526
+ return null;
1527
+ }
1528
+ };
1529
+
1530
+ _.localStorage = _storageWrapper(getLocalStorage(), 'localStorage', localStorageSupported);
1531
+ _.sessionStorage = _storageWrapper(getSessionStorage(), 'sessionStorage', sessionStorageSupported);
1520
1532
 
1521
1533
  _.register_event = (function() {
1522
1534
  // written by Dean Edwards, 2005
@@ -2268,7 +2280,7 @@
2268
2280
 
2269
2281
  var MAX_DEPTH = 5;
2270
2282
 
2271
- var logger$5 = console_with_prefix('autocapture');
2283
+ var logger$6 = console_with_prefix('autocapture');
2272
2284
 
2273
2285
 
2274
2286
  function getClasses(el) {
@@ -2532,7 +2544,7 @@
2532
2544
  return false;
2533
2545
  }
2534
2546
  } catch (err) {
2535
- logger$5.critical('Error while checking element in allowElementCallback', err);
2547
+ logger$6.critical('Error while checking element in allowElementCallback', err);
2536
2548
  return false;
2537
2549
  }
2538
2550
  }
@@ -2549,7 +2561,7 @@
2549
2561
  return true;
2550
2562
  }
2551
2563
  } catch (err) {
2552
- logger$5.critical('Error while checking selector: ' + sel, err);
2564
+ logger$6.critical('Error while checking selector: ' + sel, err);
2553
2565
  }
2554
2566
  }
2555
2567
  return false;
@@ -2564,7 +2576,7 @@
2564
2576
  return true;
2565
2577
  }
2566
2578
  } catch (err) {
2567
- logger$5.critical('Error while checking element in blockElementCallback', err);
2579
+ logger$6.critical('Error while checking element in blockElementCallback', err);
2568
2580
  return true;
2569
2581
  }
2570
2582
  }
@@ -2578,7 +2590,7 @@
2578
2590
  return true;
2579
2591
  }
2580
2592
  } catch (err) {
2581
- logger$5.critical('Error while checking selector: ' + sel, err);
2593
+ logger$6.critical('Error while checking selector: ' + sel, err);
2582
2594
  }
2583
2595
  }
2584
2596
  }
@@ -3042,7 +3054,7 @@
3042
3054
  observer.observe(shadowRoot, this.observerConfig);
3043
3055
  this.shadowObservers.push(observer);
3044
3056
  } catch (e) {
3045
- logger$5.critical('Error while observing shadow root', e);
3057
+ logger$6.critical('Error while observing shadow root', e);
3046
3058
  }
3047
3059
  };
3048
3060
 
@@ -3053,7 +3065,7 @@
3053
3065
  }
3054
3066
 
3055
3067
  if (!weakSetSupported()) {
3056
- logger$5.critical('Shadow DOM observation unavailable: WeakSet not supported');
3068
+ logger$6.critical('Shadow DOM observation unavailable: WeakSet not supported');
3057
3069
  return;
3058
3070
  }
3059
3071
 
@@ -3069,7 +3081,7 @@
3069
3081
  try {
3070
3082
  this.shadowObservers[i].disconnect();
3071
3083
  } catch (e) {
3072
- logger$5.critical('Error while disconnecting shadow DOM observer', e);
3084
+ logger$6.critical('Error while disconnecting shadow DOM observer', e);
3073
3085
  }
3074
3086
  }
3075
3087
  this.shadowObservers = [];
@@ -3257,7 +3269,7 @@
3257
3269
 
3258
3270
  this.mutationObserver.observe(document.body || document.documentElement, MUTATION_OBSERVER_CONFIG);
3259
3271
  } catch (e) {
3260
- logger$5.critical('Error while setting up mutation observer', e);
3272
+ logger$6.critical('Error while setting up mutation observer', e);
3261
3273
  }
3262
3274
  }
3263
3275
 
@@ -3272,7 +3284,7 @@
3272
3284
  );
3273
3285
  this.shadowDOMObserver.start();
3274
3286
  } catch (e) {
3275
- logger$5.critical('Error while setting up shadow DOM observer', e);
3287
+ logger$6.critical('Error while setting up shadow DOM observer', e);
3276
3288
  this.shadowDOMObserver = null;
3277
3289
  }
3278
3290
  }
@@ -3299,7 +3311,7 @@
3299
3311
  try {
3300
3312
  listener.target.removeEventListener(listener.event, listener.handler, listener.options);
3301
3313
  } catch (e) {
3302
- logger$5.critical('Error while removing event listener', e);
3314
+ logger$6.critical('Error while removing event listener', e);
3303
3315
  }
3304
3316
  }
3305
3317
  this.eventListeners = [];
@@ -3308,7 +3320,7 @@
3308
3320
  try {
3309
3321
  this.mutationObserver.disconnect();
3310
3322
  } catch (e) {
3311
- logger$5.critical('Error while disconnecting mutation observer', e);
3323
+ logger$6.critical('Error while disconnecting mutation observer', e);
3312
3324
  }
3313
3325
  this.mutationObserver = null;
3314
3326
  }
@@ -3317,7 +3329,7 @@
3317
3329
  try {
3318
3330
  this.shadowDOMObserver.stop();
3319
3331
  } catch (e) {
3320
- logger$5.critical('Error while stopping shadow DOM observer', e);
3332
+ logger$6.critical('Error while stopping shadow DOM observer', e);
3321
3333
  }
3322
3334
  this.shadowDOMObserver = null;
3323
3335
  }
@@ -3395,7 +3407,7 @@
3395
3407
 
3396
3408
  Autocapture.prototype.init = function() {
3397
3409
  if (!minDOMApisSupported()) {
3398
- logger$5.critical('Autocapture unavailable: missing required DOM APIs');
3410
+ logger$6.critical('Autocapture unavailable: missing required DOM APIs');
3399
3411
  return;
3400
3412
  }
3401
3413
  this.initPageListeners();
@@ -3435,7 +3447,7 @@
3435
3447
  try {
3436
3448
  return !urlMatchesRegexList(currentUrl, allowUrlRegexes);
3437
3449
  } catch (err) {
3438
- logger$5.critical('Error while checking block URL regexes: ', err);
3450
+ logger$6.critical('Error while checking block URL regexes: ', err);
3439
3451
  return true;
3440
3452
  }
3441
3453
  }
@@ -3448,7 +3460,7 @@
3448
3460
  try {
3449
3461
  return urlMatchesRegexList(currentUrl, blockUrlRegexes);
3450
3462
  } catch (err) {
3451
- logger$5.critical('Error while checking block URL regexes: ', err);
3463
+ logger$6.critical('Error while checking block URL regexes: ', err);
3452
3464
  return true;
3453
3465
  }
3454
3466
  };
@@ -3586,7 +3598,7 @@
3586
3598
  return;
3587
3599
  }
3588
3600
 
3589
- logger$5.log('Initializing scroll depth tracking');
3601
+ logger$6.log('Initializing scroll depth tracking');
3590
3602
 
3591
3603
  this.maxScrollViewDepth = Math.max(document$1.documentElement.clientHeight, win.innerHeight || 0);
3592
3604
 
@@ -3612,7 +3624,7 @@
3612
3624
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.get_config('record_heatmap_data')) {
3613
3625
  return;
3614
3626
  }
3615
- logger$5.log('Initializing click tracking');
3627
+ logger$6.log('Initializing click tracking');
3616
3628
 
3617
3629
  this.listenerClick = function(ev) {
3618
3630
  if (!this.getConfig(CONFIG_TRACK_CLICK) && !this.mp.is_recording_heatmap_data()) {
@@ -3631,7 +3643,7 @@
3631
3643
  return;
3632
3644
  }
3633
3645
 
3634
- logger$5.log('Initializing dead click tracking');
3646
+ logger$6.log('Initializing dead click tracking');
3635
3647
  if (!this._deadClickTracker) {
3636
3648
  this._deadClickTracker = new DeadClickTracker(function(deadClickEvent) {
3637
3649
  this.trackDomEvent(deadClickEvent, MP_EV_DEAD_CLICK);
@@ -3665,7 +3677,7 @@
3665
3677
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
3666
3678
  return;
3667
3679
  }
3668
- logger$5.log('Initializing input tracking');
3680
+ logger$6.log('Initializing input tracking');
3669
3681
 
3670
3682
  this.listenerChange = function(ev) {
3671
3683
  if (!this.getConfig(CONFIG_TRACK_INPUT)) {
@@ -3679,14 +3691,15 @@
3679
3691
  Autocapture.prototype.initPageviewTracking = function() {
3680
3692
  win.removeEventListener(EV_MP_LOCATION_CHANGE, this.listenerLocationchange);
3681
3693
 
3682
- if (!this.pageviewTrackingConfig()) {
3694
+ if (!this.pageviewTrackingConfig() && !this.mp.get_config('record_heatmap_data')) {
3683
3695
  return;
3684
3696
  }
3685
- logger$5.log('Initializing pageview tracking');
3697
+ logger$6.log('Initializing pageview tracking');
3686
3698
 
3687
3699
  var previousTrackedUrl = '';
3688
3700
  var tracked = false;
3689
- if (!this.currentUrlBlocked()) {
3701
+ // Track initial pageview if pageview tracking enabled OR heatmap recording is active
3702
+ if ((this.pageviewTrackingConfig() || this.mp.is_recording_heatmap_data()) && !this.currentUrlBlocked()) {
3690
3703
  tracked = this.mp.track_pageview(DEFAULT_PROPS);
3691
3704
  }
3692
3705
  if (tracked) {
@@ -3702,6 +3715,10 @@
3702
3715
  var shouldTrack = false;
3703
3716
  var didPathChange = currentUrl.split('#')[0].split('?')[0] !== previousTrackedUrl.split('#')[0].split('?')[0];
3704
3717
  var trackPageviewOption = this.pageviewTrackingConfig();
3718
+ if (!trackPageviewOption && this.mp.is_recording_heatmap_data()) {
3719
+ trackPageviewOption = PAGEVIEW_OPTION_FULL_URL;
3720
+ }
3721
+
3705
3722
  if (trackPageviewOption === PAGEVIEW_OPTION_FULL_URL) {
3706
3723
  shouldTrack = currentUrl !== previousTrackedUrl;
3707
3724
  } else if (trackPageviewOption === PAGEVIEW_OPTION_URL_WITH_PATH_AND_QUERY_STRING) {
@@ -3717,7 +3734,7 @@
3717
3734
  }
3718
3735
  if (didPathChange) {
3719
3736
  this.lastScrollCheckpoint = 0;
3720
- logger$5.log('Path change: re-initializing scroll depth checkpoints');
3737
+ logger$6.log('Path change: re-initializing scroll depth checkpoints');
3721
3738
  }
3722
3739
  }
3723
3740
  }.bind(this));
@@ -3732,7 +3749,7 @@
3732
3749
  return;
3733
3750
  }
3734
3751
 
3735
- logger$5.log('Initializing rage click tracking');
3752
+ logger$6.log('Initializing rage click tracking');
3736
3753
  if (!this._rageClickTracker) {
3737
3754
  this._rageClickTracker = new RageClickTracker();
3738
3755
  }
@@ -3762,7 +3779,7 @@
3762
3779
  if (!this.getConfig(CONFIG_TRACK_SCROLL)) {
3763
3780
  return;
3764
3781
  }
3765
- logger$5.log('Initializing scroll tracking');
3782
+ logger$6.log('Initializing scroll tracking');
3766
3783
  this.lastScrollCheckpoint = 0;
3767
3784
 
3768
3785
  var scrollTrackFunction = function() {
@@ -3799,7 +3816,7 @@
3799
3816
  }
3800
3817
  }
3801
3818
  } catch (err) {
3802
- logger$5.critical('Error while calculating scroll percentage', err);
3819
+ logger$6.critical('Error while calculating scroll percentage', err);
3803
3820
  }
3804
3821
  if (shouldTrack) {
3805
3822
  this.mp.track(MP_EV_SCROLL, props);
@@ -3817,7 +3834,7 @@
3817
3834
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
3818
3835
  return;
3819
3836
  }
3820
- logger$5.log('Initializing submit tracking');
3837
+ logger$6.log('Initializing submit tracking');
3821
3838
 
3822
3839
  this.listenerSubmit = function(ev) {
3823
3840
  if (!this.getConfig(CONFIG_TRACK_SUBMIT)) {
@@ -3839,7 +3856,7 @@
3839
3856
  return;
3840
3857
  }
3841
3858
 
3842
- logger$5.log('Initializing page visibility tracking.');
3859
+ logger$6.log('Initializing page visibility tracking.');
3843
3860
  this._initScrollDepthTracking();
3844
3861
  var previousTrackedUrl = _.info.currentUrl();
3845
3862
 
@@ -3924,10 +3941,309 @@
3924
3941
  return win[TARGETING_GLOBAL_NAME];
3925
3942
  };
3926
3943
 
3944
+ /**
3945
+ * @type {import('./wrapper').StorageWrapper}
3946
+ */
3947
+ var IDBStorageWrapper = function (dbName, storeName, versionData) {
3948
+ this.dbName = dbName;
3949
+ this.storeName = storeName;
3950
+ this.version = versionData.version;
3951
+ this.storeNamesInDb = versionData.storeNames;
3952
+ /**
3953
+ * @type {Promise<IDBDatabase>|null}
3954
+ */
3955
+ this.dbPromise = null;
3956
+ };
3957
+
3958
+ IDBStorageWrapper.prototype._openDb = function () {
3959
+ var dbName = this.dbName;
3960
+ var version = this.version;
3961
+ var storeNamesInDb = this.storeNamesInDb;
3962
+ return new PromisePolyfill(function (resolve, reject) {
3963
+ var openRequest = win.indexedDB.open(dbName, version);
3964
+ openRequest['onerror'] = function () {
3965
+ reject(openRequest.error);
3966
+ };
3967
+
3968
+ openRequest['onsuccess'] = function () {
3969
+ resolve(openRequest.result);
3970
+ };
3971
+
3972
+ openRequest['onupgradeneeded'] = function (ev) {
3973
+ var db = ev.target.result;
3974
+
3975
+ storeNamesInDb.forEach(function (storeName) {
3976
+ if (!db.objectStoreNames.contains(storeName)) {
3977
+ db.createObjectStore(storeName);
3978
+ }
3979
+ });
3980
+ };
3981
+ });
3982
+ };
3983
+
3984
+ IDBStorageWrapper.prototype.init = function () {
3985
+ if (!win.indexedDB) {
3986
+ return PromisePolyfill.reject('indexedDB is not supported in this browser');
3987
+ }
3988
+
3989
+ if (!this.dbPromise) {
3990
+ this.dbPromise = this._openDb();
3991
+ }
3992
+
3993
+ return this.dbPromise
3994
+ .then(function (dbOrError) {
3995
+ if (dbOrError instanceof win['IDBDatabase']) {
3996
+ return PromisePolyfill.resolve();
3997
+ } else {
3998
+ return PromisePolyfill.reject(dbOrError);
3999
+ }
4000
+ });
4001
+ };
4002
+
4003
+ IDBStorageWrapper.prototype.isInitialized = function () {
4004
+ return !!this.dbPromise;
4005
+ };
4006
+
4007
+ /**
4008
+ * @param {IDBTransactionMode} mode
4009
+ * @param {function(IDBObjectStore): void} storeCb
4010
+ */
4011
+ IDBStorageWrapper.prototype.makeTransaction = function (mode, storeCb) {
4012
+ var storeName = this.storeName;
4013
+ var doTransaction = function (db) {
4014
+ return new PromisePolyfill(function (resolve, reject) {
4015
+ var transaction = db.transaction(storeName, mode);
4016
+ transaction.oncomplete = function () {
4017
+ resolve(transaction);
4018
+ };
4019
+ transaction.onabort = transaction.onerror = function () {
4020
+ reject(transaction.error);
4021
+ };
4022
+
4023
+ storeCb(transaction.objectStore(storeName));
4024
+ });
4025
+ };
4026
+
4027
+ return this.dbPromise
4028
+ .then(doTransaction)
4029
+ .catch(function (err) {
4030
+ if (err && err['name'] === 'InvalidStateError') {
4031
+ // try reopening the DB if the connection is closed
4032
+ this.dbPromise = this._openDb();
4033
+ return this.dbPromise.then(doTransaction);
4034
+ } else {
4035
+ return PromisePolyfill.reject(err);
4036
+ }
4037
+ }.bind(this));
4038
+ };
4039
+
4040
+ IDBStorageWrapper.prototype.setItem = function (key, value) {
4041
+ return this.makeTransaction('readwrite', function (objectStore) {
4042
+ objectStore.put(value, key);
4043
+ });
4044
+ };
4045
+
4046
+ IDBStorageWrapper.prototype.getItem = function (key) {
4047
+ var req;
4048
+ return this.makeTransaction('readonly', function (objectStore) {
4049
+ req = objectStore.get(key);
4050
+ }).then(function () {
4051
+ return req.result;
4052
+ });
4053
+ };
4054
+
4055
+ IDBStorageWrapper.prototype.removeItem = function (key) {
4056
+ return this.makeTransaction('readwrite', function (objectStore) {
4057
+ objectStore.delete(key);
4058
+ });
4059
+ };
4060
+
4061
+ IDBStorageWrapper.prototype.getAll = function () {
4062
+ var req;
4063
+ return this.makeTransaction('readonly', function (objectStore) {
4064
+ req = objectStore.getAll();
4065
+ }).then(function () {
4066
+ return req.result;
4067
+ });
4068
+ };
4069
+
4070
+ var logger$5 = console_with_prefix('flags');
4071
+
4072
+ var MIXPANEL_FLAGS_DB_NAME = 'mixpanelFlagsDb';
4073
+ var FLAGS_STORE_NAME = 'mixpanelFlags';
4074
+
4075
+ // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
4076
+ var FLAGS_VERSION_DATA = { version: 1, storeNames: [FLAGS_STORE_NAME] };
4077
+
4078
+ var PERSISTED_VARIANTS_KEY_PREFIX = 'persisted_variants_for_';
4079
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
4080
+
4081
+ var VariantLookupPolicy = Object.freeze({
4082
+ NETWORK_ONLY: 'networkOnly',
4083
+ NETWORK_FIRST: 'networkFirst',
4084
+ PERSISTENCE_UNTIL_NETWORK_SUCCESS: 'persistenceUntilNetworkSuccess'
4085
+ });
4086
+
4087
+ var VALID_POLICIES = [
4088
+ VariantLookupPolicy.NETWORK_ONLY,
4089
+ VariantLookupPolicy.NETWORK_FIRST,
4090
+ VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS
4091
+ ];
4092
+
4093
+ /**
4094
+ * Module for handling the storage and retrieval of persisted feature flag variants.
4095
+ */
4096
+ var FeatureFlagPersistence = function(persistenceConfig, token, isGloballyDisabled) {
4097
+ this.idb = new IDBStorageWrapper(MIXPANEL_FLAGS_DB_NAME, FLAGS_STORE_NAME, FLAGS_VERSION_DATA);
4098
+ this.persistenceConfig = persistenceConfig;
4099
+ this.persistedVariantsKey = PERSISTED_VARIANTS_KEY_PREFIX + token;
4100
+ this.isGloballyDisabled = isGloballyDisabled || function() { return false; };
4101
+ };
4102
+
4103
+ FeatureFlagPersistence.prototype.getPolicy = function() {
4104
+ if (this.isGloballyDisabled() || !this._isConfigValid()) {
4105
+ return VariantLookupPolicy.NETWORK_ONLY;
4106
+ }
4107
+ return this.persistenceConfig['variantLookupPolicy'];
4108
+ };
4109
+
4110
+ FeatureFlagPersistence.prototype.getTtlMs = function() {
4111
+ if (!this._isConfigValid()) {
4112
+ return DEFAULT_TTL_MS;
4113
+ }
4114
+ var configuredTtl = this.persistenceConfig['persistenceTtlMs'];
4115
+ return (configuredTtl === undefined || configuredTtl === null) ? DEFAULT_TTL_MS : configuredTtl;
4116
+ };
4117
+
4118
+ FeatureFlagPersistence.prototype._isConfigValid = function() {
4119
+ var config = this.persistenceConfig;
4120
+ if (!config) {
4121
+ return false;
4122
+ }
4123
+
4124
+ if (VALID_POLICIES.indexOf(config['variantLookupPolicy']) === -1) {
4125
+ logger$5.error('Invalid variantLookupPolicy:', config['variantLookupPolicy']);
4126
+ return false;
4127
+ }
4128
+
4129
+ if (config['persistenceTtlMs'] !== undefined &&
4130
+ config['persistenceTtlMs'] !== null &&
4131
+ config['persistenceTtlMs'] <= 0) {
4132
+ logger$5.error('If provided, persistenceTtlMs must be a positive number. Provided value:', config['persistenceTtlMs']);
4133
+ return false;
4134
+ }
4135
+
4136
+ return true;
4137
+ };
4138
+
4139
+ FeatureFlagPersistence.prototype.loadFlagsFromStorage = function(context) {
4140
+ var clearAndReturnNull = _.bind(function() {
4141
+ return this.clear().then(function() { return null; }).catch(function() { return null; });
4142
+ }, this);
4143
+
4144
+ if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
4145
+ return clearAndReturnNull();
4146
+ }
4147
+
4148
+ var ttlMs = this.getTtlMs();
4149
+
4150
+ return this.idb.init().then(_.bind(function() {
4151
+ return this.idb.getItem(this.persistedVariantsKey);
4152
+ }, this)).then(_.bind(function(data) {
4153
+ if (!data) {
4154
+ logger$5.log('No persisted variants found in IndexedDB');
4155
+ return null;
4156
+ }
4157
+
4158
+ if (ttlMs && Date.now() - data['persistedAt'] >= ttlMs) {
4159
+ logger$5.log('Persisted variants are expiring');
4160
+ return null;
4161
+ }
4162
+
4163
+ if (!context || data['distinctId'] !== context['distinct_id']) {
4164
+ logger$5.log('Persisted variants found, but for a different distinct_id so clearing.');
4165
+ return clearAndReturnNull();
4166
+ }
4167
+
4168
+ var persistedFlags = new Map();
4169
+ _.each(data['flagVariants'], function(variantData, key) {
4170
+ persistedFlags.set(key, {
4171
+ 'key': variantData['variant_key'],
4172
+ 'value': variantData['variant_value'],
4173
+ 'experiment_id': variantData['experiment_id'],
4174
+ 'is_experiment_active': variantData['is_experiment_active'],
4175
+ 'is_qa_tester': variantData['is_qa_tester'],
4176
+ 'variant_source': 'persistence',
4177
+ 'persisted_at_in_ms': data['persistedAt'],
4178
+ 'ttl_in_ms': ttlMs
4179
+ });
4180
+ });
4181
+
4182
+ logger$5.log('Loaded', persistedFlags.size, 'variants from IndexedDB for distinct_id', data['distinctId']);
4183
+
4184
+ return {
4185
+ flags: persistedFlags,
4186
+ pendingFirstTimeEvents: data['pendingFirstTimeEvents'] || {},
4187
+ persistedAtMs: data['persistedAt'],
4188
+ ttlMs: ttlMs
4189
+ };
4190
+ }, this)).catch(_.bind(function(error) {
4191
+ logger$5.error('Failed to load persisted variants from IndexedDB, so clearing', error);
4192
+ return clearAndReturnNull();
4193
+ }, this));
4194
+ };
4195
+
4196
+ FeatureFlagPersistence.prototype.save = function(context, flagsMap, pendingFirstTimeEvents) {
4197
+ if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
4198
+ return Promise.resolve();
4199
+ }
4200
+
4201
+ var flagVariants = {};
4202
+ flagsMap.forEach(function(variant, key) {
4203
+ flagVariants[key] = {
4204
+ 'variant_key': variant['key'],
4205
+ 'variant_value': variant['value'],
4206
+ 'experiment_id': variant['experiment_id'],
4207
+ 'is_experiment_active': variant['is_experiment_active'],
4208
+ 'is_qa_tester': variant['is_qa_tester']
4209
+ };
4210
+ });
4211
+
4212
+ var data = {
4213
+ 'persistedAt': Date.now(),
4214
+ 'distinctId': context && context['distinct_id'],
4215
+ 'context': context,
4216
+ 'flagVariants': flagVariants,
4217
+ 'pendingFirstTimeEvents': pendingFirstTimeEvents || {}
4218
+ };
4219
+
4220
+ return this.idb.init().then(_.bind(function() {
4221
+ return this.idb.setItem(this.persistedVariantsKey, data);
4222
+ }, this)).then(function() {
4223
+ logger$5.log('Saved', flagsMap.size, 'variants to IndexedDB for distinct_id', data['distinctId']);
4224
+ }).catch(function(error) {
4225
+ logger$5.error('Failed to persist variants to IndexedDB:', error);
4226
+ });
4227
+ };
4228
+
4229
+ FeatureFlagPersistence.prototype.clear = function() {
4230
+ if (this.isGloballyDisabled()) {
4231
+ return Promise.resolve();
4232
+ }
4233
+ return this.idb.init().then(_.bind(function() {
4234
+ return this.idb.removeItem(this.persistedVariantsKey);
4235
+ }, this)).then(function() {
4236
+ logger$5.log('Cleared persisted variants from IndexedDB');
4237
+ }).catch(function(error) {
4238
+ logger$5.error('Failed to clear persisted variants from IndexedDB:', error);
4239
+ });
4240
+ };
4241
+
3927
4242
  var logger$4 = console_with_prefix('flags');
3928
4243
  var FLAGS_CONFIG_KEY = 'flags';
3929
4244
 
3930
4245
  var CONFIG_CONTEXT = 'context';
4246
+ var CONFIG_PERSISTENCE = 'persistence';
3931
4247
  var CONFIG_DEFAULTS = {};
3932
4248
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
3933
4249
 
@@ -3950,6 +4266,13 @@
3950
4266
  return eventKey.split(':')[0];
3951
4267
  };
3952
4268
 
4269
+ var withFallbackSource = function(fallback) {
4270
+ if (_.isObject(fallback)) {
4271
+ return _.extend({}, fallback, {'variant_source': 'fallback'});
4272
+ }
4273
+ return {'value': fallback, 'variant_source': 'fallback'};
4274
+ };
4275
+
3953
4276
  /**
3954
4277
  * FeatureFlagManager: support for Mixpanel's feature flagging product
3955
4278
  * @constructor
@@ -3972,11 +4295,63 @@
3972
4295
  }
3973
4296
 
3974
4297
  this.flags = null;
3975
- this.fetchFlags();
3976
-
3977
4298
  this.trackedFeatures = new Set();
3978
4299
  this.pendingFirstTimeEvents = {};
3979
4300
  this.activatedFirstTimeEvents = {};
4301
+ this._loadedPersistedAtMs = null;
4302
+ this._loadedTtlMs = null;
4303
+
4304
+ this.persistence = new FeatureFlagPersistence(
4305
+ this.getConfig(CONFIG_PERSISTENCE),
4306
+ this.getMpConfig('token'),
4307
+ _.bind(function() { return this.getMpConfig('disable_persistence'); }, this)
4308
+ );
4309
+
4310
+ this.persistenceLoadedPromise = this.persistence.loadFlagsFromStorage(this._buildContext())
4311
+ .then(_.bind(function(loaded) {
4312
+ if (loaded) {
4313
+ this.flags = loaded.flags;
4314
+ this.pendingFirstTimeEvents = loaded.pendingFirstTimeEvents;
4315
+ this._loadedPersistedAtMs = loaded.persistedAtMs;
4316
+ this._loadedTtlMs = loaded.ttlMs;
4317
+ }
4318
+ }, this));
4319
+
4320
+ return this.persistenceLoadedPromise
4321
+ .then(_.bind(function() {
4322
+ return this.fetchFlags();
4323
+ }, this))
4324
+ .catch(function() {
4325
+ logger$4.error('Error initializing feature flags');
4326
+ });
4327
+ };
4328
+
4329
+ FeatureFlagManager.prototype._buildContext = function() {
4330
+ return _.extend(
4331
+ {'distinct_id': this.getMpProperty('distinct_id'), 'device_id': this.getMpProperty('$device_id')},
4332
+ this.getConfig(CONFIG_CONTEXT)
4333
+ );
4334
+ };
4335
+
4336
+ FeatureFlagManager.prototype.reset = function() {
4337
+ if (!this.persistence) {
4338
+ return Promise.resolve();
4339
+ }
4340
+
4341
+ this.flags = null;
4342
+ this.pendingFirstTimeEvents = {};
4343
+ this.activatedFirstTimeEvents = {};
4344
+ this.trackedFeatures = new Set();
4345
+ this.fetchPromise = null;
4346
+ this._fetchInProgressStartTime = null;
4347
+ this._loadedPersistedAtMs = null;
4348
+ this._loadedTtlMs = null;
4349
+
4350
+ return this.persistence.clear().then(_.bind(function() {
4351
+ return this.fetchFlags();
4352
+ }, this)).catch(function() {
4353
+ logger$4.error('Error during flags reset');
4354
+ });
3980
4355
  };
3981
4356
 
3982
4357
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -4013,8 +4388,12 @@
4013
4388
  var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
4014
4389
  ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
4015
4390
 
4016
- this.setMpConfig(FLAGS_CONFIG_KEY, ffConfig);
4017
- return this.fetchFlags();
4391
+ var configUpdate = {};
4392
+ configUpdate[FLAGS_CONFIG_KEY] = ffConfig;
4393
+ this.setMpConfig(configUpdate);
4394
+ return this.fetchFlags().catch(function() {
4395
+ logger$4.error('Error fetching flags during updateContext');
4396
+ });
4018
4397
  };
4019
4398
 
4020
4399
  FeatureFlagManager.prototype.areFlagsReady = function() {
@@ -4029,12 +4408,11 @@
4029
4408
  return Promise.resolve();
4030
4409
  }
4031
4410
 
4032
- var distinctId = this.getMpProperty('distinct_id');
4033
- var deviceId = this.getMpProperty('$device_id');
4411
+ var context = this._buildContext();
4412
+ var distinctId = context['distinct_id'];
4034
4413
  var traceparent = generateTraceparent();
4035
4414
  logger$4.log('Fetching flags for distinct ID: ' + distinctId);
4036
4415
 
4037
- var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
4038
4416
  var searchParams = new URLSearchParams();
4039
4417
  searchParams.set('context', JSON.stringify(context));
4040
4418
  searchParams.set('token', this.getMpConfig('token'));
@@ -4051,96 +4429,116 @@
4051
4429
  }
4052
4430
  }).then(function(response) {
4053
4431
  this.markFetchComplete();
4054
- return response.json().then(function(responseBody) {
4055
- var responseFlags = responseBody['flags'];
4056
- if (!responseFlags) {
4057
- throw new Error('No flags in API response');
4058
- }
4059
- var flags = new Map();
4060
- var pendingFirstTimeEvents = {};
4061
-
4062
- // Process flags from response
4063
- _.each(responseFlags, function(data, key) {
4064
- // Check if this flag has any activated first-time events this session
4065
- var hasActivatedEvent = false;
4066
- var prefix = key + ':';
4067
- _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4068
- if (eventKey.startsWith(prefix)) {
4069
- hasActivatedEvent = true;
4070
- }
4071
- });
4432
+ return response.json();
4433
+ }.bind(this)).then(function(responseBody) {
4434
+ var responseFlags = responseBody['flags'];
4435
+ if (!responseFlags) {
4436
+ throw new Error('No flags in API response');
4437
+ }
4438
+ var flags = new Map();
4439
+ var pendingFirstTimeEvents = {};
4440
+
4441
+ // Process flags from response
4442
+ _.each(responseFlags, function(data, key) {
4443
+ // Check if this flag has any activated first-time events this session
4444
+ var hasActivatedEvent = false;
4445
+ var prefix = key + ':';
4446
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4447
+ if (eventKey.startsWith(prefix)) {
4448
+ hasActivatedEvent = true;
4449
+ }
4450
+ });
4072
4451
 
4073
- if (hasActivatedEvent) {
4074
- // Preserve the activated variant, don't overwrite with server's current variant
4075
- var currentFlag = this.flags && this.flags.get(key);
4076
- if (currentFlag) {
4077
- flags.set(key, currentFlag);
4078
- }
4079
- } else {
4080
- // Use server's current variant
4081
- flags.set(key, {
4082
- 'key': data['variant_key'],
4083
- 'value': data['variant_value'],
4084
- 'experiment_id': data['experiment_id'],
4085
- 'is_experiment_active': data['is_experiment_active'],
4086
- 'is_qa_tester': data['is_qa_tester']
4087
- });
4452
+ if (hasActivatedEvent) {
4453
+ // Preserve the activated variant, don't overwrite with server's current variant
4454
+ var currentFlag = this.flags && this.flags.get(key);
4455
+ if (currentFlag) {
4456
+ flags.set(key, currentFlag);
4088
4457
  }
4089
- }, this);
4458
+ } else {
4459
+ // Use server's current variant
4460
+ flags.set(key, {
4461
+ 'key': data['variant_key'],
4462
+ 'value': data['variant_value'],
4463
+ 'experiment_id': data['experiment_id'],
4464
+ 'is_experiment_active': data['is_experiment_active'],
4465
+ 'is_qa_tester': data['is_qa_tester'],
4466
+ 'variant_source': 'network'
4467
+ });
4468
+ }
4469
+ }, this);
4090
4470
 
4091
- // Process top-level pending_first_time_events array
4092
- var topLevelDefinitions = responseBody['pending_first_time_events'];
4093
- if (topLevelDefinitions && topLevelDefinitions.length > 0) {
4094
- _.each(topLevelDefinitions, function(def) {
4095
- var flagKey = def['flag_key'];
4096
- var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
4471
+ // Process top-level pending_first_time_events array
4472
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
4473
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
4474
+ _.each(topLevelDefinitions, function(def) {
4475
+ var flagKey = def['flag_key'];
4476
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
4097
4477
 
4098
- // Skip if this specific event has already been activated this session
4099
- if (this.activatedFirstTimeEvents[eventKey]) {
4100
- return;
4101
- }
4478
+ // Skip if this specific event has already been activated this session
4479
+ if (this.activatedFirstTimeEvents[eventKey]) {
4480
+ return;
4481
+ }
4102
4482
 
4103
- // Store pending event definition using composite key
4104
- pendingFirstTimeEvents[eventKey] = {
4105
- 'flag_key': flagKey,
4106
- 'flag_id': def['flag_id'],
4107
- 'project_id': def['project_id'],
4108
- 'first_time_event_hash': def['first_time_event_hash'],
4109
- 'event_name': def['event_name'],
4110
- 'property_filters': def['property_filters'],
4111
- 'pending_variant': def['pending_variant']
4112
- };
4113
- }, this);
4114
- }
4483
+ // Store pending event definition using composite key
4484
+ pendingFirstTimeEvents[eventKey] = {
4485
+ 'flag_key': flagKey,
4486
+ 'flag_id': def['flag_id'],
4487
+ 'project_id': def['project_id'],
4488
+ 'first_time_event_hash': def['first_time_event_hash'],
4489
+ 'event_name': def['event_name'],
4490
+ 'property_filters': def['property_filters'],
4491
+ 'pending_variant': def['pending_variant']
4492
+ };
4493
+ }, this);
4494
+ }
4115
4495
 
4116
- // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
4117
- if (this.activatedFirstTimeEvents) {
4118
- _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4119
- var flagKey = getFlagKeyFromPendingEventKey(eventKey);
4120
- if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
4121
- // Keep the activated flag even though it's not in the new response
4122
- flags.set(flagKey, this.flags.get(flagKey));
4123
- }
4124
- }, this);
4125
- }
4496
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
4497
+ if (this.activatedFirstTimeEvents) {
4498
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
4499
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
4500
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
4501
+ // Keep the activated flag even though it's not in the new response
4502
+ flags.set(flagKey, this.flags.get(flagKey));
4503
+ }
4504
+ }, this);
4505
+ }
4126
4506
 
4127
- this.flags = flags;
4128
- this.pendingFirstTimeEvents = pendingFirstTimeEvents;
4129
- this._traceparent = traceparent;
4507
+ this.flags = flags;
4508
+ this.trackedFeatures = new Set();
4509
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
4510
+ this._loadedPersistedAtMs = null;
4511
+ this._loadedTtlMs = null;
4512
+ this._traceparent = traceparent;
4130
4513
 
4131
- this._loadTargetingIfNeeded();
4132
- }.bind(this)).catch(function(error) {
4133
- this.markFetchComplete();
4134
- logger$4.error(error);
4135
- }.bind(this));
4514
+ this._loadTargetingIfNeeded();
4515
+
4516
+ this.persistence.save(context, this.flags, this.pendingFirstTimeEvents);
4136
4517
  }.bind(this)).catch(function(error) {
4137
- this.markFetchComplete();
4518
+ if (this._fetchInProgressStartTime) {
4519
+ this.markFetchComplete();
4520
+ }
4138
4521
  logger$4.error(error);
4522
+ throw error;
4139
4523
  }.bind(this));
4140
4524
 
4141
4525
  return this.fetchPromise;
4142
4526
  };
4143
4527
 
4528
+ FeatureFlagManager.prototype.loadFlags = function() {
4529
+ if (!this.isSystemEnabled()) {
4530
+ return Promise.resolve();
4531
+ }
4532
+ if (!this.trackedFeatures) {
4533
+ logger$4.error('loadFlags called before init');
4534
+ return Promise.resolve();
4535
+ }
4536
+ if (this._fetchInProgressStartTime) {
4537
+ return this.fetchPromise;
4538
+ }
4539
+ return this.fetchFlags();
4540
+ };
4541
+
4144
4542
  FeatureFlagManager.prototype.markFetchComplete = function() {
4145
4543
  if (!this._fetchInProgressStartTime) {
4146
4544
  logger$4.error('Fetch in progress started time not set, cannot mark fetch complete');
@@ -4275,6 +4673,7 @@
4275
4673
  };
4276
4674
 
4277
4675
  this.flags.set(flagKey, newVariant);
4676
+ this.trackedFeatures.delete(flagKey);
4278
4677
  this.activatedFirstTimeEvents[eventKey] = true;
4279
4678
 
4280
4679
  this.recordFirstTimeEvent(
@@ -4324,35 +4723,106 @@
4324
4723
  };
4325
4724
 
4326
4725
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
4327
- if (!this.fetchPromise) {
4726
+ if (!this.persistenceLoadedPromise) {
4328
4727
  return new Promise(function(resolve) {
4329
4728
  logger$4.critical('Feature Flags not initialized');
4330
- resolve(fallback);
4729
+ resolve(withFallbackSource(fallback));
4331
4730
  });
4332
4731
  }
4333
4732
 
4334
- return this.fetchPromise.then(function() {
4335
- return this.getVariantSync(featureName, fallback);
4336
- }.bind(this)).catch(function(error) {
4337
- logger$4.error(error);
4338
- return fallback;
4339
- });
4733
+ var policy = this.persistence.getPolicy();
4734
+
4735
+ return this.persistenceLoadedPromise.then(_.bind(function() {
4736
+ // 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.
4737
+ if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
4738
+ if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
4739
+ return this.getVariantSync(featureName, fallback);
4740
+ }
4741
+ if (!this.fetchPromise) {
4742
+ return withFallbackSource(fallback);
4743
+ }
4744
+ return this.fetchPromise.then(_.bind(function() {
4745
+ return this.getVariantSync(featureName, fallback);
4746
+ }, this)).catch(function(error) {
4747
+ logger$4.error(error);
4748
+ return withFallbackSource(fallback);
4749
+ });
4750
+ }
4751
+
4752
+ var serve = _.bind(function() { return this.getVariantSync(featureName, fallback); }, this);
4753
+ if (!this.fetchPromise) {
4754
+ return withFallbackSource(fallback);
4755
+ }
4756
+ return this.fetchPromise.then(serve).catch(serve);
4757
+ }, this));
4758
+ };
4759
+
4760
+ FeatureFlagManager.prototype._loadedPersistenceIsStale = function() {
4761
+ if (!this._loadedPersistedAtMs || !this._loadedTtlMs) {
4762
+ return false;
4763
+ }
4764
+ return Date.now() - this._loadedPersistedAtMs >= this._loadedTtlMs;
4340
4765
  };
4341
4766
 
4342
4767
  FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
4768
+ if (this._loadedPersistenceIsStale()) {
4769
+ logger$4.log('Loaded persisted variants are past TTL so returning fallback for "' + featureName + '"');
4770
+ return withFallbackSource(fallback);
4771
+ }
4343
4772
  if (!this.areFlagsReady()) {
4344
4773
  logger$4.log('Flags not loaded yet');
4345
- return fallback;
4774
+ return withFallbackSource(fallback);
4346
4775
  }
4347
4776
  var feature = this.flags.get(featureName);
4348
4777
  if (!feature) {
4349
4778
  logger$4.log('No flag found: "' + featureName + '"');
4350
- return fallback;
4779
+ return withFallbackSource(fallback);
4351
4780
  }
4352
4781
  this.trackFeatureCheck(featureName, feature);
4353
4782
  return feature;
4354
4783
  };
4355
4784
 
4785
+ FeatureFlagManager.prototype.getAllVariants = function() {
4786
+ if (!this.persistenceLoadedPromise) {
4787
+ logger$4.critical('Feature Flags not initialized');
4788
+ return Promise.resolve(new Map());
4789
+ }
4790
+
4791
+ var policy = this.persistence.getPolicy();
4792
+
4793
+ return this.persistenceLoadedPromise.then(_.bind(function() {
4794
+ // 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.
4795
+ if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
4796
+ if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
4797
+ return this.getAllVariantsSync();
4798
+ }
4799
+ if (!this.fetchPromise) {
4800
+ return new Map();
4801
+ }
4802
+ return this.fetchPromise.then(_.bind(function() {
4803
+ return this.getAllVariantsSync();
4804
+ }, this)).catch(function(error) {
4805
+ logger$4.error(error);
4806
+ return new Map();
4807
+ });
4808
+ }
4809
+
4810
+ var serve = _.bind(this.getAllVariantsSync, this);
4811
+ if (!this.fetchPromise) {
4812
+ return new Map();
4813
+ }
4814
+ return this.fetchPromise.then(serve).catch(serve);
4815
+ }, this));
4816
+ };
4817
+
4818
+ FeatureFlagManager.prototype.getAllVariantsSync = function() {
4819
+ if (this._loadedPersistenceIsStale()) {
4820
+ logger$4.log('Loaded persisted variants are past TTL so returning empty Map');
4821
+ return new Map();
4822
+ }
4823
+ return this.flags || new Map();
4824
+ };
4825
+
4356
4826
  FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
4357
4827
  return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
4358
4828
  return feature['value'];
@@ -4391,6 +4861,10 @@
4391
4861
  return val;
4392
4862
  };
4393
4863
 
4864
+ function isPresent(v) {
4865
+ return v !== undefined && v !== null;
4866
+ }
4867
+
4394
4868
  FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
4395
4869
  if (this.trackedFeatures.has(featureName)) {
4396
4870
  return;
@@ -4401,25 +4875,41 @@
4401
4875
  'Experiment name': featureName,
4402
4876
  'Variant name': feature['key'],
4403
4877
  '$experiment_type': 'feature_flag',
4404
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
4405
- 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
4878
+ 'Variant fetch start time': isPresent(this._fetchStartTime) ? new Date(this._fetchStartTime).toISOString() : null,
4879
+ 'Variant fetch complete time': isPresent(this._fetchCompleteTime) ? new Date(this._fetchCompleteTime).toISOString() : null,
4406
4880
  'Variant fetch latency (ms)': this._fetchLatency,
4407
4881
  'Variant fetch traceparent': this._traceparent,
4408
4882
  };
4409
4883
 
4410
- if (feature['experiment_id'] !== 'undefined') {
4884
+ if (isPresent(feature['experiment_id'])) {
4411
4885
  trackingProperties['$experiment_id'] = feature['experiment_id'];
4412
4886
  }
4413
- if (feature['is_experiment_active'] !== 'undefined') {
4887
+ if (isPresent(feature['is_experiment_active'])) {
4414
4888
  trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
4415
4889
  }
4416
- if (feature['is_qa_tester'] !== 'undefined') {
4890
+ if (isPresent(feature['is_qa_tester'])) {
4417
4891
  trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
4418
4892
  }
4893
+ if (isPresent(feature['variant_source'])) {
4894
+ trackingProperties['$variant_source'] = feature['variant_source'];
4895
+ }
4896
+ if (isPresent(feature['persisted_at_in_ms'])) {
4897
+ trackingProperties['$persisted_at_in_ms'] = feature['persisted_at_in_ms'];
4898
+ }
4899
+ if (isPresent(feature['ttl_in_ms'])) {
4900
+ trackingProperties['$ttl_in_ms'] = feature['ttl_in_ms'];
4901
+ }
4419
4902
 
4420
4903
  this.track('$experiment_started', trackingProperties);
4421
4904
  };
4422
4905
 
4906
+ FeatureFlagManager.prototype.whenReady = function() {
4907
+ if (this.fetchPromise) {
4908
+ return this.fetchPromise;
4909
+ }
4910
+ return Promise.resolve();
4911
+ };
4912
+
4423
4913
  FeatureFlagManager.prototype.minApisSupported = function() {
4424
4914
  return !!this.fetch &&
4425
4915
  typeof Promise !== 'undefined' &&
@@ -4432,11 +4922,15 @@
4432
4922
  FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
4433
4923
  FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
4434
4924
  FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
4925
+ FeatureFlagManager.prototype['get_all_variants'] = FeatureFlagManager.prototype.getAllVariants;
4926
+ FeatureFlagManager.prototype['get_all_variants_sync'] = FeatureFlagManager.prototype.getAllVariantsSync;
4435
4927
  FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
4436
4928
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
4437
4929
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
4438
4930
  FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
4931
+ FeatureFlagManager.prototype['load_flags'] = FeatureFlagManager.prototype.loadFlags;
4439
4932
  FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
4933
+ FeatureFlagManager.prototype['when_ready'] = FeatureFlagManager.prototype.whenReady;
4440
4934
 
4441
4935
  // Deprecated method
4442
4936
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
@@ -4444,131 +4938,14 @@
4444
4938
  // Exports intended only for testing
4445
4939
  FeatureFlagManager.prototype['getTargeting'] = FeatureFlagManager.prototype.getTargeting;
4446
4940
 
4447
- var MIXPANEL_DB_NAME = 'mixpanelBrowserDb';
4448
-
4941
+ var MIXPANEL_BROWSER_DB_NAME = 'mixpanelBrowserDb';
4449
4942
  var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
4450
4943
  var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
4451
4944
 
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];
4455
-
4456
- /**
4457
- * @type {import('./wrapper').StorageWrapper}
4458
- */
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
- };
4473
-
4474
- openRequest['onsuccess'] = function () {
4475
- resolve(openRequest.result);
4476
- };
4477
-
4478
- openRequest['onupgradeneeded'] = function (ev) {
4479
- var db = ev.target.result;
4480
-
4481
- OBJECT_STORES.forEach(function (storeName) {
4482
- db.createObjectStore(storeName);
4483
- });
4484
- };
4485
- });
4486
- };
4487
-
4488
- IDBStorageWrapper.prototype.init = function () {
4489
- if (!win.indexedDB) {
4490
- return PromisePolyfill.reject('indexedDB is not supported in this browser');
4491
- }
4492
-
4493
- if (!this.dbPromise) {
4494
- this.dbPromise = this._openDb();
4495
- }
4496
-
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
- }
4504
- });
4505
- };
4506
-
4507
- IDBStorageWrapper.prototype.isInitialized = function () {
4508
- return !!this.dbPromise;
4509
- };
4510
-
4511
- /**
4512
- * @param {IDBTransactionMode} mode
4513
- * @param {function(IDBObjectStore): void} storeCb
4514
- */
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
-
4527
- storeCb(transaction.objectStore(storeName));
4528
- });
4529
- };
4530
-
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
- };
4543
-
4544
- IDBStorageWrapper.prototype.setItem = function (key, value) {
4545
- return this.makeTransaction('readwrite', function (objectStore) {
4546
- objectStore.put(value, key);
4547
- });
4548
- };
4549
-
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
- };
4558
-
4559
- IDBStorageWrapper.prototype.removeItem = function (key) {
4560
- return this.makeTransaction('readwrite', function (objectStore) {
4561
- objectStore.delete(key);
4562
- });
4563
- };
4564
-
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
- });
4945
+ // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
4946
+ var RECORDER_VERSION_DATA = {
4947
+ version: 1,
4948
+ storeNames: [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME]
4572
4949
  };
4573
4950
 
4574
4951
  /**
@@ -4641,7 +5018,7 @@
4641
5018
  return PromisePolyfill.resolve(false);
4642
5019
  }
4643
5020
 
4644
- var recording_registry_idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
5021
+ var recording_registry_idb = new IDBStorageWrapper(MIXPANEL_BROWSER_DB_NAME, RECORDING_REGISTRY_STORE_NAME, RECORDER_VERSION_DATA);
4645
5022
  var tab_id = this.getTabId();
4646
5023
  return recording_registry_idb.init()
4647
5024
  .then(function () {
@@ -5097,7 +5474,7 @@
5097
5474
  options = options || {};
5098
5475
 
5099
5476
  this.storageKey = key;
5100
- this.storage = options.storage || win.localStorage;
5477
+ this.storage = options.storage || getLocalStorage();
5101
5478
  this.pollIntervalMS = options.pollIntervalMS || 100;
5102
5479
  this.timeoutMS = options.timeoutMS || 2000;
5103
5480
 
@@ -5225,10 +5602,13 @@
5225
5602
  * @type {import('./wrapper').StorageWrapper}
5226
5603
  */
5227
5604
  var LocalStorageWrapper = function (storageOverride) {
5228
- this.storage = storageOverride || win.localStorage;
5605
+ this.storage = storageOverride || getLocalStorage();
5229
5606
  };
5230
5607
 
5231
5608
  LocalStorageWrapper.prototype.init = function () {
5609
+ if (!this.storage) {
5610
+ return PromisePolyfill.reject(new Error('localStorage is not available'));
5611
+ }
5232
5612
  return PromisePolyfill.resolve();
5233
5613
  };
5234
5614
 
@@ -5295,7 +5675,7 @@
5295
5675
  if (this.usePersistence) {
5296
5676
  this.queueStorage = options.queueStorage || new LocalStorageWrapper();
5297
5677
  this.lock = new SharedLock(storageKey, {
5298
- storage: options.sharedLockStorage || win.localStorage,
5678
+ storage: options.sharedLockStorage,
5299
5679
  timeoutMS: options.sharedLockTimeoutMS,
5300
5680
  });
5301
5681
  }
@@ -7760,6 +8140,7 @@
7760
8140
  'disable_all_events': false,
7761
8141
  'identify_called': false
7762
8142
  };
8143
+ this._remote_settings_strict_disabled = false;
7763
8144
 
7764
8145
  // set up request queueing/batching
7765
8146
  this.request_batchers = {};
@@ -7834,9 +8215,6 @@
7834
8215
  this.flags.init();
7835
8216
  this['flags'] = this.flags;
7836
8217
 
7837
- this.autocapture = new Autocapture(this);
7838
- this.autocapture.init();
7839
-
7840
8218
  this._init_tab_id();
7841
8219
 
7842
8220
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
@@ -7848,6 +8226,9 @@
7848
8226
  } else {
7849
8227
  this.__session_recording_init_promise = this._check_and_start_session_recording();
7850
8228
  }
8229
+
8230
+ this.autocapture = new Autocapture(this);
8231
+ this.autocapture.init();
7851
8232
  };
7852
8233
 
7853
8234
  /**
@@ -7894,9 +8275,19 @@
7894
8275
  return this.recorderManager.checkAndStartSessionRecording(force_start);
7895
8276
  });
7896
8277
 
7897
- MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
7898
- return this.recorderManager.startRecordingOnEvent(event_name, properties);
7899
- };
8278
+ MixpanelLib.prototype._start_recording_on_event = safewrap(function(event_name, properties) {
8279
+ // Wait for recording init to complete before evaluating event triggers.
8280
+ // This ensures recording_event_triggers config is fully loaded when remote settings are used.
8281
+ if (this.__session_recording_init_promise) {
8282
+ this.__session_recording_init_promise.then(_.bind(function() {
8283
+ // In strict mode, skip recording if remote settings failed
8284
+ if (this._remote_settings_strict_disabled) {
8285
+ return;
8286
+ }
8287
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
8288
+ }, this));
8289
+ }
8290
+ });
7900
8291
 
7901
8292
  MixpanelLib.prototype.start_session_recording = function () {
7902
8293
  return this._check_and_start_session_recording(true);
@@ -8195,6 +8586,7 @@
8195
8586
  var disableRecordingIfStrict = function() {
8196
8587
  if (mode === 'strict') {
8197
8588
  self.set_config({'record_sessions_percent': 0});
8589
+ self._remote_settings_strict_disabled = true;
8198
8590
  }
8199
8591
  };
8200
8592
 
@@ -8820,6 +9212,10 @@
8820
9212
  properties
8821
9213
  );
8822
9214
 
9215
+ if (this.is_recording_heatmap_data()) {
9216
+ event_properties['$captured_for_heatmap'] = true;
9217
+ }
9218
+
8823
9219
  return this.track(event_name, event_properties);
8824
9220
  });
8825
9221
 
@@ -9143,7 +9539,9 @@
9143
9539
 
9144
9540
  // check feature flags again if distinct id has changed
9145
9541
  if (new_distinct_id !== previous_distinct_id) {
9146
- this.flags.fetchFlags();
9542
+ this.flags.fetchFlags().catch(function() {
9543
+ console.error('[flags] Error fetching flags during identify');
9544
+ });
9147
9545
  }
9148
9546
  };
9149
9547
 
@@ -9161,6 +9559,7 @@
9161
9559
  '$device_id': uuid
9162
9560
  }, '');
9163
9561
  this._check_and_start_session_recording();
9562
+ this.flags.reset();
9164
9563
  };
9165
9564
 
9166
9565
  /**