@microsoft/feature-management 1.0.0-preview.1 → 2.0.0-preview.1

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 (56) hide show
  1. package/README.md +2 -0
  2. package/dist/commonjs/common/targetingEvaluator.js +119 -0
  3. package/dist/commonjs/common/targetingEvaluator.js.map +1 -0
  4. package/dist/commonjs/featureManager.js +247 -0
  5. package/dist/commonjs/featureManager.js.map +1 -0
  6. package/dist/commonjs/featureProvider.js +43 -0
  7. package/dist/commonjs/featureProvider.js.map +1 -0
  8. package/{dist-esm → dist/commonjs}/filter/TargetingFilter.js +13 -47
  9. package/dist/commonjs/filter/TargetingFilter.js.map +1 -0
  10. package/dist/commonjs/filter/TimeWindowFilter.js +22 -0
  11. package/dist/commonjs/filter/TimeWindowFilter.js.map +1 -0
  12. package/dist/commonjs/index.js +18 -0
  13. package/dist/commonjs/index.js.map +1 -0
  14. package/dist/commonjs/model.js +12 -0
  15. package/dist/commonjs/model.js.map +1 -0
  16. package/dist/commonjs/variant/Variant.js +15 -0
  17. package/dist/commonjs/variant/Variant.js.map +1 -0
  18. package/dist/commonjs/version.js +8 -0
  19. package/dist/commonjs/version.js.map +1 -0
  20. package/dist/esm/common/targetingEvaluator.js +115 -0
  21. package/dist/esm/common/targetingEvaluator.js.map +1 -0
  22. package/dist/esm/featureManager.js +244 -0
  23. package/dist/esm/featureManager.js.map +1 -0
  24. package/{dist-esm → dist/esm}/featureProvider.js +7 -4
  25. package/dist/esm/featureProvider.js.map +1 -0
  26. package/dist/esm/filter/TargetingFilter.js +68 -0
  27. package/dist/esm/filter/TargetingFilter.js.map +1 -0
  28. package/{dist-esm → dist/esm}/filter/TimeWindowFilter.js +4 -2
  29. package/dist/esm/filter/TimeWindowFilter.js.map +1 -0
  30. package/dist/esm/index.js +4 -0
  31. package/dist/esm/index.js.map +1 -0
  32. package/dist/esm/model.js +9 -0
  33. package/dist/esm/model.js.map +1 -0
  34. package/dist/esm/variant/Variant.js +13 -0
  35. package/dist/esm/variant/Variant.js.map +1 -0
  36. package/dist/esm/version.js +6 -0
  37. package/dist/esm/version.js.map +1 -0
  38. package/dist/umd/index.js +504 -0
  39. package/dist/umd/index.js.map +1 -0
  40. package/package.json +14 -19
  41. package/types/index.d.ts +195 -15
  42. package/dist/index.js +0 -234
  43. package/dist/index.js.map +0 -1
  44. package/dist-esm/featureManager.js +0 -60
  45. package/dist-esm/featureManager.js.map +0 -1
  46. package/dist-esm/featureProvider.js.map +0 -1
  47. package/dist-esm/filter/FeatureFilter.js +0 -4
  48. package/dist-esm/filter/FeatureFilter.js.map +0 -1
  49. package/dist-esm/filter/TargetingFilter.js.map +0 -1
  50. package/dist-esm/filter/TimeWindowFilter.js.map +0 -1
  51. package/dist-esm/gettable.js +0 -6
  52. package/dist-esm/gettable.js.map +0 -1
  53. package/dist-esm/index.js +0 -5
  54. package/dist-esm/index.js.map +0 -1
  55. package/dist-esm/model.js +0 -12
  56. package/dist-esm/model.js.map +0 -1
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Microsoft Feature Management for JavaScript
2
2
 
