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
@@ -0,0 +1,176 @@
1
+ import { _, console_with_prefix } from '../utils'; // eslint-disable-line camelcase
2
+ import { IDBStorageWrapper } from '../storage/indexed-db';
3
+
4
+ var logger = console_with_prefix('flags');
5
+
6
+ var MIXPANEL_FLAGS_DB_NAME = 'mixpanelFlagsDb';
7
+ var FLAGS_STORE_NAME = 'mixpanelFlags';
8
+
9
+ // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
10
+ var FLAGS_VERSION_DATA = { version: 1, storeNames: [FLAGS_STORE_NAME] };
11
+
12
+ var PERSISTED_VARIANTS_KEY_PREFIX = 'persisted_variants_for_';
13
+ var DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
14
+
15
+ var VariantLookupPolicy = Object.freeze({
16
+ NETWORK_ONLY: 'networkOnly',
17
+ NETWORK_FIRST: 'networkFirst',
18
+ PERSISTENCE_UNTIL_NETWORK_SUCCESS: 'persistenceUntilNetworkSuccess'
19
+ });
20
+
21
+ var VALID_POLICIES = [
22
+ VariantLookupPolicy.NETWORK_ONLY,
23
+ VariantLookupPolicy.NETWORK_FIRST,
24
+ VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS
25
+ ];
26
+
27
+ /**
28
+ * Module for handling the storage and retrieval of persisted feature flag variants.
29
+ */
30
+ var FeatureFlagPersistence = function(persistenceConfig, token, isGloballyDisabled) {
31
+ this.idb = new IDBStorageWrapper(MIXPANEL_FLAGS_DB_NAME, FLAGS_STORE_NAME, FLAGS_VERSION_DATA);
32
+ this.persistenceConfig = persistenceConfig;
33
+ this.persistedVariantsKey = PERSISTED_VARIANTS_KEY_PREFIX + token;
34
+ this.isGloballyDisabled = isGloballyDisabled || function() { return false; };
35
+ };
36
+
37
+ FeatureFlagPersistence.prototype.getPolicy = function() {
38
+ if (this.isGloballyDisabled() || !this._isConfigValid()) {
39
+ return VariantLookupPolicy.NETWORK_ONLY;
40
+ }
41
+ return this.persistenceConfig['variantLookupPolicy'];
42
+ };
43
+
44
+ FeatureFlagPersistence.prototype.getTtlMs = function() {
45
+ if (!this._isConfigValid()) {
46
+ return DEFAULT_TTL_MS;
47
+ }
48
+ var configuredTtl = this.persistenceConfig['persistenceTtlMs'];
49
+ return (configuredTtl === undefined || configuredTtl === null) ? DEFAULT_TTL_MS : configuredTtl;
50
+ };
51
+
52
+ FeatureFlagPersistence.prototype._isConfigValid = function() {
53
+ var config = this.persistenceConfig;
54
+ if (!config) {
55
+ return false;
56
+ }
57
+
58
+ if (VALID_POLICIES.indexOf(config['variantLookupPolicy']) === -1) {
59
+ logger.error('Invalid variantLookupPolicy:', config['variantLookupPolicy']);
60
+ return false;
61
+ }
62
+
63
+ if (config['persistenceTtlMs'] !== undefined &&
64
+ config['persistenceTtlMs'] !== null &&
65
+ config['persistenceTtlMs'] <= 0) {
66
+ logger.error('If provided, persistenceTtlMs must be a positive number. Provided value:', config['persistenceTtlMs']);
67
+ return false;
68
+ }
69
+
70
+ return true;
71
+ };
72
+
73
+ FeatureFlagPersistence.prototype.loadFlagsFromStorage = function(context) {
74
+ var clearAndReturnNull = _.bind(function() {
75
+ return this.clear().then(function() { return null; }).catch(function() { return null; });
76
+ }, this);
77
+
78
+ if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
79
+ return clearAndReturnNull();
80
+ }
81
+
82
+ var ttlMs = this.getTtlMs();
83
+
84
+ return this.idb.init().then(_.bind(function() {
85
+ return this.idb.getItem(this.persistedVariantsKey);
86
+ }, this)).then(_.bind(function(data) {
87
+ if (!data) {
88
+ logger.log('No persisted variants found in IndexedDB');
89
+ return null;
90
+ }
91
+
92
+ if (ttlMs && Date.now() - data['persistedAt'] >= ttlMs) {
93
+ logger.log('Persisted variants are expiring');
94
+ return null;
95
+ }
96
+
97
+ if (!context || data['distinctId'] !== context['distinct_id']) {
98
+ logger.log('Persisted variants found, but for a different distinct_id so clearing.');
99
+ return clearAndReturnNull();
100
+ }
101
+
102
+ var persistedFlags = new Map();
103
+ _.each(data['flagVariants'], function(variantData, key) {
104
+ persistedFlags.set(key, {
105
+ 'key': variantData['variant_key'],
106
+ 'value': variantData['variant_value'],
107
+ 'experiment_id': variantData['experiment_id'],
108
+ 'is_experiment_active': variantData['is_experiment_active'],
109
+ 'is_qa_tester': variantData['is_qa_tester'],
110
+ 'variant_source': 'persistence',
111
+ 'persisted_at_in_ms': data['persistedAt'],
112
+ 'ttl_in_ms': ttlMs
113
+ });
114
+ });
115
+
116
+ logger.log('Loaded', persistedFlags.size, 'variants from IndexedDB for distinct_id', data['distinctId']);
117
+
118
+ return {
119
+ flags: persistedFlags,
120
+ pendingFirstTimeEvents: data['pendingFirstTimeEvents'] || {},
121
+ persistedAtMs: data['persistedAt'],
122
+ ttlMs: ttlMs
123
+ };
124
+ }, this)).catch(_.bind(function(error) {
125
+ logger.error('Failed to load persisted variants from IndexedDB, so clearing', error);
126
+ return clearAndReturnNull();
127
+ }, this));
128
+ };
129
+
130
+ FeatureFlagPersistence.prototype.save = function(context, flagsMap, pendingFirstTimeEvents) {
131
+ if (this.getPolicy() === VariantLookupPolicy.NETWORK_ONLY) {
132
+ return Promise.resolve();
133
+ }
134
+
135
+ var flagVariants = {};
136
+ flagsMap.forEach(function(variant, key) {
137
+ flagVariants[key] = {
138
+ 'variant_key': variant['key'],
139
+ 'variant_value': variant['value'],
140
+ 'experiment_id': variant['experiment_id'],
141
+ 'is_experiment_active': variant['is_experiment_active'],
142
+ 'is_qa_tester': variant['is_qa_tester']
143
+ };
144
+ });
145
+
146
+ var data = {
147
+ 'persistedAt': Date.now(),
148
+ 'distinctId': context && context['distinct_id'],
149
+ 'context': context,
150
+ 'flagVariants': flagVariants,
151
+ 'pendingFirstTimeEvents': pendingFirstTimeEvents || {}
152
+ };
153
+
154
+ return this.idb.init().then(_.bind(function() {
155
+ return this.idb.setItem(this.persistedVariantsKey, data);
156
+ }, this)).then(function() {
157
+ logger.log('Saved', flagsMap.size, 'variants to IndexedDB for distinct_id', data['distinctId']);
158
+ }).catch(function(error) {
159
+ logger.error('Failed to persist variants to IndexedDB:', error);
160
+ });
161
+ };
162
+
163
+ FeatureFlagPersistence.prototype.clear = function() {
164
+ if (this.isGloballyDisabled()) {
165
+ return Promise.resolve();
166
+ }
167
+ return this.idb.init().then(_.bind(function() {
168
+ return this.idb.removeItem(this.persistedVariantsKey);
169
+ }, this)).then(function() {
170
+ logger.log('Cleared persisted variants from IndexedDB');
171
+ }).catch(function(error) {
172
+ logger.error('Failed to clear persisted variants from IndexedDB:', error);
173
+ });
174
+ };
175
+
176
+ export { FeatureFlagPersistence, VariantLookupPolicy, FLAGS_STORE_NAME, PERSISTED_VARIANTS_KEY_PREFIX };
@@ -4,11 +4,13 @@ import { Config, TARGETING_GLOBAL_NAME } from '../config';
4
4
  import {
5
5
  getTargetingPromise
6
6
  } from '../targeting/loader';
