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.
- package/.claude/settings.local.json +6 -9
- package/.eslintrc.json +12 -0
- package/.github/workflows/openfeature-provider-tests.yml +31 -0
- package/CHANGELOG.md +11 -0
- package/build.sh +2 -2
- package/dist/async-modules/{mixpanel-recorder-wIWnMDLA.min.js → mixpanel-recorder-D5HJyV2E.min.js} +2 -2
- package/dist/async-modules/mixpanel-recorder-D5HJyV2E.min.js.map +1 -0
- package/dist/async-modules/{mixpanel-recorder-DLKbUIEE.js → mixpanel-recorder-P6SEnnPV.js} +57 -33
- package/dist/async-modules/mixpanel-targeting-1L9FyetZ.min.js +2 -0
- package/dist/async-modules/mixpanel-targeting-1L9FyetZ.min.js.map +1 -0
- package/dist/async-modules/{mixpanel-targeting-CmVvUyFM.js → mixpanel-targeting-BBMVbgJF.js} +24 -13
- package/dist/mixpanel-core.cjs.d.ts +46 -1
- package/dist/mixpanel-core.cjs.js +671 -272
- package/dist/mixpanel-recorder.js +57 -33
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +24 -13
- package/dist/mixpanel-targeting.min.js +1 -1
- package/dist/mixpanel-targeting.min.js.map +1 -1
- package/dist/mixpanel-with-async-modules.cjs.d.ts +46 -1
- package/dist/mixpanel-with-async-modules.cjs.js +673 -274
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +46 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +673 -274
- package/dist/mixpanel-with-recorder.d.ts +46 -1
- package/dist/mixpanel-with-recorder.js +596 -197
- package/dist/mixpanel-with-recorder.min.d.ts +46 -1
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +46 -1
- package/dist/mixpanel.amd.js +596 -197
- package/dist/mixpanel.cjs.d.ts +46 -1
- package/dist/mixpanel.cjs.js +596 -197
- package/dist/mixpanel.globals.js +673 -274
- package/dist/mixpanel.min.js +200 -189
- package/dist/mixpanel.module.d.ts +46 -1
- package/dist/mixpanel.module.js +596 -197
- package/dist/mixpanel.umd.d.ts +46 -1
- package/dist/mixpanel.umd.js +596 -197
- package/package.json +1 -1
- package/packages/openfeature-web-provider/README.md +357 -0
- package/packages/openfeature-web-provider/package-lock.json +1636 -0
- package/packages/openfeature-web-provider/package.json +51 -0
- package/packages/openfeature-web-provider/rollup.config.browser.mjs +26 -0
- package/packages/openfeature-web-provider/src/MixpanelProvider.ts +302 -0
- package/packages/openfeature-web-provider/src/index.ts +1 -0
- package/packages/openfeature-web-provider/src/types.ts +72 -0
- package/packages/openfeature-web-provider/test/MixpanelProvider.spec.ts +484 -0
- package/packages/openfeature-web-provider/tsconfig.json +15 -0
- package/src/autocapture/index.js +7 -2
- package/src/config.js +1 -1
- package/src/flags/CLAUDE.md +24 -0
- package/src/flags/flags-persistence.js +176 -0
- package/src/flags/index.js +278 -98
- package/src/index.d.ts +46 -1
- package/src/mixpanel-core.js +27 -8
- package/src/recorder/idb-config.js +16 -0
- package/src/recorder/recording-registry.js +7 -2
- package/src/recorder/session-recording.js +9 -4
- package/src/recorder-manager.js +7 -2
- package/src/request-queue.js +1 -2
- package/src/shared-lock.js +2 -3
- package/src/storage/indexed-db.js +16 -15
- package/src/storage/local-storage.js +5 -3
- package/src/utils.js +25 -12
- package/testServer.js +2 -0
- package/tsconfig.base.json +9 -0
- package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +0 -1
- package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +0 -2
- package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +0 -1
package/src/flags/index.js
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
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
|
|
114
|
-
var
|
|
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()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
209
|
-
this.pendingFirstTimeEvents = pendingFirstTimeEvents;
|
|
210
|
-
this._traceparent = traceparent;
|
|
281
|
+
this._loadTargetingIfNeeded();
|
|
211
282
|
|
|
212
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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']
|
|
651
|
+
if (isPresent(feature['experiment_id'])) {
|
|
492
652
|
trackingProperties['$experiment_id'] = feature['experiment_id'];
|
|
493
653
|
}
|
|
494
|
-
if (feature['is_experiment_active']
|
|
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']
|
|
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
|
|
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 {
|