3
+ [![feature-management](https://img.shields.io/npm/v/@microsoft/feature-management?label=@microsoft/feature-management)](https://www.npmjs.com/package/@microsoft/feature-management)
4
+
3
5
  Feature Management is a library for enabling/disabling features at runtime.
4
6
  Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.
5
7
 
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ // Copyright (c) Microsoft Corporation.
4
+ // Licensed under the MIT license.
5
+ /**
6
+ * Determines if the user is part of the audience, based on the user id and the percentage range.
7
+ *
8
+ * @param userId user id from app context
9
+ * @param hint hint string to be included in the context id
10
+ * @param from percentage range start
11
+ * @param to percentage range end
12
+ * @returns true if the user is part of the audience, false otherwise
13
+ */
14
+ async function isTargetedPercentile(userId, hint, from, to) {
15
+ if (from < 0 || from > 100) {
16
+ throw new Error("The 'from' value must be between 0 and 100.");
17
+ }
18
+ if (to < 0 || to > 100) {
19
+ throw new Error("The 'to' value must be between 0 and 100.");
20
+ }
21
+ if (from > to) {
22
+ throw new Error("The 'from' value cannot be larger than the 'to' value.");
23
+ }
24
+ const audienceContextId = constructAudienceContextId(userId, hint);
25
+ // Cryptographic hashing algorithms ensure adequate entropy across hash values.
26
+ const contextMarker = await stringToUint32(audienceContextId);
27
+ const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
28
+ // Handle edge case of exact 100 bucket
29
+ if (to === 100) {
30
+ return contextPercentage >= from;
31
+ }
32
+ return contextPercentage >= from && contextPercentage < to;
33
+ }
34
+ /**
35
+ * Determines if the user is part of the audience, based on the groups they belong to.
36
+ *
37
+ * @param sourceGroups user groups from app context
38
+ * @param targetedGroups targeted groups from feature configuration
39
+ * @returns true if the user is part of the audience, false otherwise
40
+ */
41
+ function isTargetedGroup(sourceGroups, targetedGroups) {
42
+ if (sourceGroups === undefined) {
43
+ return false;
44
+ }
45
+ return sourceGroups.some(group => targetedGroups.includes(group));
46
+ }
47
+ /**
48
+ * Determines if the user is part of the audience, based on the user id.
49
+ * @param userId user id from app context
50
+ * @param users targeted users from feature configuration
51
+ * @returns true if the user is part of the audience, false otherwise
52
+ */
53
+ function isTargetedUser(userId, users) {
54
+ if (userId === undefined) {
55
+ return false;
56
+ }
57
+ return users.includes(userId);
58
+ }
59
+ /**
60
+ * Constructs the context id for the audience.
61
+ * The context id is used to determine if the user is part of the audience for a feature.
62
+ *
63
+ * @param userId userId from app context
64
+ * @param hint hint string to be included in the context id
65
+ * @returns a string that represents the context id for the audience
66
+ */
67
+ function constructAudienceContextId(userId, hint) {
68
+ return `${userId ?? ""}\n${hint}`;
69
+ }
70
+ /**
71
+ * Converts a string to a uint32 in little-endian encoding.
72
+ * @param str the string to convert.
73
+ * @returns a uint32 value.
74
+ */
75
+ async function stringToUint32(str) {
76
+ let crypto;
77
+ // Check for browser environment
78
+ if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
79
+ crypto = window.crypto;
80
+ }
81
+ // Check for Node.js environment
82
+ else if (typeof global !== "undefined" && global.crypto) {
83
+ crypto = global.crypto;
84
+ }
85
+ // Fallback to native Node.js crypto module
86
+ else {
87
+ try {
88
+ if (typeof module !== "undefined" && module.exports) {
89
+ crypto = require("crypto");
90
+ }
91
+ else {
92
+ crypto = await import('crypto');
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error("Failed to load the crypto module:", error.message);
97
+ throw error;
98
+ }
99
+ }
100
+ // In the browser, use crypto.subtle.digest
101
+ if (crypto.subtle) {
102
+ const data = new TextEncoder().encode(str);
103
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
104
+ const dataView = new DataView(hashBuffer);
105
+ const uint32 = dataView.getUint32(0, true);
106
+ return uint32;
107
+ }
108
+ // In Node.js, use the crypto module's hash function
109
+ else {
110
+ const hash = crypto.createHash("sha256").update(str).digest();
111
+ const uint32 = hash.readUInt32LE(0);
112
+ return uint32;
113
+ }
114
+ }
115
+
116
+ exports.isTargetedGroup = isTargetedGroup;
117
+ exports.isTargetedPercentile = isTargetedPercentile;
118
+ exports.isTargetedUser = isTargetedUser;
119
+ //# sourceMappingURL=targetingEvaluator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targetingEvaluator.js","sources":["../../../src/common/targetingEvaluator.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\n/**\n * Determines if the user is part of the audience, based on the user id and the percentage range.\n *\n * @param userId user id from app context\n * @param hint hint string to be included in the context id\n * @param from percentage range start\n * @param to percentage range end\n * @returns true if the user is part of the audience, false otherwise\n */\nexport async function isTargetedPercentile(userId: string | undefined, hint: string, from: number, to: number): Promise<boolean> {\n if (from < 0 || from > 100) {\n throw new Error(\"The 'from' value must be between 0 and 100.\");\n }\n if (to < 0 || to > 100) {\n throw new Error(\"The 'to' value must be between 0 and 100.\");\n }\n if (from > to) {\n throw new Error(\"The 'from' value cannot be larger than the 'to' value.\");\n }\n\n const audienceContextId = constructAudienceContextId(userId, hint);\n\n // Cryptographic hashing algorithms ensure adequate entropy across hash values.\n const contextMarker = await stringToUint32(audienceContextId);\n const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;\n\n // Handle edge case of exact 100 bucket\n if (to === 100) {\n return contextPercentage >= from;\n }\n\n return contextPercentage >= from && contextPercentage < to;\n}\n\n/**\n * Determines if the user is part of the audience, based on the groups they belong to.\n *\n * @param sourceGroups user groups from app context\n * @param targetedGroups targeted groups from feature configuration\n * @returns true if the user is part of the audience, false otherwise\n */\nexport function isTargetedGroup(sourceGroups: string[] | undefined, targetedGroups: string[]): boolean {\n if (sourceGroups === undefined) {\n return false;\n }\n\n return sourceGroups.some(group => targetedGroups.includes(group));\n}\n\n/**\n * Determines if the user is part of the audience, based on the user id.\n * @param userId user id from app context\n * @param users targeted users from feature configuration\n * @returns true if the user is part of the audience, false otherwise\n */\nexport function isTargetedUser(userId: string | undefined, users: string[]): boolean {\n if (userId === undefined) {\n return false;\n }\n\n return users.includes(userId);\n}\n\n/**\n * Constructs the context id for the audience.\n * The context id is used to determine if the user is part of the audience for a feature.\n *\n * @param userId userId from app context\n * @param hint hint string to be included in the context id\n * @returns a string that represents the context id for the audience\n */\nfunction constructAudienceContextId(userId: string | undefined, hint: string): string {\n return `${userId ?? \"\"}\\n${hint}`;\n}\n\n/**\n * Converts a string to a uint32 in little-endian encoding.\n * @param str the string to convert.\n * @returns a uint32 value.\n */\nasync function stringToUint32(str: string): Promise<number> {\n let crypto;\n\n // Check for browser environment\n if (typeof window !== \"undefined\" && window.crypto && window.crypto.subtle) {\n crypto = window.crypto;\n }\n // Check for Node.js environment\n else if (typeof global !== \"undefined\" && global.crypto) {\n crypto = global.crypto;\n }\n // Fallback to native Node.js crypto module\n else {\n try {\n if (typeof module !== \"undefined\" && module.exports) {\n crypto = require(\"crypto\");\n }\n else {\n crypto = await import(\"crypto\");\n }\n } catch (error) {\n console.error(\"Failed to load the crypto module:\", error.message);\n throw error;\n }\n }\n\n // In the browser, use crypto.subtle.digest\n if (crypto.subtle) {\n const data = new TextEncoder().encode(str);\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const dataView = new DataView(hashBuffer);\n const uint32 = dataView.getUint32(0, true);\n return uint32;\n }\n // In Node.js, use the crypto module's hash function\n else {\n const hash = crypto.createHash(\"sha256\").update(str).digest();\n const uint32 = hash.readUInt32LE(0);\n return uint32;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+
3
+ var TimeWindowFilter = require('./filter/TimeWindowFilter.js');
4
+ var TargetingFilter = require('./filter/TargetingFilter.js');
5
+ var Variant = require('./variant/Variant.js');
6
+ var targetingEvaluator = require('./common/targetingEvaluator.js');
7
+
8
+ // Copyright (c) Microsoft Corporation.
9
+ // Licensed under the MIT license.
10
+ class FeatureManager {
11
+ #provider;
12
+ #featureFilters = new Map();
13
+ #onFeatureEvaluated;
14
+ constructor(provider, options) {
15
+ this.#provider = provider;
16
+ const builtinFilters = [new TimeWindowFilter.TimeWindowFilter(), new TargetingFilter.TargetingFilter()];
17
+ // If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.
18
+ for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
19
+ this.#featureFilters.set(filter.name, filter);
20
+ }
21
+ this.#onFeatureEvaluated = options?.onFeatureEvaluated;
22
+ }
23
+ async listFeatureNames() {
24
+ const features = await this.#provider.getFeatureFlags();
25
+ const featureNameSet = new Set(features.map((feature) => feature.id));
26
+ return Array.from(featureNameSet);
27
+ }
28
+ // If multiple feature flags are found, the first one takes precedence.
29
+ async isEnabled(featureName, context) {
30
+ const result = await this.#evaluateFeature(featureName, context);
31
+ return result.enabled;
32
+ }
33
+ async getVariant(featureName, context) {
34
+ const result = await this.#evaluateFeature(featureName, context);
35
+ return result.variant;
36
+ }
37
+ async #assignVariant(featureFlag, context) {
38
+ // user allocation
39
+ if (featureFlag.allocation?.user !== undefined) {
40
+ for (const userAllocation of featureFlag.allocation.user) {
41
+ if (targetingEvaluator.isTargetedUser(context.userId, userAllocation.users)) {
42
+ return getVariantAssignment(featureFlag, userAllocation.variant, exports.VariantAssignmentReason.User);
43
+ }
44
+ }
45
+ }
46
+ // group allocation
47
+ if (featureFlag.allocation?.group !== undefined) {
48
+ for (const groupAllocation of featureFlag.allocation.group) {
49
+ if (targetingEvaluator.isTargetedGroup(context.groups, groupAllocation.groups)) {
50
+ return getVariantAssignment(featureFlag, groupAllocation.variant, exports.VariantAssignmentReason.Group);
51
+ }
52
+ }
53
+ }
54
+ // percentile allocation
55
+ if (featureFlag.allocation?.percentile !== undefined) {
56
+ for (const percentileAllocation of featureFlag.allocation.percentile) {
57
+ const hint = featureFlag.allocation.seed ?? `allocation\n${featureFlag.id}`;
58
+ if (await targetingEvaluator.isTargetedPercentile(context.userId, hint, percentileAllocation.from, percentileAllocation.to)) {
59
+ return getVariantAssignment(featureFlag, percentileAllocation.variant, exports.VariantAssignmentReason.Percentile);
60
+ }
61
+ }
62
+ }
63
+ return { variant: undefined, reason: exports.VariantAssignmentReason.None };
64
+ }
65
+ async #isEnabled(featureFlag, context) {
66
+ if (featureFlag.enabled !== true) {
67
+ // If the feature is not explicitly enabled, then it is disabled by default.
68
+ return false;
69
+ }
70
+ const clientFilters = featureFlag.conditions?.client_filters;
71
+ if (clientFilters === undefined || clientFilters.length <= 0) {
72
+ // If there are no client filters, then the feature is enabled.
73
+ return true;
74
+ }
75
+ const requirementType = featureFlag.conditions?.requirement_type ?? "Any"; // default to any.
76
+ /**
77
+ * While iterating through the client filters, we short-circuit the evaluation based on the requirement type.
78
+ * - When requirement type is "All", the feature is enabled if all client filters are matched. If any client filter is not matched, the feature is disabled, otherwise it is enabled. `shortCircuitEvaluationResult` is false.
79
+ * - When requirement type is "Any", the feature is enabled if any client filter is matched. If any client filter is matched, the feature is enabled, otherwise it is disabled. `shortCircuitEvaluationResult` is true.
80
+ */
81
+ const shortCircuitEvaluationResult = requirementType === "Any";
82
+ for (const clientFilter of clientFilters) {
83
+ const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
84
+ const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters };
85
+ if (matchedFeatureFilter === undefined) {
86
+ console.warn(`Feature filter ${clientFilter.name} is not found.`);
87
+ return false;
88
+ }
89
+ if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) {
90
+ return shortCircuitEvaluationResult;
91
+ }
92
+ }
93
+ // If we get here, then we have not found a client filter that matches the requirement type.
94
+ return !shortCircuitEvaluationResult;
95
+ }
96
+ async #evaluateFeature(featureName, context) {
97
+ const featureFlag = await this.#provider.getFeatureFlag(featureName);
98
+ const result = new EvaluationResult(featureFlag);
99
+ if (featureFlag === undefined) {
100
+ return result;
101
+ }
102
+ // 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.
103
+ // TODO: move to the feature flag provider implementation.
104
+ validateFeatureFlagFormat(featureFlag);
105
+ // Evaluate if the feature is enabled.
106
+ result.enabled = await this.#isEnabled(featureFlag, context);
107
+ const targetingContext = context;
108
+ result.targetingId = targetingContext?.userId;
109
+ // Determine Variant
110
+ let variantDef;
111
+ let reason = exports.VariantAssignmentReason.None;
112
+ // featureFlag.variant not empty
113
+ if (featureFlag.variants !== undefined && featureFlag.variants.length > 0) {
114
+ if (!result.enabled) {
115
+ // not enabled, assign default if specified
116
+ if (featureFlag.allocation?.default_when_disabled !== undefined) {
117
+ variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_disabled);
118
+ reason = exports.VariantAssignmentReason.DefaultWhenDisabled;
119
+ }
120
+ else {
121
+ // no default specified
122
+ variantDef = undefined;
123
+ reason = exports.VariantAssignmentReason.DefaultWhenDisabled;
124
+ }
125
+ }
126
+ else {
127
+ // enabled, assign based on allocation
128
+ if (context !== undefined && featureFlag.allocation !== undefined) {
129
+ const variantAndReason = await this.#assignVariant(featureFlag, targetingContext);
130
+ variantDef = variantAndReason.variant;
131
+ reason = variantAndReason.reason;
132
+ }
133
+ // allocation failed, assign default if specified
134
+ if (variantDef === undefined && reason === exports.VariantAssignmentReason.None) {
135
+ if (featureFlag.allocation?.default_when_enabled !== undefined) {
136
+ variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_enabled);
137
+ reason = exports.VariantAssignmentReason.DefaultWhenEnabled;
138
+ }
139
+ else {
140
+ variantDef = undefined;
141
+ reason = exports.VariantAssignmentReason.DefaultWhenEnabled;
142
+ }
143
+ }
144
+ }
145
+ }
146
+ result.variant = variantDef !== undefined ? new Variant.Variant(variantDef.name, variantDef.configuration_value) : undefined;
147
+ result.variantAssignmentReason = reason;
148
+ // Status override for isEnabled
149
+ if (variantDef !== undefined && featureFlag.enabled) {
150
+ if (variantDef.status_override === "Enabled") {
151
+ result.enabled = true;
152
+ }
153
+ else if (variantDef.status_override === "Disabled") {
154
+ result.enabled = false;
155
+ }
156
+ }
157
+ // The callback will only be executed if telemetry is enabled for the feature flag
158
+ if (featureFlag.telemetry?.enabled && this.#onFeatureEvaluated !== undefined) {
159
+ this.#onFeatureEvaluated(result);
160
+ }
161
+ return result;
162
+ }
163
+ }
164
+ class EvaluationResult {
165
+ feature;
166
+ enabled;
167
+ targetingId;
168
+ variant;
169
+ variantAssignmentReason;
170
+ constructor(
171
+ // feature flag definition
172
+ feature,
173
+ // enabled state
174
+ enabled = false,
175
+ // variant assignment
176
+ targetingId = undefined, variant = undefined, variantAssignmentReason = exports.VariantAssignmentReason.None) {
177
+ this.feature = feature;
178
+ this.enabled = enabled;
179
+ this.targetingId = targetingId;
180
+ this.variant = variant;
181
+ this.variantAssignmentReason = variantAssignmentReason;
182
+ }
183
+ }
184
+ exports.VariantAssignmentReason = void 0;
185
+ (function (VariantAssignmentReason) {
186
+ /**
187
+ * Variant allocation did not happen. No variant is assigned.
188
+ */
189
+ VariantAssignmentReason["None"] = "None";
190
+ /**
191
+ * The default variant is assigned when a feature flag is disabled.
192
+ */
193
+ VariantAssignmentReason["DefaultWhenDisabled"] = "DefaultWhenDisabled";
194
+ /**
195
+ * The default variant is assigned because of no applicable user/group/percentile allocation when a feature flag is enabled.
196
+ */
197
+ VariantAssignmentReason["DefaultWhenEnabled"] = "DefaultWhenEnabled";
198
+ /**
199
+ * The variant is assigned because of the user allocation when a feature flag is enabled.
200
+ */
201
+ VariantAssignmentReason["User"] = "User";
202
+ /**
203
+ * The variant is assigned because of the group allocation when a feature flag is enabled.
204
+ */
205
+ VariantAssignmentReason["Group"] = "Group";
206
+ /**
207
+ * The variant is assigned because of the percentile allocation when a feature flag is enabled.
208
+ */
209
+ VariantAssignmentReason["Percentile"] = "Percentile";
210
+ })(exports.VariantAssignmentReason || (exports.VariantAssignmentReason = {}));
211
+ /**
212
+ * Validates the format of the feature flag definition.
213
+ *
214
+ * FeatureFlag data objects are from IFeatureFlagProvider, depending on the implementation.
215
+ * Thus the properties are not guaranteed to have the expected types.
216
+ *
217
+ * @param featureFlag The feature flag definition to validate.
218
+ */
219
+ function validateFeatureFlagFormat(featureFlag) {
220
+ if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") {
221
+ throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`);
222
+ }
223
+ // TODO: add more validations.
224
+ // TODO: should be moved to the feature flag provider.
225
+ }
226
+ /**
227
+ * Try to get the variant assignment for the given variant name. If the variant is not found, override the reason with VariantAssignmentReason.None.
228
+ *
229
+ * @param featureFlag feature flag definition
230
+ * @param variantName variant name
231
+ * @param reason variant assignment reason
232
+ * @returns variant assignment containing the variant definition and the reason
233
+ */
234
+ function getVariantAssignment(featureFlag, variantName, reason) {
235
+ const variant = featureFlag.variants?.find(v => v.name == variantName);
236
+ if (variant !== undefined) {
237
+ return { variant, reason };
238
+ }
239
+ else {
240
+ console.warn(`Variant ${variantName} not found for feature ${featureFlag.id}.`);
241
+ return { variant: undefined, reason: exports.VariantAssignmentReason.None };
242
+ }
243
+ }
244
+
245
+ exports.EvaluationResult = EvaluationResult;
246
+ exports.FeatureManager = FeatureManager;
247
+ //# sourceMappingURL=featureManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureManager.js","sources":["../../src/featureManager.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { TimeWindowFilter } from \"./filter/TimeWindowFilter.js\";\nimport { IFeatureFilter } from \"./filter/FeatureFilter.js\";\nimport { FeatureFlag, RequirementType, VariantDefinition } from \"./model.js\";\nimport { IFeatureFlagProvider } from \"./featureProvider.js\";\nimport { TargetingFilter } from \"./filter/TargetingFilter.js\";\nimport { Variant } from \"./variant/Variant.js\";\nimport { IFeatureManager } from \"./IFeatureManager.js\";\nimport { ITargetingContext } from \"./common/ITargetingContext.js\";\nimport { isTargetedGroup, isTargetedPercentile, isTargetedUser } from \"./common/targetingEvaluator.js\";\n\nexport class FeatureManager implements IFeatureManager {\n #provider: IFeatureFlagProvider;\n #featureFilters: Map<string, IFeatureFilter> = new Map();\n #onFeatureEvaluated?: (event: EvaluationResult) => void;\n\n constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) {\n this.#provider = provider;\n\n const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];\n\n // If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.\n for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {\n this.#featureFilters.set(filter.name, filter);\n }\n\n this.#onFeatureEvaluated = options?.onFeatureEvaluated;\n }\n\n async listFeatureNames(): Promise<string[]> {\n const features = await this.#provider.getFeatureFlags();\n const featureNameSet = new Set(features.map((feature) => feature.id));\n return Array.from(featureNameSet);\n }\n\n // If multiple feature flags are found, the first one takes precedence.\n async isEnabled(featureName: string, context?: unknown): Promise<boolean> {\n const result = await this.#evaluateFeature(featureName, context);\n return result.enabled;\n }\n\n async getVariant(featureName: string, context?: ITargetingContext): Promise<Variant | undefined> {\n const result = await this.#evaluateFeature(featureName, context);\n return result.variant;\n }\n\n async #assignVariant(featureFlag: FeatureFlag, context: ITargetingContext): Promise<VariantAssignment> {\n // user allocation\n if (featureFlag.allocation?.user !== undefined) {\n for (const userAllocation of featureFlag.allocation.user) {\n if (isTargetedUser(context.userId, userAllocation.users)) {\n return getVariantAssignment(featureFlag, userAllocation.variant, VariantAssignmentReason.User);\n }\n }\n }\n\n // group allocation\n if (featureFlag.allocation?.group !== undefined) {\n for (const groupAllocation of featureFlag.allocation.group) {\n if (isTargetedGroup(context.groups, groupAllocation.groups)) {\n return getVariantAssignment(featureFlag, groupAllocation.variant, VariantAssignmentReason.Group);\n }\n }\n }\n\n // percentile allocation\n if (featureFlag.allocation?.percentile !== undefined) {\n for (const percentileAllocation of featureFlag.allocation.percentile) {\n const hint = featureFlag.allocation.seed ?? `allocation\\n${featureFlag.id}`;\n if (await isTargetedPercentile(context.userId, hint, percentileAllocation.from, percentileAllocation.to)) {\n return getVariantAssignment(featureFlag, percentileAllocation.variant, VariantAssignmentReason.Percentile);\n }\n }\n }\n\n return { variant: undefined, reason: VariantAssignmentReason.None };\n }\n\n async #isEnabled(featureFlag: FeatureFlag, context?: unknown): Promise<boolean> {\n if (featureFlag.enabled !== true) {\n // If the feature is not explicitly enabled, then it is disabled by default.\n return false;\n }\n\n const clientFilters = featureFlag.conditions?.client_filters;\n if (clientFilters === undefined || clientFilters.length <= 0) {\n // If there are no client filters, then the feature is enabled.\n return true;\n }\n\n const requirementType: RequirementType = featureFlag.conditions?.requirement_type ?? \"Any\"; // default to any.\n\n /**\n * While iterating through the client filters, we short-circuit the evaluation based on the requirement type.\n * - When requirement type is \"All\", the feature is enabled if all client filters are matched. If any client filter is not matched, the feature is disabled, otherwise it is enabled. `shortCircuitEvaluationResult` is false.\n * - When requirement type is \"Any\", the feature is enabled if any client filter is matched. If any client filter is matched, the feature is enabled, otherwise it is disabled. `shortCircuitEvaluationResult` is true.\n */\n const shortCircuitEvaluationResult: boolean = requirementType === \"Any\";\n\n for (const clientFilter of clientFilters) {\n const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);\n const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters };\n if (matchedFeatureFilter === undefined) {\n console.warn(`Feature filter ${clientFilter.name} is not found.`);\n return false;\n }\n if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) {\n return shortCircuitEvaluationResult;\n }\n }\n\n // If we get here, then we have not found a client filter that matches the requirement type.\n return !shortCircuitEvaluationResult;\n }\n\n async #evaluateFeature(featureName: string, context: unknown): Promise<EvaluationResult> {\n const featureFlag = await this.#provider.getFeatureFlag(featureName);\n const result = new EvaluationResult(featureFlag);\n\n if (featureFlag === undefined) {\n return result;\n }\n\n // 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.\n // TODO: move to the feature flag provider implementation.\n validateFeatureFlagFormat(featureFlag);\n\n // Evaluate if the feature is enabled.\n result.enabled = await this.#isEnabled(featureFlag, context);\n\n const targetingContext = context as ITargetingContext;\n result.targetingId = targetingContext?.userId;\n\n // Determine Variant\n let variantDef: VariantDefinition | undefined;\n let reason: VariantAssignmentReason = VariantAssignmentReason.None;\n\n // featureFlag.variant not empty\n if (featureFlag.variants !== undefined && featureFlag.variants.length > 0) {\n if (!result.enabled) {\n // not enabled, assign default if specified\n if (featureFlag.allocation?.default_when_disabled !== undefined) {\n variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_disabled);\n reason = VariantAssignmentReason.DefaultWhenDisabled;\n } else {\n // no default specified\n variantDef = undefined;\n reason = VariantAssignmentReason.DefaultWhenDisabled;\n }\n } else {\n // enabled, assign based on allocation\n if (context !== undefined && featureFlag.allocation !== undefined) {\n const variantAndReason = await this.#assignVariant(featureFlag, targetingContext);\n variantDef = variantAndReason.variant;\n reason = variantAndReason.reason;\n }\n\n // allocation failed, assign default if specified\n if (variantDef === undefined && reason === VariantAssignmentReason.None) {\n if (featureFlag.allocation?.default_when_enabled !== undefined) {\n variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_enabled);\n reason = VariantAssignmentReason.DefaultWhenEnabled;\n } else {\n variantDef = undefined;\n reason = VariantAssignmentReason.DefaultWhenEnabled;\n }\n }\n }\n }\n\n result.variant = variantDef !== undefined ? new Variant(variantDef.name, variantDef.configuration_value) : undefined;\n result.variantAssignmentReason = reason;\n\n // Status override for isEnabled\n if (variantDef !== undefined && featureFlag.enabled) {\n if (variantDef.status_override === \"Enabled\") {\n result.enabled = true;\n } else if (variantDef.status_override === \"Disabled\") {\n result.enabled = false;\n }\n }\n\n // The callback will only be executed if telemetry is enabled for the feature flag\n if (featureFlag.telemetry?.enabled && this.#onFeatureEvaluated !== undefined) {\n this.#onFeatureEvaluated(result);\n }\n\n return result;\n }\n}\n\nexport interface FeatureManagerOptions {\n /**\n * The custom filters to be used by the feature manager.\n */\n customFilters?: IFeatureFilter[];\n\n /**\n * The callback function that is called when a feature flag is evaluated.\n * The callback function is called only when telemetry is enabled for the feature flag.\n */\n onFeatureEvaluated?: (event: EvaluationResult) => void;\n}\n\nexport class EvaluationResult {\n constructor(\n // feature flag definition\n public readonly feature: FeatureFlag | undefined,\n\n // enabled state\n public enabled: boolean = false,\n\n // variant assignment\n public targetingId: string | undefined = undefined,\n public variant: Variant | undefined = undefined,\n public variantAssignmentReason: VariantAssignmentReason = VariantAssignmentReason.None\n ) { }\n}\n\nexport enum VariantAssignmentReason {\n /**\n * Variant allocation did not happen. No variant is assigned.\n */\n None = \"None\",\n\n /**\n * The default variant is assigned when a feature flag is disabled.\n */\n DefaultWhenDisabled = \"DefaultWhenDisabled\",\n\n /**\n * The default variant is assigned because of no applicable user/group/percentile allocation when a feature flag is enabled.\n */\n DefaultWhenEnabled = \"DefaultWhenEnabled\",\n\n /**\n * The variant is assigned because of the user allocation when a feature flag is enabled.\n */\n User = \"User\",\n\n /**\n * The variant is assigned because of the group allocation when a feature flag is enabled.\n */\n Group = \"Group\",\n\n /**\n * The variant is assigned because of the percentile allocation when a feature flag is enabled.\n */\n Percentile = \"Percentile\"\n}\n\n/**\n * Validates the format of the feature flag definition.\n *\n * FeatureFlag data objects are from IFeatureFlagProvider, depending on the implementation.\n * Thus the properties are not guaranteed to have the expected types.\n *\n * @param featureFlag The feature flag definition to validate.\n */\nfunction validateFeatureFlagFormat(featureFlag: any): void {\n if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== \"boolean\") {\n throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`);\n }\n // TODO: add more validations.\n // TODO: should be moved to the feature flag provider.\n}\n\n/**\n * Try to get the variant assignment for the given variant name. If the variant is not found, override the reason with VariantAssignmentReason.None.\n *\n * @param featureFlag feature flag definition\n * @param variantName variant name\n * @param reason variant assignment reason\n * @returns variant assignment containing the variant definition and the reason\n */\nfunction getVariantAssignment(featureFlag: FeatureFlag, variantName: string, reason: VariantAssignmentReason): VariantAssignment {\n const variant = featureFlag.variants?.find(v => v.name == variantName);\n if (variant !== undefined) {\n return { variant, reason };\n } else {\n console.warn(`Variant ${variantName} not found for feature ${featureFlag.id}.`);\n return { variant: undefined, reason: VariantAssignmentReason.None };\n }\n}\n\ntype VariantAssignment = {\n variant: VariantDefinition | undefined;\n reason: VariantAssignmentReason;\n};\n"],"names":["TimeWindowFilter","TargetingFilter","isTargetedUser","VariantAssignmentReason","isTargetedGroup","isTargetedPercentile","Variant"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ var model = require('./model.js');
4
+
5
+ // Copyright (c) Microsoft Corporation.
6
+ // Licensed under the MIT license.
7
+ /**
8
+ * A feature flag provider that uses a map-like configuration to provide feature flags.
9
+ */
10
+ class ConfigurationMapFeatureFlagProvider {
11
+ #configuration;
12
+ constructor(configuration) {
13
+ this.#configuration = configuration;
14
+ }
15
+ async getFeatureFlag(featureName) {
16
+ const featureConfig = this.#configuration.get(model.FEATURE_MANAGEMENT_KEY);
17
+ return featureConfig?.[model.FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
18
+ }
19
+ async getFeatureFlags() {
20
+ const featureConfig = this.#configuration.get(model.FEATURE_MANAGEMENT_KEY);
21
+ return featureConfig?.[model.FEATURE_FLAGS_KEY] ?? [];
22
+ }
23
+ }
24
+ /**
25
+ * A feature flag provider that uses an object-like configuration to provide feature flags.
26
+ */
27
+ class ConfigurationObjectFeatureFlagProvider {
28
+ #configuration;
29
+ constructor(configuration) {
30
+ this.#configuration = configuration;
31
+ }
32
+ async getFeatureFlag(featureName) {
33
+ const featureFlags = this.#configuration[model.FEATURE_MANAGEMENT_KEY]?.[model.FEATURE_FLAGS_KEY];
34
+ return featureFlags?.find((feature) => feature.id === featureName);
35
+ }
36
+ async getFeatureFlags() {
37
+ return this.#configuration[model.FEATURE_MANAGEMENT_KEY]?.[model.FEATURE_FLAGS_KEY] ?? [];
38
+ }
39
+ }
40
+
41
+ exports.ConfigurationMapFeatureFlagProvider = ConfigurationMapFeatureFlagProvider;
42
+ exports.ConfigurationObjectFeatureFlagProvider = ConfigurationObjectFeatureFlagProvider;
43
+ //# sourceMappingURL=featureProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureProvider.js","sources":["../../src/featureProvider.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { IGettable } from \"./gettable.js\";\nimport { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from \"./model.js\";\n\nexport interface IFeatureFlagProvider {\n /**\n * Get all feature flags.\n */\n getFeatureFlags(): Promise<FeatureFlag[]>;\n\n /**\n * Get a feature flag by name.\n * @param featureName The name of the feature flag.\n */\n getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;\n}\n\n/**\n * A feature flag provider that uses a map-like configuration to provide feature flags.\n */\nexport class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider {\n #configuration: IGettable;\n\n constructor(configuration: IGettable) {\n this.#configuration = configuration;\n }\n async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {\n const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);\n return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);\n }\n\n async getFeatureFlags(): Promise<FeatureFlag[]> {\n const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);\n return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];\n }\n}\n\n/**\n * A feature flag provider that uses an object-like configuration to provide feature flags.\n */\nexport class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvider {\n #configuration: Record<string, unknown>;\n\n constructor(configuration: Record<string, unknown>) {\n this.#configuration = configuration;\n }\n\n async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {\n const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];\n return featureFlags?.find((feature: FeatureFlag) => feature.id === featureName);\n }\n\n async getFeatureFlags(): Promise<FeatureFlag[]> {\n return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];\n }\n}\n"],"names":["FEATURE_MANAGEMENT_KEY","FEATURE_FLAGS_KEY"],"mappings":";;;;AAAA;AACA;AAkBA;;AAEG;MACU,mCAAmC,CAAA;AAC5C,IAAA,cAAc,CAAY;AAE1B,IAAA,WAAA,CAAY,aAAwB,EAAA;AAChC,QAAA,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;KACvC;IACD,MAAM,cAAc,CAAC,WAAmB,EAAA;QACpC,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAiCA,4BAAsB,CAAC,CAAC;AACtG,QAAA,OAAO,aAAa,GAAGC,uBAAiB,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;KAC5F;AAED,IAAA,MAAM,eAAe,GAAA;QACjB,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAiCD,4BAAsB,CAAC,CAAC;AACtG,QAAA,OAAO,aAAa,GAAGC,uBAAiB,CAAC,IAAI,EAAE,CAAC;KACnD;AACJ,CAAA;AAED;;AAEG;MACU,sCAAsC,CAAA;AAC/C,IAAA,cAAc,CAA0B;AAExC,IAAA,WAAA,CAAY,aAAsC,EAAA;AAC9C,QAAA,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;KACvC;IAED,MAAM,cAAc,CAAC,WAAmB,EAAA;AACpC,QAAA,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAACD,4BAAsB,CAAC,GAAGC,uBAAiB,CAAC,CAAC;AACtF,QAAA,OAAO,YAAY,EAAE,IAAI,CAAC,CAAC,OAAoB,KAAK,OAAO,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;KACnF;AAED,IAAA,MAAM,eAAe,GAAA;AACjB,QAAA,OAAO,IAAI,CAAC,cAAc,CAACD,4BAAsB,CAAC,GAAGC,uBAAiB,CAAC,IAAI,EAAE,CAAC;KACjF;AACJ;;;;;"}
@@ -1,9 +1,12 @@
1
+ 'use strict';
2
+
3
+ var targetingEvaluator = require('../common/targetingEvaluator.js');
4
+
1
5
  // Copyright (c) Microsoft Corporation.
2
6
  // Licensed under the MIT license.
3
- import { createHash } from "crypto";
4
- export class TargetingFilter {
7
+ class TargetingFilter {
5
8
  name = "Microsoft.Targeting";
6
- evaluate(context, appContext) {
9
+ async evaluate(context, appContext) {
7
10
  const { featureName, parameters } = context;
8
11
  TargetingFilter.#validateParameters(parameters);
9
12
  if (appContext === undefined) {
@@ -37,26 +40,16 @@ export class TargetingFilter {
37
40
  parameters.Audience.Groups !== undefined) {
38
41
  for (const group of parameters.Audience.Groups) {
39
42
  if (appContext.groups.includes(group.Name)) {
40
- const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name);
41
- const rolloutPercentage = group.RolloutPercentage;
42
- if (TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {
43
+ const hint = `${featureName}\n${group.Name}`;
44
+ if (await targetingEvaluator.isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) {
43
45
  return true;
44
46
  }
45
47
  }
46
48
  }
47
49
  }
48
50
  // check if the user is being targeted by a default rollout percentage
49
- const defaultContextId = constructAudienceContextId(featureName, appContext?.userId);
50
- return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage);
51
- }
52
- static #isTargeted(audienceContextId, rolloutPercentage) {
53
- if (rolloutPercentage === 100) {
54
- return true;
55
- }
56
- // Cryptographic hashing algorithms ensure adequate entropy across hash values.
57
- const contextMarker = stringToUint32(audienceContextId);
58
- const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
59
- return contextPercentage < rolloutPercentage;
51
+ const hint = featureName;
52
+ return targetingEvaluator.isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
60
53
  }
61
54
  static #validateParameters(parameters) {
62
55
  if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
@@ -72,33 +65,6 @@ export class TargetingFilter {
72
65
  }
73
66
  }
74
67
  }
75
- /**
76
- * Constructs the context id for the audience.
77
- * The context id is used to determine if the user is part of the audience for a feature.
78
- * If groupName is provided, the context id is constructed as follows:
79
- * userId + "\n" + featureName + "\n" + groupName
80
- * Otherwise, the context id is constructed as follows:
81
- * userId + "\n" + featureName
82
- *
83
- * @param featureName name of the feature
84
- * @param userId userId from app context
85
- * @param groupName group name from app context
86
- * @returns a string that represents the context id for the audience
87
- */
88
- function constructAudienceContextId(featureName, userId, groupName) {
89
- let contextId = `${userId ?? ""}\n${featureName}`;
90
- if (groupName !== undefined) {
91
- contextId += `\n${groupName}`;
92
- }
93
- return contextId;
94
- }
95
- function stringToUint32(str) {
96
- // Create a SHA-256 hash of the string
97
- const hash = createHash("sha256").update(str).digest();
98
- // Get the first 4 bytes of the hash
99
- const first4Bytes = hash.subarray(0, 4);
100
- // Convert the 4 bytes to a uint32 with little-endian encoding
101
- const uint32 = first4Bytes.readUInt32LE(0);
102
- return uint32;
103
- }
104
- //# sourceMappingURL=TargetingFilter.js.map
68
+
69
+ exports.TargetingFilter = TargetingFilter;
70
+ //# sourceMappingURL=TargetingFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TargetingFilter.js","sources":["../../../src/filter/TargetingFilter.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { IFeatureFilter } from \"./FeatureFilter.js\";\nimport { isTargetedPercentile } from \"../common/targetingEvaluator.js\";\nimport { ITargetingContext } from \"../common/ITargetingContext.js\";\n\ntype TargetingFilterParameters = {\n Audience: {\n DefaultRolloutPercentage: number;\n Users?: string[];\n Groups?: {\n Name: string;\n RolloutPercentage: number;\n }[];\n Exclusion?: {\n Users?: string[];\n Groups?: string[];\n };\n }\n}\n\ntype TargetingFilterEvaluationContext = {\n featureName: string;\n parameters: TargetingFilterParameters;\n}\n\nexport class TargetingFilter implements IFeatureFilter {\n name: string = \"Microsoft.Targeting\";\n\n async evaluate(context: TargetingFilterEvaluationContext, appContext?: ITargetingContext): Promise<boolean> {\n const { featureName, parameters } = context;\n TargetingFilter.#validateParameters(parameters);\n\n if (appContext === undefined) {\n throw new Error(\"The app context is required for targeting filter.\");\n }\n\n if (parameters.Audience.Exclusion !== undefined) {\n // check if the user is in the exclusion list\n if (appContext?.userId !== undefined &&\n parameters.Audience.Exclusion.Users !== undefined &&\n parameters.Audience.Exclusion.Users.includes(appContext.userId)) {\n return false;\n }\n // check if the user is in a group within exclusion list\n if (appContext?.groups !== undefined &&\n parameters.Audience.Exclusion.Groups !== undefined) {\n for (const excludedGroup of parameters.Audience.Exclusion.Groups) {\n if (appContext.groups.includes(excludedGroup)) {\n return false;\n }\n }\n }\n }\n\n // check if the user is being targeted directly\n if (appContext?.userId !== undefined &&\n parameters.Audience.Users !== undefined &&\n parameters.Audience.Users.includes(appContext.userId)) {\n return true;\n }\n\n // check if the user is in a group that is being targeted\n if (appContext?.groups !== undefined &&\n parameters.Audience.Groups !== undefined) {\n for (const group of parameters.Audience.Groups) {\n if (appContext.groups.includes(group.Name)) {\n const hint = `${featureName}\\n${group.Name}`;\n if (await isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) {\n return true;\n }\n }\n }\n }\n\n // check if the user is being targeted by a default rollout percentage\n const hint = featureName;\n return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);\n }\n\n static #validateParameters(parameters: TargetingFilterParameters): void {\n if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {\n throw new Error(\"Audience.DefaultRolloutPercentage must be a number between 0 and 100.\");\n }\n // validate RolloutPercentage for each group\n if (parameters.Audience.Groups !== undefined) {\n for (const group of parameters.Audience.Groups) {\n if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {\n throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);\n }\n }\n }\n }\n}\n"],"names":["isTargetedPercentile"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ // Copyright (c) Microsoft Corporation.
4
+ // Licensed under the MIT license.
5
+ class TimeWindowFilter {
6
+ name = "Microsoft.TimeWindow";
7
+ evaluate(context) {
8
+ const { featureName, parameters } = context;
9
+ const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;
10
+ const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;
11
+ if (startTime === undefined && endTime === undefined) {
12
+ // If neither start nor end time is specified, then the filter is not applicable.
13
+ console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);
14
+ return false;
15
+ }
16
+ const now = new Date();
17
+ return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);
18
+ }
19
+ }
20
+
21
+ exports.TimeWindowFilter = TimeWindowFilter;
22
+ //# sourceMappingURL=TimeWindowFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimeWindowFilter.js","sources":["../../../src/filter/TimeWindowFilter.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { IFeatureFilter } from \"./FeatureFilter.js\";\n\n// [Start, End)\ntype TimeWindowParameters = {\n Start?: string;\n End?: string;\n}\n\ntype TimeWindowFilterEvaluationContext = {\n featureName: string;\n parameters: TimeWindowParameters;\n}\n\nexport class TimeWindowFilter implements IFeatureFilter {\n name: string = \"Microsoft.TimeWindow\";\n\n evaluate(context: TimeWindowFilterEvaluationContext): boolean {\n const {featureName, parameters} = context;\n const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;\n const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;\n\n if (startTime === undefined && endTime === undefined) {\n // If neither start nor end time is specified, then the filter is not applicable.\n console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);\n return false;\n }\n const now = new Date();\n return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);\n }\n}\n"],"names":[],"mappings":";;AAAA;AACA;MAea,gBAAgB,CAAA;IACzB,IAAI,GAAW,sBAAsB,CAAC;AAEtC,IAAA,QAAQ,CAAC,OAA0C,EAAA;AAC/C,QAAA,MAAM,EAAC,WAAW,EAAE,UAAU,EAAC,GAAG,OAAO,CAAC;QAC1C,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,KAAK,SAAS,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC;QAC1F,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,KAAK,SAAS,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;QAEpF,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,EAAE;;YAElD,OAAO,CAAC,IAAI,CAAC,CAAO,IAAA,EAAA,IAAI,CAAC,IAAI,CAA4C,yCAAA,EAAA,WAAW,CAAmD,iDAAA,CAAA,CAAC,CAAC;AACzI,YAAA,OAAO,KAAK,CAAC;SAChB;AACD,QAAA,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;AACvB,QAAA,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,SAAS,IAAI,GAAG,MAAM,OAAO,KAAK,SAAS,IAAI,GAAG,GAAG,OAAO,CAAC,CAAC;KACpG;AACJ;;;;"}
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ var featureManager = require('./featureManager.js');
4
+ var featureProvider = require('./featureProvider.js');
5
+ var version = require('./version.js');
6
+
7
+
8
+
9
+ exports.EvaluationResult = featureManager.EvaluationResult;
10
+ exports.FeatureManager = featureManager.FeatureManager;
11
+ Object.defineProperty(exports, "VariantAssignmentReason", {
12
+ enumerable: true,
13
+ get: function () { return featureManager.VariantAssignmentReason; }
14
+ });
15
+ exports.ConfigurationMapFeatureFlagProvider = featureProvider.ConfigurationMapFeatureFlagProvider;
16
+ exports.ConfigurationObjectFeatureFlagProvider = featureProvider.ConfigurationObjectFeatureFlagProvider;
17
+ exports.VERSION = version.VERSION;
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}