7
+ import { FeatureFlagPersistence, VariantLookupPolicy } from './flags-persistence';
7
8
 
8
9
  var logger = console_with_prefix('flags');
9
10
  var FLAGS_CONFIG_KEY = 'flags';
10
11
 
11
12
  var CONFIG_CONTEXT = 'context';
13
+ var CONFIG_PERSISTENCE = 'persistence';
12
14
  var CONFIG_DEFAULTS = {};
13
15
  CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
14
16
 
@@ -31,6 +33,13 @@ var getFlagKeyFromPendingEventKey = function(eventKey) {
31
33
  return eventKey.split(':')[0];
32
34
  };
33
35
 
36
+ var withFallbackSource = function(fallback) {
37
+ if (_.isObject(fallback)) {
38
+ return _.extend({}, fallback, {'variant_source': 'fallback'});
39
+ }
40
+ return {'value': fallback, 'variant_source': 'fallback'};
41
+ };
42
+
34
43
  /**
35
44
  * FeatureFlagManager: support for Mixpanel's feature flagging product
36
45
  * @constructor
@@ -53,13 +62,63 @@ FeatureFlagManager.prototype.init = function() {
53
62
  }
54
63
 
55
64
  this.flags = null;
56
- this.fetchFlags().catch(function() {
57
- logger.error('Error fetching flags during init');
58
- });
59
-
60
65
  this.trackedFeatures = new Set();
61
66
  this.pendingFirstTimeEvents = {};
62
67
  this.activatedFirstTimeEvents = {};
68
+ this._loadedPersistedAtMs = null;
69
+ this._loadedTtlMs = null;
70
+
71
+ this.persistence = new FeatureFlagPersistence(
72
+ this.getConfig(CONFIG_PERSISTENCE),
73
+ this.getMpConfig('token'),
74
+ _.bind(function() { return this.getMpConfig('disable_persistence'); }, this)
75
+ );
76
+
77
+ this.persistenceLoadedPromise = this.persistence.loadFlagsFromStorage(this._buildContext())
78
+ .then(_.bind(function(loaded) {
79
+ if (loaded) {
80
+ this.flags = loaded.flags;
81
+ this.pendingFirstTimeEvents = loaded.pendingFirstTimeEvents;
82
+ this._loadedPersistedAtMs = loaded.persistedAtMs;
83
+ this._loadedTtlMs = loaded.ttlMs;
84
+ }
85
+ }, this));
86
+
87
+ return this.persistenceLoadedPromise
88
+ .then(_.bind(function() {
89
+ return this.fetchFlags();
90
+ }, this))
91
+ .catch(function() {
92
+ logger.error('Error initializing feature flags');
93
+ });
94
+ };
95
+
96
+ FeatureFlagManager.prototype._buildContext = function() {
97
+ return _.extend(
98
+ {'distinct_id': this.getMpProperty('distinct_id'), 'device_id': this.getMpProperty('$device_id')},
99
+ this.getConfig(CONFIG_CONTEXT)
100
+ );
101
+ };
102
+
103
+ FeatureFlagManager.prototype.reset = function() {
104
+ if (!this.persistence) {
105
+ return Promise.resolve();
106
+ }
107
+
108
+ this.flags = null;
109
+ this.pendingFirstTimeEvents = {};
110
+ this.activatedFirstTimeEvents = {};
111
+ this.trackedFeatures = new Set();
112
+ this.fetchPromise = null;
113
+ this._fetchInProgressStartTime = null;
114
+ this._loadedPersistedAtMs = null;
115
+ this._loadedTtlMs = null;
116
+
117
+ return this.persistence.clear().then(_.bind(function() {
118
+ return this.fetchFlags();
119
+ }, this)).catch(function() {
120
+ logger.error('Error during flags reset');
121
+ });
63
122
  };
64
123
 
65
124
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -116,12 +175,11 @@ FeatureFlagManager.prototype.fetchFlags = function() {
116
175
  return Promise.resolve();
117
176
  }
118
177
 
119
- var distinctId = this.getMpProperty('distinct_id');
120
- var deviceId = this.getMpProperty('$device_id');
178
+ var context = this._buildContext();
179
+ var distinctId = context['distinct_id'];
121
180
  var traceparent = generateTraceparent();
122
181
  logger.log('Fetching flags for distinct ID: ' + distinctId);
123
182
 
124
- var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
125
183
  var searchParams = new URLSearchParams();
126
184
  searchParams.set('context', JSON.stringify(context));
127
185
  searchParams.set('token', this.getMpConfig('token'));
@@ -171,7 +229,8 @@ FeatureFlagManager.prototype.fetchFlags = function() {
171
229
  'value': data['variant_value'],
172
230
  'experiment_id': data['experiment_id'],
173
231
  'is_experiment_active': data['is_experiment_active'],
174
- 'is_qa_tester': data['is_qa_tester']
232
+ 'is_qa_tester': data['is_qa_tester'],
233
+ 'variant_source': 'network'
175
234
  });
176
235
  }
177
236
  }, this);
@@ -213,10 +272,15 @@ FeatureFlagManager.prototype.fetchFlags = function() {
213
272
  }
214
273
 
215
274
  this.flags = flags;
275
+ this.trackedFeatures = new Set();
216
276
  this.pendingFirstTimeEvents = pendingFirstTimeEvents;
277
+ this._loadedPersistedAtMs = null;
278
+ this._loadedTtlMs = null;
217
279
  this._traceparent = traceparent;
218
280
 
219
281
  this._loadTargetingIfNeeded();
282
+
283
+ this.persistence.save(context, this.flags, this.pendingFirstTimeEvents);
220
284
  }.bind(this)).catch(function(error) {
221
285
  if (this._fetchInProgressStartTime) {
222
286
  this.markFetchComplete();
@@ -376,6 +440,7 @@ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, p
376
440
  };
377
441
 
378
442
  this.flags.set(flagKey, newVariant);
443
+ this.trackedFeatures.delete(flagKey);
379
444
  this.activatedFirstTimeEvents[eventKey] = true;
380
445
 
381
446
  this.recordFirstTimeEvent(
@@ -425,35 +490,106 @@ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId,
425
490
  };
426
491
 
427
492
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
428
- if (!this.fetchPromise) {
493
+ if (!this.persistenceLoadedPromise) {
429
494
  return new Promise(function(resolve) {
430
495
  logger.critical('Feature Flags not initialized');
431
- resolve(fallback);
496
+ resolve(withFallbackSource(fallback));
432
497
  });
433
498
  }
434
499
 
435
- return this.fetchPromise.then(function() {
436
- return this.getVariantSync(featureName, fallback);
437
- }.bind(this)).catch(function(error) {
438
- logger.error(error);
439
- return fallback;
440
- });
500
+ var policy = this.persistence.getPolicy();
501
+
502
+ return this.persistenceLoadedPromise.then(_.bind(function() {
503
+ // 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.
504
+ if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
505
+ if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
506
+ return this.getVariantSync(featureName, fallback);
507
+ }
508
+ if (!this.fetchPromise) {
509
+ return withFallbackSource(fallback);
510
+ }
511
+ return this.fetchPromise.then(_.bind(function() {
512
+ return this.getVariantSync(featureName, fallback);
513
+ }, this)).catch(function(error) {
514
+ logger.error(error);
515
+ return withFallbackSource(fallback);
516
+ });
517
+ }
518
+
519
+ var serve = _.bind(function() { return this.getVariantSync(featureName, fallback); }, this);
520
+ if (!this.fetchPromise) {
521
+ return withFallbackSource(fallback);
522
+ }
523
+ return this.fetchPromise.then(serve).catch(serve);
524
+ }, this));
525
+ };
526
+
527
+ FeatureFlagManager.prototype._loadedPersistenceIsStale = function() {
528
+ if (!this._loadedPersistedAtMs || !this._loadedTtlMs) {
529
+ return false;
530
+ }
531
+ return Date.now() - this._loadedPersistedAtMs >= this._loadedTtlMs;
441
532
  };
442
533
 
443
534
  FeatureFlagManager.prototype.getVariantSync = function(featureName, fallback) {
535
+ if (this._loadedPersistenceIsStale()) {
536
+ logger.log('Loaded persisted variants are past TTL so returning fallback for "' + featureName + '"');
537
+ return withFallbackSource(fallback);
538
+ }
444
539
  if (!this.areFlagsReady()) {
445
540
  logger.log('Flags not loaded yet');
446
- return fallback;
541
+ return withFallbackSource(fallback);
447
542
  }
448
543
  var feature = this.flags.get(featureName);
449
544
  if (!feature) {
450
545
  logger.log('No flag found: "' + featureName + '"');
451
- return fallback;
546
+ return withFallbackSource(fallback);
452
547
  }
453
548
  this.trackFeatureCheck(featureName, feature);
454
549
  return feature;
455
550
  };
456
551
 
552
+ FeatureFlagManager.prototype.getAllVariants = function() {
553
+ if (!this.persistenceLoadedPromise) {
554
+ logger.critical('Feature Flags not initialized');
555
+ return Promise.resolve(new Map());
556
+ }
557
+
558
+ var policy = this.persistence.getPolicy();
559
+
560
+ return this.persistenceLoadedPromise.then(_.bind(function() {
561
+ // 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.
562
+ if (policy === VariantLookupPolicy.PERSISTENCE_UNTIL_NETWORK_SUCCESS) {
563
+ if (this.areFlagsReady() && !this._loadedPersistenceIsStale()) {
564
+ return this.getAllVariantsSync();
565
+ }
566
+ if (!this.fetchPromise) {
567
+ return new Map();
568
+ }
569
+ return this.fetchPromise.then(_.bind(function() {
570
+ return this.getAllVariantsSync();
571
+ }, this)).catch(function(error) {
572
+ logger.error(error);
573
+ return new Map();
574
+ });
575
+ }
576
+
577
+ var serve = _.bind(this.getAllVariantsSync, this);
578
+ if (!this.fetchPromise) {
579
+ return new Map();
580
+ }
581
+ return this.fetchPromise.then(serve).catch(serve);
582
+ }, this));
583
+ };
584
+
585
+ FeatureFlagManager.prototype.getAllVariantsSync = function() {
586
+ if (this._loadedPersistenceIsStale()) {
587
+ logger.log('Loaded persisted variants are past TTL so returning empty Map');
588
+ return new Map();
589
+ }
590
+ return this.flags || new Map();
591
+ };
592
+
457
593
  FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
458
594
  return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
459
595
  return feature['value'];
@@ -492,6 +628,10 @@ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue
492
628
  return val;
493
629
  };
494
630
 
631
+ function isPresent(v) {
632
+ return v !== undefined && v !== null;
633
+ }
634
+
495
635
  FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
496
636
  if (this.trackedFeatures.has(featureName)) {
497
637
  return;
@@ -502,21 +642,30 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
502
642
  'Experiment name': featureName,
503
643
  'Variant name': feature['key'],
504
644
  '$experiment_type': 'feature_flag',
505
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
506
- 'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
645
+ 'Variant fetch start time': isPresent(this._fetchStartTime) ? new Date(this._fetchStartTime).toISOString() : null,
646
+ 'Variant fetch complete time': isPresent(this._fetchCompleteTime) ? new Date(this._fetchCompleteTime).toISOString() : null,
507
647
  'Variant fetch latency (ms)': this._fetchLatency,
508
648
  'Variant fetch traceparent': this._traceparent,
509
649
  };
510
650
 
511
- if (feature['experiment_id'] !== 'undefined') {
651
+ if (isPresent(feature['experiment_id'])) {
512
652
  trackingProperties['$experiment_id'] = feature['experiment_id'];
513
653
  }
514
- if (feature['is_experiment_active'] !== 'undefined') {
654
+ if (isPresent(feature['is_experiment_active'])) {
515
655
  trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
516
656
  }
517
- if (feature['is_qa_tester'] !== 'undefined') {
657
+ if (isPresent(feature['is_qa_tester'])) {
518
658
  trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
519
659
  }
660
+ if (isPresent(feature['variant_source'])) {
661
+ trackingProperties['$variant_source'] = feature['variant_source'];
662
+ }
663
+ if (isPresent(feature['persisted_at_in_ms'])) {
664
+ trackingProperties['$persisted_at_in_ms'] = feature['persisted_at_in_ms'];
665
+ }
666
+ if (isPresent(feature['ttl_in_ms'])) {
667
+ trackingProperties['$ttl_in_ms'] = feature['ttl_in_ms'];
668
+ }
520
669
 
521
670
  this.track('$experiment_started', trackingProperties);
522
671
  };
@@ -540,6 +689,8 @@ safewrapClass(FeatureFlagManager);
540
689
  FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
541
690
  FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
542
691
  FeatureFlagManager.prototype['get_variant_sync'] = FeatureFlagManager.prototype.getVariantSync;
692
+ FeatureFlagManager.prototype['get_all_variants'] = FeatureFlagManager.prototype.getAllVariants;
693
+ FeatureFlagManager.prototype['get_all_variants_sync'] = FeatureFlagManager.prototype.getAllVariantsSync;
543
694
  FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
544
695
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
545
696
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
package/src/index.d.ts CHANGED
@@ -165,8 +165,47 @@ export interface AutocaptureConfig {
165
165
  block_element_callback?: (element: Element, event: Event) => boolean;
166
166
  }
167
167
 
168
+ /**
169
+ * Network-only variant lookup - no persistence. Default behavior.
170
+ */
171
+ export interface NetworkOnlyFlagsPolicy {
172
+ variantLookupPolicy: "networkOnly";
173
+ }
174
+
175
+ /**
176
+ * Network-first variant lookup - prioritizes freshness.
177
+ * Attempts network fetch first, falls back to persisted variants on failure.
178
+ */
179
+ export interface NetworkFirstFlagsPolicy {
180
+ variantLookupPolicy: "networkFirst";
181
+ /**
182
+ * Time-to-live in milliseconds. Persisted variants older than this are discarded.
183
+ * Defaults to 24 hours.
184
+ */
185
+ persistenceTtlMs?: number;
186
+ }
187
+
188
+ /**
189
+ * Serves persisted variants immediately while a network fetch runs in the background.
190
+ * Once the fetch succeeds, its result replaces the cached variants for the rest of the session.
191
+ */
192
+ export interface PersistenceUntilNetworkSuccessFlagsPolicy {
193
+ variantLookupPolicy: "persistenceUntilNetworkSuccess";
194
+ /**
195
+ * Time-to-live in milliseconds. Persisted variants older than this are discarded.
196
+ * Defaults to 24 hours.
197
+ */
198
+ persistenceTtlMs?: number;
199
+ }
200
+
201
+ export type FlagsPersistencePolicy =
202
+ | NetworkOnlyFlagsPolicy
203
+ | NetworkFirstFlagsPolicy
204
+ | PersistenceUntilNetworkSuccessFlagsPolicy;
205
+
168
206
  export interface FlagsConfig {
169
- context: Dict;
207
+ context?: Dict;
208
+ persistence?: FlagsPersistencePolicy;
170
209
  }
