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
@@ -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,11 +62,63 @@ FeatureFlagManager.prototype.init = function() {
53
62
  }
54
63
 
55
64
  this.flags = null;
56
- this.fetchFlags();
57
-
58
65
  this.trackedFeatures = new Set();
59
66
  this.pendingFirstTimeEvents = {};
60
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
+ });
61
122
  };
62
123
 
63
124
  FeatureFlagManager.prototype.getFullConfig = function() {
@@ -94,8 +155,12 @@ FeatureFlagManager.prototype.updateContext = function(newContext, options) {
94
155
  var oldContext = (options && options['replace']) ? {} : this.getConfig(CONFIG_CONTEXT);
95
156
  ffConfig[CONFIG_CONTEXT] = _.extend({}, oldContext, newContext);
96
157
 
97
- this.setMpConfig(FLAGS_CONFIG_KEY, ffConfig);
98
- return this.fetchFlags();
158
+ var configUpdate = {};
159
+ configUpdate[FLAGS_CONFIG_KEY] = ffConfig;
160
+ this.setMpConfig(configUpdate);
161
+ return this.fetchFlags().catch(function() {
162
+ logger.error('Error fetching flags during updateContext');
163
+ });
99
164
  };
100
165
 
101
166
  FeatureFlagManager.prototype.areFlagsReady = function() {
@@ -110,12 +175,11 @@ FeatureFlagManager.prototype.fetchFlags = function() {
110
175
  return Promise.resolve();
111
176
  }
112
177
 
113
- var distinctId = this.getMpProperty('distinct_id');
114
- var deviceId = this.getMpProperty('$device_id');
178
+ var context = this._buildContext();
179
+ var distinctId = context['distinct_id'];
115
180
  var traceparent = generateTraceparent();
116
181
  logger.log('Fetching flags for distinct ID: ' + distinctId);
117
182
 
118
- var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
119
183
  var searchParams = new URLSearchParams();
120
184
  searchParams.set('context', JSON.stringify(context));
121
185
  searchParams.set('token', this.getMpConfig('token'));
@@ -132,96 +196,116 @@ FeatureFlagManager.prototype.fetchFlags = function() {
132
196
  }
133
197
  }).then(function(response) {
134
198
  this.markFetchComplete();
135
- return response.json().then(function(responseBody) {
136
- var responseFlags = responseBody['flags'];
137
- if (!responseFlags) {
138
- throw new Error('No flags in API response');
139
- }
140
- var flags = new Map();
141
- var pendingFirstTimeEvents = {};
142
-
143
- // Process flags from response
144
- _.each(responseFlags, function(data, key) {
145
- // Check if this flag has any activated first-time events this session
146
- var hasActivatedEvent = false;
147
- var prefix = key + ':';
148
- _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
149
- if (eventKey.startsWith(prefix)) {
150
- hasActivatedEvent = true;
151
- }
152
- });
199
+ return response.json();
200
+ }.bind(this)).then(function(responseBody) {
201
+ var responseFlags = responseBody['flags'];
202
+ if (!responseFlags) {
203
+ throw new Error('No flags in API response');
204
+ }
205
+ var flags = new Map();
206
+ var pendingFirstTimeEvents = {};
207
+
208
+ // Process flags from response
209
+ _.each(responseFlags, function(data, key) {
210
+ // Check if this flag has any activated first-time events this session
211
+ var hasActivatedEvent = false;
212
+ var prefix = key + ':';
213
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
214
+ if (eventKey.startsWith(prefix)) {
215
+ hasActivatedEvent = true;
216
+ }
217
+ });
153
218
 
154
- if (hasActivatedEvent) {
155
- // Preserve the activated variant, don't overwrite with server's current variant
156
- var currentFlag = this.flags && this.flags.get(key);
157
- if (currentFlag) {
158
- flags.set(key, currentFlag);
159
- }
160
- } else {
161
- // Use server's current variant
162
- flags.set(key, {
163
- 'key': data['variant_key'],
164
- 'value': data['variant_value'],
165
- 'experiment_id': data['experiment_id'],
166
- 'is_experiment_active': data['is_experiment_active'],
167
- 'is_qa_tester': data['is_qa_tester']
168
- });
219
+ if (hasActivatedEvent) {
220
+ // Preserve the activated variant, don't overwrite with server's current variant
221
+ var currentFlag = this.flags && this.flags.get(key);
222
+ if (currentFlag) {
223
+ flags.set(key, currentFlag);
169
224
  }
225
+ } else {
226
+ // Use server's current variant
227
+ flags.set(key, {
228
+ 'key': data['variant_key'],
229
+ 'value': data['variant_value'],
230
+ 'experiment_id': data['experiment_id'],
231
+ 'is_experiment_active': data['is_experiment_active'],
232
+ 'is_qa_tester': data['is_qa_tester'],
233
+ 'variant_source': 'network'
234
+ });
235
+ }
236
+ }, this);
237
+
238
+ // Process top-level pending_first_time_events array
239
+ var topLevelDefinitions = responseBody['pending_first_time_events'];
240
+ if (topLevelDefinitions && topLevelDefinitions.length > 0) {
241
+ _.each(topLevelDefinitions, function(def) {
242
+ var flagKey = def['flag_key'];
243
+ var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
244
+
245
+ // Skip if this specific event has already been activated this session
246
+ if (this.activatedFirstTimeEvents[eventKey]) {
247
+ return;
248
+ }
249
+
250
+ // Store pending event definition using composite key
251
+ pendingFirstTimeEvents[eventKey] = {
252
+ 'flag_key': flagKey,
253
+ 'flag_id': def['flag_id'],
254
+ 'project_id': def['project_id'],
255
+ 'first_time_event_hash': def['first_time_event_hash'],
256
+ 'event_name': def['event_name'],
257
+ 'property_filters': def['property_filters'],
258
+ 'pending_variant': def['pending_variant']
259
+ };
170
260
  }, this);
261
+ }
171
262
 
172
- // Process top-level pending_first_time_events array
173
- var topLevelDefinitions = responseBody['pending_first_time_events'];
174
- if (topLevelDefinitions && topLevelDefinitions.length > 0) {
175
- _.each(topLevelDefinitions, function(def) {
176
- var flagKey = def['flag_key'];
177
- var eventKey = getPendingEventKey(flagKey, def['first_time_event_hash']);
178
-
179
- // Skip if this specific event has already been activated this session
180
- if (this.activatedFirstTimeEvents[eventKey]) {
181
- return;
182
- }
183
-
184
- // Store pending event definition using composite key
185
- pendingFirstTimeEvents[eventKey] = {
186
- 'flag_key': flagKey,
187
- 'flag_id': def['flag_id'],
188
- 'project_id': def['project_id'],
189
- 'first_time_event_hash': def['first_time_event_hash'],
190
- 'event_name': def['event_name'],
191
- 'property_filters': def['property_filters'],
192
- 'pending_variant': def['pending_variant']
193
- };
194
- }, this);
195
- }
263
+ // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
264
+ if (this.activatedFirstTimeEvents) {
265
+ _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
266
+ var flagKey = getFlagKeyFromPendingEventKey(eventKey);
267
+ if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
268
+ // Keep the activated flag even though it's not in the new response
269
+ flags.set(flagKey, this.flags.get(flagKey));
270
+ }
271
+ }, this);
272
+ }
196
273
 
