@microsoft/feature-management 1.0.0 → 2.0.0-preview.2
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/dist/commonjs/common/targetingEvaluator.js +119 -0
- package/dist/commonjs/common/targetingEvaluator.js.map +1 -0
- package/dist/commonjs/featureManager.js +183 -7
- package/dist/commonjs/featureManager.js.map +1 -1
- package/dist/commonjs/featureProvider.js +2 -2
- package/dist/commonjs/featureProvider.js.map +1 -1
- package/dist/commonjs/filter/TargetingFilter.js +6 -74
- package/dist/commonjs/filter/TargetingFilter.js.map +1 -1
- package/dist/commonjs/filter/TimeWindowFilter.js.map +1 -1
- package/dist/commonjs/index.js +5 -0
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/model.js.map +1 -1
- package/dist/commonjs/variant/Variant.js +15 -0
- package/dist/commonjs/variant/Variant.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/esm/common/targetingEvaluator.js +115 -0
- package/dist/esm/common/targetingEvaluator.js.map +1 -0
- package/dist/esm/featureManager.js +183 -8
- package/dist/esm/featureManager.js.map +1 -1
- package/dist/esm/featureProvider.js +2 -2
- package/dist/esm/featureProvider.js.map +1 -1
- package/dist/esm/filter/TargetingFilter.js +6 -74
- package/dist/esm/filter/TargetingFilter.js.map +1 -1
- package/dist/esm/filter/TimeWindowFilter.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/variant/Variant.js +13 -0
- package/dist/esm/variant/Variant.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/umd/index.js +310 -82
- package/dist/umd/index.js.map +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +82 -13
package/dist/umd/index.js
CHANGED
|
@@ -22,6 +22,119 @@
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Copyright (c) Microsoft Corporation.
|
|
26
|
+
// Licensed under the MIT license.
|
|
27
|
+
/**
|
|
28
|
+
* Determines if the user is part of the audience, based on the user id and the percentage range.
|
|
29
|
+
*
|
|
30
|
+
* @param userId user id from app context
|
|
31
|
+
* @param hint hint string to be included in the context id
|
|
32
|
+
* @param from percentage range start
|
|
33
|
+
* @param to percentage range end
|
|
34
|
+
* @returns true if the user is part of the audience, false otherwise
|
|
35
|
+
*/
|
|
36
|
+
async function isTargetedPercentile(userId, hint, from, to) {
|
|
37
|
+
if (from < 0 || from > 100) {
|
|
38
|
+
throw new Error("The 'from' value must be between 0 and 100.");
|
|
39
|
+
}
|
|
40
|
+
if (to < 0 || to > 100) {
|
|
41
|
+
throw new Error("The 'to' value must be between 0 and 100.");
|
|
42
|
+
}
|
|
43
|
+
if (from > to) {
|
|
44
|
+
throw new Error("The 'from' value cannot be larger than the 'to' value.");
|
|
45
|
+
}
|
|
46
|
+
const audienceContextId = constructAudienceContextId(userId, hint);
|
|
47
|
+
// Cryptographic hashing algorithms ensure adequate entropy across hash values.
|
|
48
|
+
const contextMarker = await stringToUint32(audienceContextId);
|
|
49
|
+
const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
|
|
50
|
+
// Handle edge case of exact 100 bucket
|
|
51
|
+
if (to === 100) {
|
|
52
|
+
return contextPercentage >= from;
|
|
53
|
+
}
|
|
54
|
+
return contextPercentage >= from && contextPercentage < to;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Determines if the user is part of the audience, based on the groups they belong to.
|
|
58
|
+
*
|
|
59
|
+
* @param sourceGroups user groups from app context
|
|
60
|
+
* @param targetedGroups targeted groups from feature configuration
|
|
61
|
+
* @returns true if the user is part of the audience, false otherwise
|
|
62
|
+
*/
|
|
63
|
+
function isTargetedGroup(sourceGroups, targetedGroups) {
|
|
64
|
+
if (sourceGroups === undefined) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return sourceGroups.some(group => targetedGroups.includes(group));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Determines if the user is part of the audience, based on the user id.
|
|
71
|
+
* @param userId user id from app context
|
|
72
|
+
* @param users targeted users from feature configuration
|
|
73
|
+
* @returns true if the user is part of the audience, false otherwise
|
|
74
|
+
*/
|
|
75
|
+
function isTargetedUser(userId, users) {
|
|
76
|
+
if (userId === undefined) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return users.includes(userId);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Constructs the context id for the audience.
|
|
83
|
+
* The context id is used to determine if the user is part of the audience for a feature.
|
|
84
|
+
*
|
|
85
|
+
* @param userId userId from app context
|
|
86
|
+
* @param hint hint string to be included in the context id
|
|
87
|
+
* @returns a string that represents the context id for the audience
|
|
88
|
+
*/
|
|
89
|
+
function constructAudienceContextId(userId, hint) {
|
|
90
|
+
return `${userId ?? ""}\n${hint}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Converts a string to a uint32 in little-endian encoding.
|
|
94
|
+
* @param str the string to convert.
|
|
95
|
+
* @returns a uint32 value.
|
|
96
|
+
*/
|
|
97
|
+
async function stringToUint32(str) {
|
|
98
|
+
let crypto;
|
|
99
|
+
// Check for browser environment
|
|
100
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
101
|
+
crypto = window.crypto;
|
|
102
|
+
}
|
|
103
|
+
// Check for Node.js environment
|
|
104
|
+
else if (typeof global !== "undefined" && global.crypto) {
|
|
105
|
+
crypto = global.crypto;
|
|
106
|
+
}
|
|
107
|
+
// Fallback to native Node.js crypto module
|
|
108
|
+
else {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
111
|
+
crypto = require("crypto");
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
crypto = await import('crypto');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error("Failed to load the crypto module:", error.message);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// In the browser, use crypto.subtle.digest
|
|
123
|
+
if (crypto.subtle) {
|
|
124
|
+
const data = new TextEncoder().encode(str);
|
|
125
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
126
|
+
const dataView = new DataView(hashBuffer);
|
|
127
|
+
const uint32 = dataView.getUint32(0, true);
|
|
128
|
+
return uint32;
|
|
129
|
+
}
|
|
130
|
+
// In Node.js, use the crypto module's hash function
|
|
131
|
+
else {
|
|
132
|
+
const hash = crypto.createHash("sha256").update(str).digest();
|
|
133
|
+
const uint32 = hash.readUInt32LE(0);
|
|
134
|
+
return uint32;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
25
138
|
// Copyright (c) Microsoft Corporation.
|
|
26
139
|
// Licensed under the MIT license.
|
|
27
140
|
class TargetingFilter {
|
|
@@ -60,26 +173,16 @@
|
|
|
60
173
|
parameters.Audience.Groups !== undefined) {
|
|
61
174
|
for (const group of parameters.Audience.Groups) {
|
|
62
175
|
if (appContext.groups.includes(group.Name)) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {
|
|
176
|
+
const hint = `${featureName}\n${group.Name}`;
|
|
177
|
+
if (await isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) {
|
|
66
178
|
return true;
|
|
67
179
|
}
|
|
68
180
|
}
|
|
69
181
|
}
|
|
70
182
|
}
|
|
71
183
|
// check if the user is being targeted by a default rollout percentage
|
|
72
|
-
const
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
static async #isTargeted(audienceContextId, rolloutPercentage) {
|
|
76
|
-
if (rolloutPercentage === 100) {
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
// Cryptographic hashing algorithms ensure adequate entropy across hash values.
|
|
80
|
-
const contextMarker = await stringToUint32(audienceContextId);
|
|
81
|
-
const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
|
|
82
|
-
return contextPercentage < rolloutPercentage;
|
|
184
|
+
const hint = featureName;
|
|
185
|
+
return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
|
|
83
186
|
}
|
|
84
187
|
static #validateParameters(parameters) {
|
|
85
188
|
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
|
|
@@ -95,64 +198,15 @@
|
|
|
95
198
|
}
|
|
96
199
|
}
|
|
97
200
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* @param userId userId from app context
|
|
108
|
-
* @param groupName group name from app context
|
|
109
|
-
* @returns a string that represents the context id for the audience
|
|
110
|
-
*/
|
|
111
|
-
function constructAudienceContextId(featureName, userId, groupName) {
|
|
112
|
-
let contextId = `${userId ?? ""}\n${featureName}`;
|
|
113
|
-
if (groupName !== undefined) {
|
|
114
|
-
contextId += `\n${groupName}`;
|
|
115
|
-
}
|
|
116
|
-
return contextId;
|
|
117
|
-
}
|
|
118
|
-
async function stringToUint32(str) {
|
|
119
|
-
let crypto;
|
|
120
|
-
// Check for browser environment
|
|
121
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
122
|
-
crypto = window.crypto;
|
|
123
|
-
}
|
|
124
|
-
// Check for Node.js environment
|
|
125
|
-
else if (typeof global !== "undefined" && global.crypto) {
|
|
126
|
-
crypto = global.crypto;
|
|
127
|
-
}
|
|
128
|
-
// Fallback to native Node.js crypto module
|
|
129
|
-
else {
|
|
130
|
-
try {
|
|
131
|
-
if (typeof module !== "undefined" && module.exports) {
|
|
132
|
-
crypto = require("crypto");
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
crypto = await import('crypto');
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
console.error("Failed to load the crypto module:", error.message);
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// In the browser, use crypto.subtle.digest
|
|
144
|
-
if (crypto.subtle) {
|
|
145
|
-
const data = new TextEncoder().encode(str);
|
|
146
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
147
|
-
const dataView = new DataView(hashBuffer);
|
|
148
|
-
const uint32 = dataView.getUint32(0, true);
|
|
149
|
-
return uint32;
|
|
150
|
-
}
|
|
151
|
-
// In Node.js, use the crypto module's hash function
|
|
152
|
-
else {
|
|
153
|
-
const hash = crypto.createHash("sha256").update(str).digest();
|
|
154
|
-
const uint32 = hash.readUInt32LE(0);
|
|
155
|
-
return uint32;
|
|
201
|
+
|
|
202
|
+
// Copyright (c) Microsoft Corporation.
|
|
203
|
+
// Licensed under the MIT license.
|
|
204
|
+
class Variant {
|
|
205
|
+
name;
|
|
206
|
+
configuration;
|
|
207
|
+
constructor(name, configuration) {
|
|
208
|
+
this.name = name;
|
|
209
|
+
this.configuration = configuration;
|
|
156
210
|
}
|
|
157
211
|
}
|
|
158
212
|
|
|
@@ -161,6 +215,7 @@
|
|
|
161
215
|
class FeatureManager {
|
|
162
216
|
#provider;
|
|
163
217
|
#featureFilters = new Map();
|
|
218
|
+
#onFeatureEvaluated;
|
|
164
219
|
constructor(provider, options) {
|
|
165
220
|
this.#provider = provider;
|
|
166
221
|
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];
|
|
@@ -168,6 +223,7 @@
|
|
|
168
223
|
for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
|
|
169
224
|
this.#featureFilters.set(filter.name, filter);
|
|
170
225
|
}
|
|
226
|
+
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
|
|
171
227
|
}
|
|
172
228
|
async listFeatureNames() {
|
|
173
229
|
const features = await this.#provider.getFeatureFlags();
|
|
@@ -176,13 +232,42 @@
|
|
|
176
232
|
}
|
|
177
233
|
// If multiple feature flags are found, the first one takes precedence.
|
|
178
234
|
async isEnabled(featureName, context) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
235
|
+
const result = await this.#evaluateFeature(featureName, context);
|
|
236
|
+
return result.enabled;
|
|
237
|
+
}
|
|
238
|
+
async getVariant(featureName, context) {
|
|
239
|
+
const result = await this.#evaluateFeature(featureName, context);
|
|
240
|
+
return result.variant;
|
|
241
|
+
}
|
|
242
|
+
async #assignVariant(featureFlag, context) {
|
|
243
|
+
// user allocation
|
|
244
|
+
if (featureFlag.allocation?.user !== undefined) {
|
|
245
|
+
for (const userAllocation of featureFlag.allocation.user) {
|
|
246
|
+
if (isTargetedUser(context.userId, userAllocation.users)) {
|
|
247
|
+
return getVariantAssignment(featureFlag, userAllocation.variant, exports.VariantAssignmentReason.User);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
183
250
|
}
|
|
184
|
-
//
|
|
185
|
-
|
|
251
|
+
// group allocation
|
|
252
|
+
if (featureFlag.allocation?.group !== undefined) {
|
|
253
|
+
for (const groupAllocation of featureFlag.allocation.group) {
|
|
254
|
+
if (isTargetedGroup(context.groups, groupAllocation.groups)) {
|
|
255
|
+
return getVariantAssignment(featureFlag, groupAllocation.variant, exports.VariantAssignmentReason.Group);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// percentile allocation
|
|
260
|
+
if (featureFlag.allocation?.percentile !== undefined) {
|
|
261
|
+
for (const percentileAllocation of featureFlag.allocation.percentile) {
|
|
262
|
+
const hint = featureFlag.allocation.seed ?? `allocation\n${featureFlag.id}`;
|
|
263
|
+
if (await isTargetedPercentile(context.userId, hint, percentileAllocation.from, percentileAllocation.to)) {
|
|
264
|
+
return getVariantAssignment(featureFlag, percentileAllocation.variant, exports.VariantAssignmentReason.Percentile);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { variant: undefined, reason: exports.VariantAssignmentReason.None };
|
|
269
|
+
}
|
|
270
|
+
async #isEnabled(featureFlag, context) {
|
|
186
271
|
if (featureFlag.enabled !== true) {
|
|
187
272
|
// If the feature is not explicitly enabled, then it is disabled by default.
|
|
188
273
|
return false;
|
|
@@ -201,7 +286,7 @@
|
|
|
201
286
|
const shortCircuitEvaluationResult = requirementType === "Any";
|
|
202
287
|
for (const clientFilter of clientFilters) {
|
|
203
288
|
const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
|
|
204
|
-
const contextWithFeatureName = { featureName, parameters: clientFilter.parameters };
|
|
289
|
+
const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters };
|
|
205
290
|
if (matchedFeatureFilter === undefined) {
|
|
206
291
|
console.warn(`Feature filter ${clientFilter.name} is not found.`);
|
|
207
292
|
return false;
|
|
@@ -213,11 +298,153 @@
|
|
|
213
298
|
// If we get here, then we have not found a client filter that matches the requirement type.
|
|
214
299
|
return !shortCircuitEvaluationResult;
|
|
215
300
|
}
|
|
301
|
+
async #evaluateFeature(featureName, context) {
|
|
302
|
+
const featureFlag = await this.#provider.getFeatureFlag(featureName);
|
|
303
|
+
const result = new EvaluationResult(featureFlag);
|
|
304
|
+
if (featureFlag === undefined) {
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
// Ensure that the feature flag is in the correct format. Feature providers should validate the feature flags, but we do it here as a safeguard.
|
|
308
|
+
// TODO: move to the feature flag provider implementation.
|
|
309
|
+
validateFeatureFlagFormat(featureFlag);
|
|
310
|
+
// Evaluate if the feature is enabled.
|
|
311
|
+
result.enabled = await this.#isEnabled(featureFlag, context);
|
|
312
|
+
const targetingContext = context;
|
|
313
|
+
result.targetingId = targetingContext?.userId;
|
|
314
|
+
// Determine Variant
|
|
315
|
+
let variantDef;
|
|
316
|
+
let reason = exports.VariantAssignmentReason.None;
|
|
317
|
+
// featureFlag.variant not empty
|
|
318
|
+
if (featureFlag.variants !== undefined && featureFlag.variants.length > 0) {
|
|
319
|
+
if (!result.enabled) {
|
|
320
|
+
// not enabled, assign default if specified
|
|
321
|
+
if (featureFlag.allocation?.default_when_disabled !== undefined) {
|
|
322
|
+
variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_disabled);
|
|
323
|
+
reason = exports.VariantAssignmentReason.DefaultWhenDisabled;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
// no default specified
|
|
327
|
+
variantDef = undefined;
|
|
328
|
+
reason = exports.VariantAssignmentReason.DefaultWhenDisabled;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// enabled, assign based on allocation
|
|
333
|
+
if (context !== undefined && featureFlag.allocation !== undefined) {
|
|
334
|
+
const variantAndReason = await this.#assignVariant(featureFlag, targetingContext);
|
|
335
|
+
variantDef = variantAndReason.variant;
|
|
336
|
+
reason = variantAndReason.reason;
|
|
337
|
+
}
|
|
338
|
+
// allocation failed, assign default if specified
|
|
339
|
+
if (variantDef === undefined && reason === exports.VariantAssignmentReason.None) {
|
|
340
|
+
if (featureFlag.allocation?.default_when_enabled !== undefined) {
|
|
341
|
+
variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_enabled);
|
|
342
|
+
reason = exports.VariantAssignmentReason.DefaultWhenEnabled;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
variantDef = undefined;
|
|
346
|
+
reason = exports.VariantAssignmentReason.DefaultWhenEnabled;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
result.variant = variantDef !== undefined ? new Variant(variantDef.name, variantDef.configuration_value) : undefined;
|
|
352
|
+
result.variantAssignmentReason = reason;
|
|
353
|
+
// Status override for isEnabled
|
|
354
|
+
if (variantDef !== undefined && featureFlag.enabled) {
|
|
355
|
+
if (variantDef.status_override === "Enabled") {
|
|
356
|
+
result.enabled = true;
|
|
357
|
+
}
|
|
358
|
+
else if (variantDef.status_override === "Disabled") {
|
|
359
|
+
result.enabled = false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// The callback will only be executed if telemetry is enabled for the feature flag
|
|
363
|
+
if (featureFlag.telemetry?.enabled && this.#onFeatureEvaluated !== undefined) {
|
|
364
|
+
this.#onFeatureEvaluated(result);
|
|
365
|
+
}
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
216
368
|
}
|
|
369
|
+
class EvaluationResult {
|
|
370
|
+
feature;
|
|
371
|
+
enabled;
|
|
372
|
+
targetingId;
|
|
373
|
+
variant;
|
|
374
|
+
variantAssignmentReason;
|
|
375
|
+
constructor(
|
|
376
|
+
// feature flag definition
|
|
377
|
+
feature,
|
|
378
|
+
// enabled state
|
|
379
|
+
enabled = false,
|
|
380
|
+
// variant assignment
|
|
381
|
+
targetingId = undefined, variant = undefined, variantAssignmentReason = exports.VariantAssignmentReason.None) {
|
|
382
|
+
this.feature = feature;
|
|
383
|
+
this.enabled = enabled;
|
|
384
|
+
this.targetingId = targetingId;
|
|
385
|
+
this.variant = variant;
|
|
386
|
+
this.variantAssignmentReason = variantAssignmentReason;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
exports.VariantAssignmentReason = void 0;
|
|
390
|
+
(function (VariantAssignmentReason) {
|
|
391
|
+
/**
|
|
392
|
+
* Variant allocation did not happen. No variant is assigned.
|
|
393
|
+
*/
|
|
394
|
+
VariantAssignmentReason["None"] = "None";
|
|
395
|
+
/**
|
|
396
|
+
* The default variant is assigned when a feature flag is disabled.
|
|
397
|
+
*/
|
|
398
|
+
VariantAssignmentReason["DefaultWhenDisabled"] = "DefaultWhenDisabled";
|
|
399
|
+
/**
|
|
400
|
+
* The default variant is assigned because of no applicable user/group/percentile allocation when a feature flag is enabled.
|
|
401
|
+
*/
|
|
402
|
+
VariantAssignmentReason["DefaultWhenEnabled"] = "DefaultWhenEnabled";
|
|
403
|
+
/**
|
|
404
|
+
* The variant is assigned because of the user allocation when a feature flag is enabled.
|
|
405
|
+
*/
|
|
406
|
+
VariantAssignmentReason["User"] = "User";
|
|
407
|
+
/**
|
|
408
|
+
* The variant is assigned because of the group allocation when a feature flag is enabled.
|
|
409
|
+
*/
|
|
410
|
+
VariantAssignmentReason["Group"] = "Group";
|
|
411
|
+
/**
|
|
412
|
+
* The variant is assigned because of the percentile allocation when a feature flag is enabled.
|
|
413
|
+
*/
|
|
414
|
+
VariantAssignmentReason["Percentile"] = "Percentile";
|
|
415
|
+
})(exports.VariantAssignmentReason || (exports.VariantAssignmentReason = {}));
|
|
416
|
+
/**
|
|
417
|
+
* Validates the format of the feature flag definition.
|
|
418
|
+
*
|
|
419
|
+
* FeatureFlag data objects are from IFeatureFlagProvider, depending on the implementation.
|
|
420
|
+
* Thus the properties are not guaranteed to have the expected types.
|
|
421
|
+
*
|
|
422
|
+
* @param featureFlag The feature flag definition to validate.
|
|
423
|
+
*/
|
|
217
424
|
function validateFeatureFlagFormat(featureFlag) {
|
|
218
425
|
if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") {
|
|
219
426
|
throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`);
|
|
220
427
|
}
|
|
428
|
+
// TODO: add more validations.
|
|
429
|
+
// TODO: should be moved to the feature flag provider.
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Try to get the variant assignment for the given variant name. If the variant is not found, override the reason with VariantAssignmentReason.None.
|
|
433
|
+
*
|
|
434
|
+
* @param featureFlag feature flag definition
|
|
435
|
+
* @param variantName variant name
|
|
436
|
+
* @param reason variant assignment reason
|
|
437
|
+
* @returns variant assignment containing the variant definition and the reason
|
|
438
|
+
*/
|
|
439
|
+
function getVariantAssignment(featureFlag, variantName, reason) {
|
|
440
|
+
const variant = featureFlag.variants?.find(v => v.name == variantName);
|
|
441
|
+
if (variant !== undefined) {
|
|
442
|
+
return { variant, reason };
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
console.warn(`Variant ${variantName} not found for feature ${featureFlag.id}.`);
|
|
446
|
+
return { variant: undefined, reason: exports.VariantAssignmentReason.None };
|
|
447
|
+
}
|
|
221
448
|
}
|
|
222
449
|
|
|
223
450
|
// Copyright (c) Microsoft Corporation.
|
|
@@ -239,7 +466,7 @@
|
|
|
239
466
|
}
|
|
240
467
|
async getFeatureFlag(featureName) {
|
|
241
468
|
const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
|
|
242
|
-
return featureConfig?.[FEATURE_FLAGS_KEY]?.
|
|
469
|
+
return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
|
|
243
470
|
}
|
|
244
471
|
async getFeatureFlags() {
|
|
245
472
|
const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
|
|
@@ -256,7 +483,7 @@
|
|
|
256
483
|
}
|
|
257
484
|
async getFeatureFlag(featureName) {
|
|
258
485
|
const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];
|
|
259
|
-
return featureFlags?.
|
|
486
|
+
return featureFlags?.find((feature) => feature.id === featureName);
|
|
260
487
|
}
|
|
261
488
|
async getFeatureFlags() {
|
|
262
489
|
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
|
|
@@ -265,10 +492,11 @@
|
|
|
265
492
|
|
|
266
493
|
// Copyright (c) Microsoft Corporation.
|
|
267
494
|
// Licensed under the MIT license.
|
|
268
|
-
const VERSION = "
|
|
495
|
+
const VERSION = "2.0.0-preview.2";
|
|
269
496
|
|
|
270
497
|
exports.ConfigurationMapFeatureFlagProvider = ConfigurationMapFeatureFlagProvider;
|
|
271
498
|
exports.ConfigurationObjectFeatureFlagProvider = ConfigurationObjectFeatureFlagProvider;
|
|
499
|
+
exports.EvaluationResult = EvaluationResult;
|
|
272
500
|
exports.FeatureManager = FeatureManager;
|
|
273
501
|
exports.VERSION = VERSION;
|
|
274
502
|
|