171
210
 
172
211
  export interface BeforeSendHookPayload {
@@ -349,6 +388,8 @@ export interface FlagsVariant {
349
388
  experiment_id?: string;
350
389
  is_experiment_active?: boolean;
351
390
  is_qa_tester?: boolean;
391
+ variant_source?: string;
392
+ persisted_at_in_ms?: number;
352
393
  }
353
394
 
354
395
  export interface FlagsUpdateContextOptions {
@@ -365,12 +406,15 @@ export interface FlagsManager {
365
406
  get_variant_sync(featureName: string, fallback: FlagsVariant): FlagsVariant;
366
407
  get_variant_value(featureName: string, fallbackValue: any): Promise<any>;
367
408
  get_variant_value_sync(featureName: string, fallbackValue: any): any;
409
+ get_all_variants(): Promise<Map<string, FlagsVariant>>;
410
+ get_all_variants_sync(): Map<string, FlagsVariant>;
368
411
  is_enabled(featureName: string, fallbackValue?: boolean): Promise<boolean>;
369
412
  is_enabled_sync(featureName: string, fallbackValue?: boolean): boolean;
370
413
  update_context(
371
414
  context: Dict,
372
415
  options?: FlagsUpdateContextOptions
373
416
  ): Promise<void>;
417
+ when_ready(): Promise<void>;
374
418
  }
375
419
 
376
420
  export interface Mixpanel {
@@ -1,6 +1,6 @@
1
1
  /* eslint camelcase: "off" */
2
2
  import {Config, TARGETING_FILENAME} from './config';
3
- import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice, NOOP_FUNC, JSONStringify } from './utils';
3
+ import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice, NOOP_FUNC, JSONStringify, safewrap } from './utils';
4
4
  import { window } from './window';
5
5
  import { Autocapture } from './autocapture';
6
6
  import { FeatureFlagManager } from './flags';
@@ -341,6 +341,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
341
341
  'disable_all_events': false,
342
342
  'identify_called': false
343
343
  };