197
- // Preserve any activated orphaned flags (flags that were activated but are no longer in response)
198
- if (this.activatedFirstTimeEvents) {
199
- _.each(this.activatedFirstTimeEvents, function(activated, eventKey) {
200
- var flagKey = getFlagKeyFromPendingEventKey(eventKey);
201
- if (activated && !flags.has(flagKey) && this.flags && this.flags.has(flagKey)) {
202
- // Keep the activated flag even though it's not in the new response
203
- flags.set(flagKey, this.flags.get(flagKey));
204
- }
205
- }, this);
206
- }
274
+ this.flags = flags;
275
+ this.trackedFeatures = new Set();
276
+ this.pendingFirstTimeEvents = pendingFirstTimeEvents;
277
+ this._loadedPersistedAtMs = null;
278
+ this._loadedTtlMs = null;
279
+ this._traceparent = traceparent;
207
280
 
208
- this.flags = flags;
209
- this.pendingFirstTimeEvents = pendingFirstTimeEvents;
210
- this._traceparent = traceparent;
281
+ this._loadTargetingIfNeeded();
211
282
 
212
- this._loadTargetingIfNeeded();
213
- }.bind(this)).catch(function(error) {
214
- this.markFetchComplete();
215
- logger.error(error);
216
- }.bind(this));
283
+ this.persistence.save(context, this.flags, this.pendingFirstTimeEvents);
217
284
  }.bind(this)).catch(function(error) {
218
- this.markFetchComplete();
285
+ if (this._fetchInProgressStartTime) {
286
+ this.markFetchComplete();
287
+ }
219
288
  logger.error(error);
289
+ throw error;
220
290
  }.bind(this));
