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.
- package/.claude/settings.local.json +6 -11
- package/.eslintrc.json +12 -0
- package/.github/workflows/openfeature-provider-tests.yml +31 -0
- package/CHANGELOG.md +8 -1
- package/build.sh +2 -2
- package/dist/async-modules/{mixpanel-recorder-BjSlYaNJ.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-zMBXIyeG.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-UHf4eBfC.js → mixpanel-targeting-BBMVbgJF.js} +24 -13
- package/dist/mixpanel-core.cjs.d.ts +45 -1
- package/dist/mixpanel-core.cjs.js +565 -197
- 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 +45 -1
- package/dist/mixpanel-with-async-modules.cjs.js +567 -199
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +45 -1
- package/dist/mixpanel-with-async-recorder.cjs.js +567 -199
- package/dist/mixpanel-with-recorder.d.ts +45 -1
- package/dist/mixpanel-with-recorder.js +490 -122
- package/dist/mixpanel-with-recorder.min.d.ts +45 -1
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +45 -1
- package/dist/mixpanel.amd.js +490 -122
- package/dist/mixpanel.cjs.d.ts +45 -1
- package/dist/mixpanel.cjs.js +490 -122
- package/dist/mixpanel.globals.js +567 -199
- package/dist/mixpanel.min.js +199 -189
- package/dist/mixpanel.module.d.ts +45 -1
- package/dist/mixpanel.module.js +490 -122
- package/dist/mixpanel.umd.d.ts +45 -1
- package/dist/mixpanel.umd.js +490 -122
- 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/flags-persistence.js +176 -0
- package/src/flags/index.js +174 -23
- package/src/index.d.ts +45 -1
- package/src/mixpanel-core.js +24 -7
- 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/tsconfig.base.json +9 -0
- package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js.map +0 -1
- package/dist/async-modules/mixpanel-targeting-BSHal4N9.min.js +0 -2
- 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 };
|
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,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
|
|
120
|
-
var
|
|
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.
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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']
|
|
651
|
+
if (isPresent(feature['experiment_id'])) {
|
|
512
652
|
trackingProperties['$experiment_id'] = feature['experiment_id'];
|
|
513
653
|
}
|
|
514
|
-
if (feature['is_experiment_active']
|
|
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']
|
|
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
|
|
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 {
|
package/src/mixpanel-core.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|