344
+ this._remote_settings_strict_disabled = false;
344
345
 
345
346
  // set up request queueing/batching
346
347
  this.request_batchers = {};
@@ -415,9 +416,6 @@ MixpanelLib.prototype._init = function(token, config, name) {
415
416
  this.flags.init();
416
417
  this['flags'] = this.flags;
417
418
 
418
- this.autocapture = new Autocapture(this);
419
- this.autocapture.init();
420
-
421
419
  this._init_tab_id();
422
420
 
423
421
  // Based on remote_settings_mode, fetch remote settings and then start session recording if applicable
@@ -429,6 +427,9 @@ MixpanelLib.prototype._init = function(token, config, name) {
429
427
  } else {
430
428
  this.__session_recording_init_promise = this._check_and_start_session_recording();
431
429
  }
430
+
431
+ this.autocapture = new Autocapture(this);
432
+ this.autocapture.init();
432
433
  };
433
434
 
434
435
  /**
@@ -475,9 +476,19 @@ MixpanelLib.prototype._check_and_start_session_recording = addOptOutCheckMixpane
475
476
  return this.recorderManager.checkAndStartSessionRecording(force_start);
476
477
  });
477
478
 
478
- MixpanelLib.prototype._start_recording_on_event = function(event_name, properties) {
479
- return this.recorderManager.startRecordingOnEvent(event_name, properties);
480
- };
479
+ MixpanelLib.prototype._start_recording_on_event = safewrap(function(event_name, properties) {
480
+ // Wait for recording init to complete before evaluating event triggers.
481
+ // This ensures recording_event_triggers config is fully loaded when remote settings are used.
482
+ if (this.__session_recording_init_promise) {
483
+ this.__session_recording_init_promise.then(_.bind(function() {
484
+ // In strict mode, skip recording if remote settings failed
485
+ if (this._remote_settings_strict_disabled) {
486
+ return;
487
+ }
488
+ return this.recorderManager.startRecordingOnEvent(event_name, properties);
489
+ }, this));
490
+ }
491
+ });
481
492
 
482
493
  MixpanelLib.prototype.start_session_recording = function () {
483
494
  return this._check_and_start_session_recording(true);
@@ -776,6 +787,7 @@ MixpanelLib.prototype._fetch_remote_settings = function(mode) {
776
787
  var disableRecordingIfStrict = function() {
777
788
  if (mode === 'strict') {
778
789
  self.set_config({'record_sessions_percent': 0});
790
+ self._remote_settings_strict_disabled = true;
779
791
  }
780
792
  };
781
793
 
@@ -1401,6 +1413,10 @@ MixpanelLib.prototype.track_pageview = addOptOutCheckMixpanelLib(function(proper
1401
1413
  properties
1402
1414
  );
1403
1415
 
1416
+ if (this.is_recording_heatmap_data()) {
1417
+ event_properties['$captured_for_heatmap'] = true;
1418
+ }
1419
+
1404
1420
  return this.track(event_name, event_properties);
1405
1421
  });
1406
1422
 
@@ -1744,6 +1760,7 @@ MixpanelLib.prototype.reset = function() {
1744
1760
  '$device_id': uuid
1745
1761
  }, '');
1746
1762
  this._check_and_start_session_recording();
1763
+ this.flags.reset();
1747
1764
  };
1748
1765
 
1749
1766
  /**
@@ -0,0 +1,16 @@
1
+ var MIXPANEL_BROWSER_DB_NAME = 'mixpanelBrowserDb';
2
+ var RECORDING_EVENTS_STORE_NAME = 'mixpanelRecordingEvents';
3
+ var RECORDING_REGISTRY_STORE_NAME = 'mixpanelRecordingRegistry';
4
+
5
+ // Keeping these two properties closeby, as adding additional stores to a DB in IndexedDB requires a version increment
6
+ var RECORDER_VERSION_DATA = {
7
+ version: 1,
8
+ storeNames: [RECORDING_EVENTS_STORE_NAME, RECORDING_REGISTRY_STORE_NAME]
9
+ };
10
+
11
+ export {
12
+ MIXPANEL_BROWSER_DB_NAME,
13
+ RECORDING_EVENTS_STORE_NAME,
14
+ RECORDING_REGISTRY_STORE_NAME,
15
+ RECORDER_VERSION_DATA
16
+ };
@@ -1,5 +1,10 @@
1
1
  import { Promise } from '../promise-polyfill';
2
- import { IDBStorageWrapper, RECORDING_REGISTRY_STORE_NAME } from '../storage/indexed-db';
2
+ import { IDBStorageWrapper } from '../storage/indexed-db';
3
+ import {
4
+ MIXPANEL_BROWSER_DB_NAME,
5
+ RECORDING_REGISTRY_STORE_NAME,
6
+ RECORDER_VERSION_DATA
7
+ } from './idb-config';
3
8
  import { SessionRecording } from './session-recording';
4
9
  import { isRecordingExpired } from './utils';
5
10
 
@@ -9,7 +14,7 @@ import { isRecordingExpired } from './utils';
9
14
  */
10
15
  var RecordingRegistry = function (options) {
11
16
  /** @type {IDBStorageWrapper} */
12
- this.idb = new IDBStorageWrapper(RECORDING_REGISTRY_STORE_NAME);
17
+ this.idb = new IDBStorageWrapper(MIXPANEL_BROWSER_DB_NAME, RECORDING_REGISTRY_STORE_NAME, RECORDER_VERSION_DATA);
13
18
  this.errorReporter = options.errorReporter;
14
19
  this.mixpanelInstance = options.mixpanelInstance;
15
20
  this.sharedLockStorage = options.sharedLockStorage;