221
291
 
222
292
  return this.fetchPromise;
223
293
  };
224
294
 
295
+ FeatureFlagManager.prototype.loadFlags = function() {
296
+ if (!this.isSystemEnabled()) {
297
+ return Promise.resolve();
298
+ }
299
+ if (!this.trackedFeatures) {
300
+ logger.error('loadFlags called before init');
301
+ return Promise.resolve();
302
+ }
303
+ if (this._fetchInProgressStartTime) {
304
+ return this.fetchPromise;
305
+ }
306
+ return this.fetchFlags();
307
+ };
308
+
225
309
  FeatureFlagManager.prototype.markFetchComplete = function() {
226
310
  if (!this._fetchInProgressStartTime) {
227
311
  logger.error('Fetch in progress started time not set, cannot mark fetch complete');
@@ -356,6 +440,7 @@ FeatureFlagManager.prototype._processFirstTimeEventCheck = function(eventName, p
356
440
  };
357
441
 
358
442
  this.flags.set(flagKey, newVariant);
443
+ this.trackedFeatures.delete(flagKey);
359
444
  this.activatedFirstTimeEvents[eventKey] = true;
360
445
 
361
446
  this.recordFirstTimeEvent(
@@ -405,35 +490,106 @@ FeatureFlagManager.prototype.recordFirstTimeEvent = function(flagId, projectId,
405
490
  };
406
491
 
407
492
  FeatureFlagManager.prototype.getVariant = function(featureName, fallback) {
408
- if (!this.fetchPromise) {
493
+ if (!this.persistenceLoadedPromise) {
409
494
  return new Promise(function(resolve) {
410
495
  logger.critical('Feature Flags not initialized');
411
- resolve(fallback);
496
+ resolve(withFallbackSource(fallback));
412
497
  });
413
498
  }
414
499
 
415
- return this.fetchPromise.then(function() {
416
- return this.getVariantSync(featureName, fallback);
417
- }.bind(this)).catch(function(error) {
418
- logger.error(error);
419
- return fallback;
420
- });
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;
421
532
  };
422
533
 
423
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
+ }
424
539
  if (!this.areFlagsReady()) {
425
540
  logger.log('Flags not loaded yet');
426
- return fallback;
541
+ return withFallbackSource(fallback);
427
542
  }
428
543
  var feature = this.flags.get(featureName);
429
544
  if (!feature) {
430
545
  logger.log('No flag found: "' + featureName + '"');
431
- return fallback;
546
+ return withFallbackSource(fallback);
432
547
  }
433
548
  this.trackFeatureCheck(featureName, feature);
434
549
  return feature;
435
550
  };
436
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
+
437
593
  FeatureFlagManager.prototype.getVariantValue = function(featureName, fallbackValue) {
438
594
  return this.getVariant(featureName, {'value': fallbackValue}).then(function(feature) {
439
595
  return feature['value'];
@@ -472,6 +628,10 @@ FeatureFlagManager.prototype.isEnabledSync = function(featureName, fallbackValue
472
628
  return val;
473
629
  };
474
630
 
631
+ function isPresent(v) {
632
+ return v !== undefined && v !== null;
633
+ }
634
+
475
635
  FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature) {
476
636
  if (this.trackedFeatures.has(featureName)) {
477
637
  return;
@@ -482,25 +642,41 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
482
642
  'Experiment name': featureName,
483
643
  'Variant name': feature['key'],
484
644
  '$experiment_type': 'feature_flag',
485
- 'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
486
- '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,
487
647
  'Variant fetch latency (ms)': this._fetchLatency,
488
648
  'Variant fetch traceparent': this._traceparent,
489
649
  };
490
650
 
491
- if (feature['experiment_id'] !== 'undefined') {
651
+ if (isPresent(feature['experiment_id'])) {
492
652
  trackingProperties['$experiment_id'] = feature['experiment_id'];
493
653
  }
494
- if (feature['is_experiment_active'] !== 'undefined') {
654
+ if (isPresent(feature['is_experiment_active'])) {
495
655
  trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
496
656
  }
497
- if (feature['is_qa_tester'] !== 'undefined') {
657
+ if (isPresent(feature['is_qa_tester'])) {
498
658
  trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
499
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
+ }
500
669
 
501
670
  this.track('$experiment_started', trackingProperties);
502
671
  };
503
672
 
673
+ FeatureFlagManager.prototype.whenReady = function() {
674
+ if (this.fetchPromise) {
675
+ return this.fetchPromise;
676
+ }
677
+ return Promise.resolve();
678
+ };
679
+
504
680
  FeatureFlagManager.prototype.minApisSupported = function() {
505
681
  return !!this.fetch &&
506
682
  typeof Promise !== 'undefined' &&
@@ -513,11 +689,15 @@ safewrapClass(FeatureFlagManager);
513
689
  FeatureFlagManager.prototype['are_flags_ready'] = FeatureFlagManager.prototype.areFlagsReady;
514
690
  FeatureFlagManager.prototype['get_variant'] = FeatureFlagManager.prototype.getVariant;
515
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;
516
694
  FeatureFlagManager.prototype['get_variant_value'] = FeatureFlagManager.prototype.getVariantValue;
517
695
  FeatureFlagManager.prototype['get_variant_value_sync'] = FeatureFlagManager.prototype.getVariantValueSync;
518
696
  FeatureFlagManager.prototype['is_enabled'] = FeatureFlagManager.prototype.isEnabled;
519
697
  FeatureFlagManager.prototype['is_enabled_sync'] = FeatureFlagManager.prototype.isEnabledSync;
698
+ FeatureFlagManager.prototype['load_flags'] = FeatureFlagManager.prototype.loadFlags;
520
699
  FeatureFlagManager.prototype['update_context'] = FeatureFlagManager.prototype.updateContext;
700
+ FeatureFlagManager.prototype['when_ready'] = FeatureFlagManager.prototype.whenReady;
521
701
 
522
702
  // Deprecated method
523
703
  FeatureFlagManager.prototype['get_feature_data'] = FeatureFlagManager.prototype.getFeatureData;
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 {
@@ -357,6 +398,7 @@ export interface FlagsUpdateContextOptions {
357
398
 
358
399
  export interface FlagsManager {
359
400
  are_flags_ready(): boolean;
401
+ load_flags(): Promise<void>;
360
402
  get_variant(
361
403
  featureName: string,
362
404
  fallback: FlagsVariant
@@ -364,12 +406,15 @@ export interface FlagsManager {
364
406
  get_variant_sync(featureName: string, fallback: FlagsVariant): FlagsVariant;
365
407
  get_variant_value(featureName: string, fallbackValue: any): Promise<any>;
366
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>;
367
411
  is_enabled(featureName: string, fallbackValue?: boolean): Promise<boolean>;
368
412
  is_enabled_sync(featureName: string, fallbackValue?: boolean): boolean;
369
413
  update_context(
370
414
  context: Dict,
371
415
  options?: FlagsUpdateContextOptions
372
416
  ): Promise<void>;
417
+ when_ready(): Promise<void>;
373
418
  }
374
419
 
375
420
  export interface Mixpanel {