@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.
Files changed (35) hide show
  1. package/dist/commonjs/common/targetingEvaluator.js +119 -0
  2. package/dist/commonjs/common/targetingEvaluator.js.map +1 -0
  3. package/dist/commonjs/featureManager.js +183 -7
  4. package/dist/commonjs/featureManager.js.map +1 -1
  5. package/dist/commonjs/featureProvider.js +2 -2
  6. package/dist/commonjs/featureProvider.js.map +1 -1
  7. package/dist/commonjs/filter/TargetingFilter.js +6 -74
  8. package/dist/commonjs/filter/TargetingFilter.js.map +1 -1
  9. package/dist/commonjs/filter/TimeWindowFilter.js.map +1 -1
  10. package/dist/commonjs/index.js +5 -0
  11. package/dist/commonjs/index.js.map +1 -1
  12. package/dist/commonjs/model.js.map +1 -1
  13. package/dist/commonjs/variant/Variant.js +15 -0
  14. package/dist/commonjs/variant/Variant.js.map +1 -0
  15. package/dist/commonjs/version.js +1 -1
  16. package/dist/commonjs/version.js.map +1 -1
  17. package/dist/esm/common/targetingEvaluator.js +115 -0
  18. package/dist/esm/common/targetingEvaluator.js.map +1 -0
  19. package/dist/esm/featureManager.js +183 -8
  20. package/dist/esm/featureManager.js.map +1 -1
  21. package/dist/esm/featureProvider.js +2 -2
  22. package/dist/esm/featureProvider.js.map +1 -1
  23. package/dist/esm/filter/TargetingFilter.js +6 -74
  24. package/dist/esm/filter/TargetingFilter.js.map +1 -1
  25. package/dist/esm/filter/TimeWindowFilter.js.map +1 -1
  26. package/dist/esm/index.js +1 -1
  27. package/dist/esm/model.js.map +1 -1
  28. package/dist/esm/variant/Variant.js +13 -0
  29. package/dist/esm/variant/Variant.js.map +1 -0
  30. package/dist/esm/version.js +1 -1
  31. package/dist/esm/version.js.map +1 -1
  32. package/dist/umd/index.js +310 -82
  33. package/dist/umd/index.js.map +1 -1
  34. package/package.json +1 -1
  35. package/types/index.d.ts +82 -13
@@ -6,7 +6,12 @@ var version = require('./version.js');
6
6
 
7
7
 
8
8
 
9
+ exports.EvaluationResult = featureManager.EvaluationResult;
9
10
  exports.FeatureManager = featureManager.FeatureManager;
11
+ Object.defineProperty(exports, "VariantAssignmentReason", {
12
+ enumerable: true,
13
+ get: function () { return featureManager.VariantAssignmentReason; }
14
+ });
10
15
  exports.ConfigurationMapFeatureFlagProvider = featureProvider.ConfigurationMapFeatureFlagProvider;
11
16
  exports.ConfigurationObjectFeatureFlagProvider = featureProvider.ConfigurationObjectFeatureFlagProvider;
12
17
  exports.VERSION = version.VERSION;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"model.js","sources":["../../src/model.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\n// Converted from:\r\n// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json\r\n\r\n/**\r\n * A feature flag is a named property that can be toggled to enable or disable some feature of an application.\r\n */\r\nexport interface FeatureFlag {\r\n /**\r\n * An ID used to uniquely identify and reference the feature.\r\n */\r\n id: string;\r\n /**\r\n * A description of the feature.\r\n */\r\n description?: string;\r\n /**\r\n * A display name for the feature to use for display rather than the ID.\r\n */\r\n display_name?: string;\r\n /**\r\n * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied.\r\n */\r\n enabled?: boolean;\r\n /**\r\n * The declaration of conditions used to dynamically enable the feature.\r\n */\r\n conditions?: FeatureEnablementConditions;\r\n /**\r\n * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object.\r\n */\r\n variants?: Variant[];\r\n /**\r\n * Determines how variants should be allocated for the feature to various users.\r\n */\r\n allocation?: VariantAllocation;\r\n /**\r\n * The declaration of options used to configure telemetry for this feature.\r\n */\r\n telemetry?: TelemetryOptions\r\n}\r\n\r\n/**\r\n* The declaration of conditions used to dynamically enable the feature\r\n*/\r\ninterface FeatureEnablementConditions {\r\n /**\r\n * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled.\r\n */\r\n requirement_type?: RequirementType;\r\n /**\r\n * Filters that must run on the client and be evaluated as true for the feature to be considered enabled.\r\n */\r\n client_filters?: ClientFilter[];\r\n}\r\n\r\nexport type RequirementType = \"Any\" | \"All\";\r\n\r\ninterface ClientFilter {\r\n /**\r\n * The name used to refer to a client filter.\r\n */\r\n name: string;\r\n /**\r\n * Parameters for a given client filter. A client filter can require any set of parameters of any type.\r\n */\r\n parameters?: Record<string, unknown>;\r\n}\r\n\r\ninterface Variant {\r\n /**\r\n * The name used to refer to a feature variant.\r\n */\r\n name: string;\r\n /**\r\n * The configuration value for this feature variant.\r\n */\r\n configuration_value?: unknown;\r\n /**\r\n * The path to a configuration section used as the configuration value for this feature variant.\r\n */\r\n configuration_reference?: string;\r\n /**\r\n * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None.\r\n */\r\n status_override?: \"None\" | \"Enabled\" | \"Disabled\";\r\n}\r\n\r\n/**\r\n* Determines how variants should be allocated for the feature to various users.\r\n*/\r\ninterface VariantAllocation {\r\n /**\r\n * Specifies which variant should be used when the feature is considered disabled.\r\n */\r\n default_when_disabled?: string;\r\n /**\r\n * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable.\r\n */\r\n default_when_enabled?: string;\r\n /**\r\n * A list of objects, each containing a variant name and list of users for whom that variant should be used.\r\n */\r\n user?: UserAllocation[];\r\n /**\r\n * A list of objects, each containing a variant name and list of groups for which that variant should be used.\r\n */\r\n group?: GroupAllocation[];\r\n /**\r\n * A list of objects, each containing a variant name and percentage range for which that variant should be used.\r\n */\r\n percentile?: PercentileAllocation[]\r\n /**\r\n * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used.\r\n */\r\n seed?: string;\r\n}\r\n\r\ninterface UserAllocation {\r\n /**\r\n * The name of the variant to use if the user allocation matches the current user.\r\n */\r\n variant: string;\r\n /**\r\n * Collection of users where if any match the current user, the variant specified in the user allocation is used.\r\n */\r\n users: string[];\r\n}\r\n\r\ninterface GroupAllocation {\r\n /**\r\n * The name of the variant to use if the group allocation matches a group the current user is in.\r\n */\r\n variant: string;\r\n /**\r\n * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used.\r\n */\r\n groups: string[];\r\n}\r\n\r\ninterface PercentileAllocation {\r\n /**\r\n * The name of the variant to use if the calculated percentile for the current user falls in the provided range.\r\n */\r\n variant: string;\r\n /**\r\n * The lower end of the percentage range for which this variant will be used.\r\n */\r\n from: number;\r\n /**\r\n * The upper end of the percentage range for which this variant will be used.\r\n */\r\n to: number;\r\n}\r\n\r\n/**\r\n* The declaration of options used to configure telemetry for this feature.\r\n*/\r\ninterface TelemetryOptions {\r\n /**\r\n * Indicates if telemetry is enabled.\r\n */\r\n enabled?: boolean;\r\n /**\r\n * A container for metadata that should be bundled with flag telemetry.\r\n */\r\n metadata?: Record<string, string>;\r\n}\r\n\r\n// Feature Management Section fed into feature manager.\r\n// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json\r\n\r\nexport const FEATURE_MANAGEMENT_KEY = \"feature_management\";\r\nexport const FEATURE_FLAGS_KEY = \"feature_flags\";\r\n\r\nexport interface FeatureManagementConfiguration {\r\n feature_management: FeatureManagement\r\n}\r\n\r\n/**\r\n * Declares feature management configuration.\r\n */\r\nexport interface FeatureManagement {\r\n feature_flags: FeatureFlag[];\r\n}\r\n"],"names":[],"mappings":";;AAAA;AACA;AA0KA;AACA;AAEO,MAAM,sBAAsB,GAAG,qBAAqB;AACpD,MAAM,iBAAiB,GAAG;;;;;"}
1
+ {"version":3,"file":"model.js","sources":["../../src/model.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\n// Converted from:\n// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json\n\n/**\n * A feature flag is a named property that can be toggled to enable or disable some feature of an application.\n */\nexport interface FeatureFlag {\n /**\n * An ID used to uniquely identify and reference the feature.\n */\n id: string;\n /**\n * A description of the feature.\n */\n description?: string;\n /**\n * A display name for the feature to use for display rather than the ID.\n */\n display_name?: string;\n /**\n * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied.\n */\n enabled?: boolean;\n /**\n * The declaration of conditions used to dynamically enable the feature.\n */\n conditions?: FeatureEnablementConditions;\n /**\n * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object.\n */\n variants?: VariantDefinition[];\n /**\n * Determines how variants should be allocated for the feature to various users.\n */\n allocation?: VariantAllocation;\n /**\n * The declaration of options used to configure telemetry for this feature.\n */\n telemetry?: TelemetryOptions\n}\n\n/**\n* The declaration of conditions used to dynamically enable the feature\n*/\ninterface FeatureEnablementConditions {\n /**\n * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled.\n */\n requirement_type?: RequirementType;\n /**\n * Filters that must run on the client and be evaluated as true for the feature to be considered enabled.\n */\n client_filters?: ClientFilter[];\n}\n\nexport type RequirementType = \"Any\" | \"All\";\n\ninterface ClientFilter {\n /**\n * The name used to refer to a client filter.\n */\n name: string;\n /**\n * Parameters for a given client filter. A client filter can require any set of parameters of any type.\n */\n parameters?: Record<string, unknown>;\n}\n\nexport interface VariantDefinition {\n /**\n * The name used to refer to a feature variant.\n */\n name: string;\n /**\n * The configuration value for this feature variant.\n */\n configuration_value?: unknown;\n /**\n * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None.\n */\n status_override?: \"None\" | \"Enabled\" | \"Disabled\";\n}\n\n/**\n* Determines how variants should be allocated for the feature to various users.\n*/\ninterface VariantAllocation {\n /**\n * Specifies which variant should be used when the feature is considered disabled.\n */\n default_when_disabled?: string;\n /**\n * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable.\n */\n default_when_enabled?: string;\n /**\n * A list of objects, each containing a variant name and list of users for whom that variant should be used.\n */\n user?: UserAllocation[];\n /**\n * A list of objects, each containing a variant name and list of groups for which that variant should be used.\n */\n group?: GroupAllocation[];\n /**\n * A list of objects, each containing a variant name and percentage range for which that variant should be used.\n */\n percentile?: PercentileAllocation[]\n /**\n * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used.\n */\n seed?: string;\n}\n\ninterface UserAllocation {\n /**\n * The name of the variant to use if the user allocation matches the current user.\n */\n variant: string;\n /**\n * Collection of users where if any match the current user, the variant specified in the user allocation is used.\n */\n users: string[];\n}\n\ninterface GroupAllocation {\n /**\n * The name of the variant to use if the group allocation matches a group the current user is in.\n */\n variant: string;\n /**\n * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used.\n */\n groups: string[];\n}\n\ninterface PercentileAllocation {\n /**\n * The name of the variant to use if the calculated percentile for the current user falls in the provided range.\n */\n variant: string;\n /**\n * The lower end of the percentage range for which this variant will be used.\n */\n from: number;\n /**\n * The upper end of the percentage range for which this variant will be used.\n */\n to: number;\n}\n\n/**\n* The declaration of options used to configure telemetry for this feature.\n*/\ninterface TelemetryOptions {\n /**\n * Indicates if telemetry is enabled.\n */\n enabled?: boolean;\n /**\n * A container for metadata that should be bundled with flag telemetry.\n */\n metadata?: Record<string, string>;\n}\n\n// Feature Management Section fed into feature manager.\n// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json\n\nexport const FEATURE_MANAGEMENT_KEY = \"feature_management\";\nexport const FEATURE_FLAGS_KEY = \"feature_flags\";\n\nexport interface FeatureManagementConfiguration {\n feature_management: FeatureManagement\n}\n\n/**\n * Declares feature management configuration.\n */\nexport interface FeatureManagement {\n feature_flags: FeatureFlag[];\n}\n"],"names":[],"mappings":";;AAAA;AACA;AAsKA;AACA;AAEO,MAAM,sBAAsB,GAAG,qBAAqB;AACpD,MAAM,iBAAiB,GAAG;;;;;"}
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ // Copyright (c) Microsoft Corporation.
4
+ // Licensed under the MIT license.
5
+ class Variant {
6
+ name;
7
+ configuration;
8
+ constructor(name, configuration) {
9
+ this.name = name;
10
+ this.configuration = configuration;
11
+ }
12
+ }
13
+
14
+ exports.Variant = Variant;
15
+ //# sourceMappingURL=Variant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Variant.js","sources":["../../../src/variant/Variant.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nexport class Variant {\n constructor(\n public name: string,\n public configuration: unknown\n ) {}\n}\n"],"names":[],"mappings":";;AAAA;AACA;MAEa,OAAO,CAAA;AAEL,IAAA,IAAA,CAAA;AACA,IAAA,aAAA,CAAA;IAFX,WACW,CAAA,IAAY,EACZ,aAAsB,EAAA;QADtB,IAAI,CAAA,IAAA,GAAJ,IAAI,CAAQ;QACZ,IAAa,CAAA,aAAA,GAAb,aAAa,CAAS;KAC7B;AACP;;;;"}
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Copyright (c) Microsoft Corporation.
4
4
  // Licensed under the MIT license.
5
- const VERSION = "1.0.0";
5
+ const VERSION = "2.0.0-preview.2";
6
6
 
7
7
  exports.VERSION = VERSION;
8
8
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sources":["../../src/version.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nexport const VERSION = \"1.0.0\";\n"],"names":[],"mappings":";;AAAA;AACA;AAEO,MAAM,OAAO,GAAG;;;;"}
1
+ {"version":3,"file":"version.js","sources":["../../src/version.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nexport const VERSION = \"2.0.0-preview.2\";\n"],"names":[],"mappings":";;AAAA;AACA;AAEO,MAAM,OAAO,GAAG;;;;"}
@@ -0,0 +1,115 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ /**
4
+ * Determines if the user is part of the audience, based on the user id and the percentage range.
5
+ *
6
+ * @param userId user id from app context
7
+ * @param hint hint string to be included in the context id
8
+ * @param from percentage range start
9
+ * @param to percentage range end
10
+ * @returns true if the user is part of the audience, false otherwise
11
+ */
12
+ async function isTargetedPercentile(userId, hint, from, to) {
13
+ if (from < 0 || from > 100) {
14
+ throw new Error("The 'from' value must be between 0 and 100.");
15
+ }
16
+ if (to < 0 || to > 100) {
17
+ throw new Error("The 'to' value must be between 0 and 100.");
18
+ }
19
+ if (from > to) {
20
+ throw new Error("The 'from' value cannot be larger than the 'to' value.");
21
+ }
22
+ const audienceContextId = constructAudienceContextId(userId, hint);
23
+ // Cryptographic hashing algorithms ensure adequate entropy across hash values.
24
+ const contextMarker = await stringToUint32(audienceContextId);
25
+ const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
26
+ // Handle edge case of exact 100 bucket
27
+ if (to === 100) {
28
+ return contextPercentage >= from;
29
+ }
30
+ return contextPercentage >= from && contextPercentage < to;
31
+ }
32
+ /**
33
+ * Determines if the user is part of the audience, based on the groups they belong to.
34
+ *
35
+ * @param sourceGroups user groups from app context
36
+ * @param targetedGroups targeted groups from feature configuration
37
+ * @returns true if the user is part of the audience, false otherwise
38
+ */
39
+ function isTargetedGroup(sourceGroups, targetedGroups) {
40
+ if (sourceGroups === undefined) {
41
+ return false;
42
+ }
43
+ return sourceGroups.some(group => targetedGroups.includes(group));
44
+ }
45
+ /**
46
+ * Determines if the user is part of the audience, based on the user id.
47
+ * @param userId user id from app context
48
+ * @param users targeted users from feature configuration
49
+ * @returns true if the user is part of the audience, false otherwise
50
+ */
51
+ function isTargetedUser(userId, users) {
52
+ if (userId === undefined) {
53
+ return false;
54
+ }
55
+ return users.includes(userId);
56
+ }
57
+ /**
58
+ * Constructs the context id for the audience.
59
+ * The context id is used to determine if the user is part of the audience for a feature.
60
+ *
61
+ * @param userId userId from app context
62
+ * @param hint hint string to be included in the context id
63
+ * @returns a string that represents the context id for the audience
64
+ */
65
+ function constructAudienceContextId(userId, hint) {
66
+ return `${userId ?? ""}\n${hint}`;
67
+ }
68
+ /**
69
+ * Converts a string to a uint32 in little-endian encoding.
70
+ * @param str the string to convert.
71
+ * @returns a uint32 value.
72
+ */
73
+ async function stringToUint32(str) {
74
+ let crypto;
75
+ // Check for browser environment
76
+ if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
77
+ crypto = window.crypto;
78
+ }
79
+ // Check for Node.js environment
80
+ else if (typeof global !== "undefined" && global.crypto) {
81
+ crypto = global.crypto;
82
+ }
83
+ // Fallback to native Node.js crypto module
84
+ else {
85
+ try {
86
+ if (typeof module !== "undefined" && module.exports) {
87
+ crypto = require("crypto");
88
+ }
89
+ else {
90
+ crypto = await import('crypto');
91
+ }
92
+ }
93
+ catch (error) {
94
+ console.error("Failed to load the crypto module:", error.message);
95
+ throw error;
96
+ }
97
+ }
98
+ // In the browser, use crypto.subtle.digest
99
+ if (crypto.subtle) {
100
+ const data = new TextEncoder().encode(str);
101
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
102
+ const dataView = new DataView(hashBuffer);
103
+ const uint32 = dataView.getUint32(0, true);
104
+ return uint32;
105
+ }
106
+ // In Node.js, use the crypto module's hash function
107
+ else {
108
+ const hash = crypto.createHash("sha256").update(str).digest();
109
+ const uint32 = hash.readUInt32LE(0);
110
+ return uint32;
111
+ }
112
+ }
113
+
114
+ export { isTargetedGroup, isTargetedPercentile, isTargetedUser };
115
+ //# 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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"}
@@ -1,11 +1,14 @@
1
1
  import { TimeWindowFilter } from './filter/TimeWindowFilter.js';
2
2
  import { TargetingFilter } from './filter/TargetingFilter.js';
3
+ import { Variant } from './variant/Variant.js';
4
+ import { isTargetedUser, isTargetedGroup, isTargetedPercentile } from './common/targetingEvaluator.js';
3
5
 
4
6
  // Copyright (c) Microsoft Corporation.
5
7
  // Licensed under the MIT license.
6
8
  class FeatureManager {
7
9
  #provider;
8
10
  #featureFilters = new Map();
11
+ #onFeatureEvaluated;
9
12
  constructor(provider, options) {
10
13
  this.#provider = provider;
11
14
  const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];
@@ -13,6 +16,7 @@ class FeatureManager {
13
16
  for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
14
17
  this.#featureFilters.set(filter.name, filter);
15
18
  }
19
+ this.#onFeatureEvaluated = options?.onFeatureEvaluated;
16
20
  }
17
21
  async listFeatureNames() {
18
22
  const features = await this.#provider.getFeatureFlags();
@@ -21,13 +25,42 @@ class FeatureManager {
21
25
  }
22
26
  // If multiple feature flags are found, the first one takes precedence.
23
27
  async isEnabled(featureName, context) {
24
- const featureFlag = await this.#provider.getFeatureFlag(featureName);
25
- if (featureFlag === undefined) {
26
- // If the feature is not found, then it is disabled.
27
- return false;
28
+ const result = await this.#evaluateFeature(featureName, context);
29
+ return result.enabled;
30
+ }
31
+ async getVariant(featureName, context) {
32
+ const result = await this.#evaluateFeature(featureName, context);
33
+ return result.variant;
34
+ }
35
+ async #assignVariant(featureFlag, context) {
36
+ // user allocation
37
+ if (featureFlag.allocation?.user !== undefined) {
38
+ for (const userAllocation of featureFlag.allocation.user) {
39
+ if (isTargetedUser(context.userId, userAllocation.users)) {
40
+ return getVariantAssignment(featureFlag, userAllocation.variant, VariantAssignmentReason.User);
41
+ }
42
+ }
28
43
  }
29
- // 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.
30
- validateFeatureFlagFormat(featureFlag);
44
+ // group allocation
45
+ if (featureFlag.allocation?.group !== undefined) {
46
+ for (const groupAllocation of featureFlag.allocation.group) {
47
+ if (isTargetedGroup(context.groups, groupAllocation.groups)) {
48
+ return getVariantAssignment(featureFlag, groupAllocation.variant, VariantAssignmentReason.Group);
49
+ }
50
+ }
51
+ }
52
+ // percentile allocation
53
+ if (featureFlag.allocation?.percentile !== undefined) {
54
+ for (const percentileAllocation of featureFlag.allocation.percentile) {
55
+ const hint = featureFlag.allocation.seed ?? `allocation\n${featureFlag.id}`;
56
+ if (await isTargetedPercentile(context.userId, hint, percentileAllocation.from, percentileAllocation.to)) {
57
+ return getVariantAssignment(featureFlag, percentileAllocation.variant, VariantAssignmentReason.Percentile);
58
+ }
59
+ }
60
+ }
61
+ return { variant: undefined, reason: VariantAssignmentReason.None };
62
+ }
63
+ async #isEnabled(featureFlag, context) {
31
64
  if (featureFlag.enabled !== true) {
32
65
  // If the feature is not explicitly enabled, then it is disabled by default.
33
66
  return false;
@@ -46,7 +79,7 @@ class FeatureManager {
46
79
  const shortCircuitEvaluationResult = requirementType === "Any";
47
80
  for (const clientFilter of clientFilters) {
48
81
  const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
49
- const contextWithFeatureName = { featureName, parameters: clientFilter.parameters };
82
+ const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters };
50
83
  if (matchedFeatureFilter === undefined) {
51
84
  console.warn(`Feature filter ${clientFilter.name} is not found.`);
52
85
  return false;
@@ -58,12 +91,154 @@ class FeatureManager {
58
91
  // If we get here, then we have not found a client filter that matches the requirement type.
59
92
  return !shortCircuitEvaluationResult;
60
93
  }
94
+ async #evaluateFeature(featureName, context) {
95
+ const featureFlag = await this.#provider.getFeatureFlag(featureName);
96
+ const result = new EvaluationResult(featureFlag);
97
+ if (featureFlag === undefined) {
98
+ return result;
99
+ }
100
+ // 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.
101
+ // TODO: move to the feature flag provider implementation.
102
+ validateFeatureFlagFormat(featureFlag);
103
+ // Evaluate if the feature is enabled.
104
+ result.enabled = await this.#isEnabled(featureFlag, context);
105
+ const targetingContext = context;
106
+ result.targetingId = targetingContext?.userId;
107
+ // Determine Variant
108
+ let variantDef;
109
+ let reason = VariantAssignmentReason.None;
110
+ // featureFlag.variant not empty
111
+ if (featureFlag.variants !== undefined && featureFlag.variants.length > 0) {
112
+ if (!result.enabled) {
113
+ // not enabled, assign default if specified
114
+ if (featureFlag.allocation?.default_when_disabled !== undefined) {
115
+ variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_disabled);
116
+ reason = VariantAssignmentReason.DefaultWhenDisabled;
117
+ }
118
+ else {
119
+ // no default specified
120
+ variantDef = undefined;
121
+ reason = VariantAssignmentReason.DefaultWhenDisabled;
122
+ }
123
+ }
124
+ else {
125
+ // enabled, assign based on allocation
126
+ if (context !== undefined && featureFlag.allocation !== undefined) {
127
+ const variantAndReason = await this.#assignVariant(featureFlag, targetingContext);
128
+ variantDef = variantAndReason.variant;
129
+ reason = variantAndReason.reason;
130
+ }
131
+ // allocation failed, assign default if specified
132
+ if (variantDef === undefined && reason === VariantAssignmentReason.None) {
133
+ if (featureFlag.allocation?.default_when_enabled !== undefined) {
134
+ variantDef = featureFlag.variants.find(v => v.name == featureFlag.allocation?.default_when_enabled);
135
+ reason = VariantAssignmentReason.DefaultWhenEnabled;
136
+ }
137
+ else {
138
+ variantDef = undefined;
139
+ reason = VariantAssignmentReason.DefaultWhenEnabled;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ result.variant = variantDef !== undefined ? new Variant(variantDef.name, variantDef.configuration_value) : undefined;
145
+ result.variantAssignmentReason = reason;
146
+ // Status override for isEnabled
147
+ if (variantDef !== undefined && featureFlag.enabled) {
148
+ if (variantDef.status_override === "Enabled") {
149
+ result.enabled = true;
150
+ }
151
+ else if (variantDef.status_override === "Disabled") {
152
+ result.enabled = false;
153
+ }
154
+ }
155
+ // The callback will only be executed if telemetry is enabled for the feature flag
156
+ if (featureFlag.telemetry?.enabled && this.#onFeatureEvaluated !== undefined) {
157
+ this.#onFeatureEvaluated(result);
158
+ }
159
+ return result;
160
+ }
161
+ }
162
+ class EvaluationResult {
163
+ feature;
164
+ enabled;
165
+ targetingId;
166
+ variant;
167
+ variantAssignmentReason;
168
+ constructor(
169
+ // feature flag definition
170
+ feature,
171
+ // enabled state
172
+ enabled = false,
173
+ // variant assignment
174
+ targetingId = undefined, variant = undefined, variantAssignmentReason = VariantAssignmentReason.None) {
175
+ this.feature = feature;
176
+ this.enabled = enabled;
177
+ this.targetingId = targetingId;
178
+ this.variant = variant;
179
+ this.variantAssignmentReason = variantAssignmentReason;
180
+ }
61
181
  }
182
+ var VariantAssignmentReason;
183
+ (function (VariantAssignmentReason) {
184
+ /**
185
+ * Variant allocation did not happen. No variant is assigned.
186
+ */
187
+ VariantAssignmentReason["None"] = "None";
188
+ /**
189
+ * The default variant is assigned when a feature flag is disabled.
190
+ */
191
+ VariantAssignmentReason["DefaultWhenDisabled"] = "DefaultWhenDisabled";
192
+ /**
193
+ * The default variant is assigned because of no applicable user/group/percentile allocation when a feature flag is enabled.
194
+ */
195
+ VariantAssignmentReason["DefaultWhenEnabled"] = "DefaultWhenEnabled";
196
+ /**
197
+ * The variant is assigned because of the user allocation when a feature flag is enabled.
198
+ */
199
+ VariantAssignmentReason["User"] = "User";
200
+ /**
201
+ * The variant is assigned because of the group allocation when a feature flag is enabled.
202
+ */
203
+ VariantAssignmentReason["Group"] = "Group";
204
+ /**
205
+ * The variant is assigned because of the percentile allocation when a feature flag is enabled.
206
+ */
207
+ VariantAssignmentReason["Percentile"] = "Percentile";
208
+ })(VariantAssignmentReason || (VariantAssignmentReason = {}));
209
+ /**
210
+ * Validates the format of the feature flag definition.
211
+ *
212
+ * FeatureFlag data objects are from IFeatureFlagProvider, depending on the implementation.
213
+ * Thus the properties are not guaranteed to have the expected types.
214
+ *
215
+ * @param featureFlag The feature flag definition to validate.
216
+ */
62
217
  function validateFeatureFlagFormat(featureFlag) {
63
218
  if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") {
64
219
  throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`);
65
220
  }
221
+ // TODO: add more validations.
222
+ // TODO: should be moved to the feature flag provider.
223
+ }
224
+ /**
225
+ * Try to get the variant assignment for the given variant name. If the variant is not found, override the reason with VariantAssignmentReason.None.
226
+ *
227
+ * @param featureFlag feature flag definition
228
+ * @param variantName variant name
229
+ * @param reason variant assignment reason
230
+ * @returns variant assignment containing the variant definition and the reason
231
+ */
232
+ function getVariantAssignment(featureFlag, variantName, reason) {
233
+ const variant = featureFlag.variants?.find(v => v.name == variantName);
234
+ if (variant !== undefined) {
235
+ return { variant, reason };
236
+ }
237
+ else {
238
+ console.warn(`Variant ${variantName} not found for feature ${featureFlag.id}.`);
239
+ return { variant: undefined, reason: VariantAssignmentReason.None };
240
+ }
66
241
  }
67
242
 
68
- export { FeatureManager };
243
+ export { EvaluationResult, FeatureManager, VariantAssignmentReason };
69
244
  //# sourceMappingURL=featureManager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"featureManager.js","sources":["../../src/featureManager.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { TimeWindowFilter } from \"./filter/TimeWindowFilter.js\";\r\nimport { IFeatureFilter } from \"./filter/FeatureFilter.js\";\r\nimport { RequirementType } from \"./model.js\";\r\nimport { IFeatureFlagProvider } from \"./featureProvider.js\";\r\nimport { TargetingFilter } from \"./filter/TargetingFilter.js\";\r\n\r\nexport class FeatureManager {\r\n #provider: IFeatureFlagProvider;\r\n #featureFilters: Map<string, IFeatureFilter> = new Map();\r\n\r\n constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) {\r\n this.#provider = provider;\r\n\r\n const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];\r\n\r\n // If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.\r\n for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {\r\n this.#featureFilters.set(filter.name, filter);\r\n }\r\n }\r\n\r\n async listFeatureNames(): Promise<string[]> {\r\n const features = await this.#provider.getFeatureFlags();\r\n const featureNameSet = new Set(features.map((feature) => feature.id));\r\n return Array.from(featureNameSet);\r\n }\r\n\r\n // If multiple feature flags are found, the first one takes precedence.\r\n async isEnabled(featureName: string, context?: unknown): Promise<boolean> {\r\n const featureFlag = await this.#provider.getFeatureFlag(featureName);\r\n if (featureFlag === undefined) {\r\n // If the feature is not found, then it is disabled.\r\n return false;\r\n }\r\n\r\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.\r\n validateFeatureFlagFormat(featureFlag);\r\n\r\n if (featureFlag.enabled !== true) {\r\n // If the feature is not explicitly enabled, then it is disabled by default.\r\n return false;\r\n }\r\n\r\n const clientFilters = featureFlag.conditions?.client_filters;\r\n if (clientFilters === undefined || clientFilters.length <= 0) {\r\n // If there are no client filters, then the feature is enabled.\r\n return true;\r\n }\r\n\r\n const requirementType: RequirementType = featureFlag.conditions?.requirement_type ?? \"Any\"; // default to any.\r\n\r\n /**\r\n * While iterating through the client filters, we short-circuit the evaluation based on the requirement type.\r\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.\r\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.\r\n */\r\n const shortCircuitEvaluationResult: boolean = requirementType === \"Any\";\r\n\r\n for (const clientFilter of clientFilters) {\r\n const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);\r\n const contextWithFeatureName = { featureName, parameters: clientFilter.parameters };\r\n if (matchedFeatureFilter === undefined) {\r\n console.warn(`Feature filter ${clientFilter.name} is not found.`);\r\n return false;\r\n }\r\n if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) {\r\n return shortCircuitEvaluationResult;\r\n }\r\n }\r\n\r\n // If we get here, then we have not found a client filter that matches the requirement type.\r\n return !shortCircuitEvaluationResult;\r\n }\r\n\r\n}\r\n\r\ninterface FeatureManagerOptions {\r\n customFilters?: IFeatureFilter[];\r\n}\r\n\r\nfunction validateFeatureFlagFormat(featureFlag: any): void {\r\n if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== \"boolean\") {\r\n throw new Error(`Feature flag ${featureFlag.id} has an invalid 'enabled' value.`);\r\n }\r\n}\r\n"],"names":[],"mappings":";;;AAAA;AACA;MAQa,cAAc,CAAA;AACvB,IAAA,SAAS,CAAuB;AAChC,IAAA,eAAe,GAAgC,IAAI,GAAG,EAAE,CAAC;IAEzD,WAAY,CAAA,QAA8B,EAAE,OAA+B,EAAA;AACvE,QAAA,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAE1B,MAAM,cAAc,GAAG,CAAC,IAAI,gBAAgB,EAAE,EAAE,IAAI,eAAe,EAAE,CAAC,CAAC;;AAGvE,QAAA,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,cAAc,EAAE,IAAI,OAAO,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC,EAAE;YACzE,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;SACjD;KACJ;AAED,IAAA,MAAM,gBAAgB,GAAA;QAClB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,eAAe,EAAE,CAAC;AACxD,QAAA,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;AACtE,QAAA,OAAO,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;KACrC;;AAGD,IAAA,MAAM,SAAS,CAAC,WAAmB,EAAE,OAAiB,EAAA;QAClD,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;AACrE,QAAA,IAAI,WAAW,KAAK,SAAS,EAAE;;AAE3B,YAAA,OAAO,KAAK,CAAC;SAChB;;QAGD,yBAAyB,CAAC,WAAW,CAAC,CAAC;AAEvC,QAAA,IAAI,WAAW,CAAC,OAAO,KAAK,IAAI,EAAE;;AAE9B,YAAA,OAAO,KAAK,CAAC;SAChB;AAED,QAAA,MAAM,aAAa,GAAG,WAAW,CAAC,UAAU,EAAE,cAAc,CAAC;QAC7D,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,MAAM,IAAI,CAAC,EAAE;;AAE1D,YAAA,OAAO,IAAI,CAAC;SACf;QAED,MAAM,eAAe,GAAoB,WAAW,CAAC,UAAU,EAAE,gBAAgB,IAAI,KAAK,CAAC;AAE3F;;;;AAIG;AACH,QAAA,MAAM,4BAA4B,GAAY,eAAe,KAAK,KAAK,CAAC;AAExE,QAAA,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE;AACtC,YAAA,MAAM,oBAAoB,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YACzE,MAAM,sBAAsB,GAAG,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,CAAC,UAAU,EAAE,CAAC;AACpF,YAAA,IAAI,oBAAoB,KAAK,SAAS,EAAE;gBACpC,OAAO,CAAC,IAAI,CAAC,CAAA,eAAA,EAAkB,YAAY,CAAC,IAAI,CAAgB,cAAA,CAAA,CAAC,CAAC;AAClE,gBAAA,OAAO,KAAK,CAAC;aAChB;AACD,YAAA,IAAI,MAAM,oBAAoB,CAAC,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC,KAAK,4BAA4B,EAAE;AACvG,gBAAA,OAAO,4BAA4B,CAAC;aACvC;SACJ;;QAGD,OAAO,CAAC,4BAA4B,CAAC;KACxC;AAEJ,CAAA;AAMD,SAAS,yBAAyB,CAAC,WAAgB,EAAA;AAC/C,IAAA,IAAI,WAAW,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,WAAW,CAAC,OAAO,KAAK,SAAS,EAAE;QAC/E,MAAM,IAAI,KAAK,CAAC,CAAA,aAAA,EAAgB,WAAW,CAAC,EAAE,CAAkC,gCAAA,CAAA,CAAC,CAAC;KACrF;AACL;;;;"}
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":[],"mappings":"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"}
@@ -12,7 +12,7 @@ class ConfigurationMapFeatureFlagProvider {
12
12
  }
13
13
  async getFeatureFlag(featureName) {
14
14
  const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
15
- return featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName);
15
+ return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
16
16
  }
17
17
  async getFeatureFlags() {
18
18
  const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
@@ -29,7 +29,7 @@ class ConfigurationObjectFeatureFlagProvider {
29
29
  }
30
30
  async getFeatureFlag(featureName) {
31
31
  const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];
32
- return featureFlags?.findLast((feature) => feature.id === featureName);
32
+ return featureFlags?.find((feature) => feature.id === featureName);
33
33
  }
34
34
  async getFeatureFlags() {
35
35
  return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
@@ -1 +1 @@
1
- {"version":3,"file":"featureProvider.js","sources":["../../src/featureProvider.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IGettable } from \"./gettable.js\";\r\nimport { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from \"./model.js\";\r\n\r\nexport interface IFeatureFlagProvider {\r\n /**\r\n * Get all feature flags.\r\n */\r\n getFeatureFlags(): Promise<FeatureFlag[]>;\r\n\r\n /**\r\n * Get a feature flag by name.\r\n * @param featureName The name of the feature flag.\r\n */\r\n getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;\r\n}\r\n\r\n/**\r\n * A feature flag provider that uses a map-like configuration to provide feature flags.\r\n */\r\nexport class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider {\r\n #configuration: IGettable;\r\n\r\n constructor(configuration: IGettable) {\r\n this.#configuration = configuration;\r\n }\r\n async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {\r\n const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);\r\n return featureConfig?.[FEATURE_FLAGS_KEY]?.findLast((feature) => feature.id === featureName);\r\n }\r\n\r\n async getFeatureFlags(): Promise<FeatureFlag[]> {\r\n const featureConfig = this.#configuration.get<FeatureManagementConfiguration>(FEATURE_MANAGEMENT_KEY);\r\n return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];\r\n }\r\n}\r\n\r\n/**\r\n * A feature flag provider that uses an object-like configuration to provide feature flags.\r\n */\r\nexport class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvider {\r\n #configuration: Record<string, unknown>;\r\n\r\n constructor(configuration: Record<string, unknown>) {\r\n this.#configuration = configuration;\r\n }\r\n\r\n async getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined> {\r\n const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];\r\n return featureFlags?.findLast((feature: FeatureFlag) => feature.id === featureName);\r\n }\r\n\r\n async getFeatureFlags(): Promise<FeatureFlag[]> {\r\n return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];\r\n }\r\n}\r\n"],"names":[],"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,CAAiC,sBAAsB,CAAC,CAAC;AACtG,QAAA,OAAO,aAAa,GAAG,iBAAiB,CAAC,EAAE,QAAQ,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;KAChG;AAED,IAAA,MAAM,eAAe,GAAA;QACjB,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAiC,sBAAsB,CAAC,CAAC;AACtG,QAAA,OAAO,aAAa,GAAG,iBAAiB,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,CAAC,sBAAsB,CAAC,GAAG,iBAAiB,CAAC,CAAC;AACtF,QAAA,OAAO,YAAY,EAAE,QAAQ,CAAC,CAAC,OAAoB,KAAK,OAAO,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;KACvF;AAED,IAAA,MAAM,eAAe,GAAA;AACjB,QAAA,OAAO,IAAI,CAAC,cAAc,CAAC,sBAAsB,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC;KACjF;AACJ;;;;"}
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":[],"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,CAAiC,sBAAsB,CAAC,CAAC;AACtG,QAAA,OAAO,aAAa,GAAG,iBAAiB,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,CAAiC,sBAAsB,CAAC,CAAC;AACtG,QAAA,OAAO,aAAa,GAAG,iBAAiB,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,CAAC,sBAAsB,CAAC,GAAG,iBAAiB,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,CAAC,sBAAsB,CAAC,GAAG,iBAAiB,CAAC,IAAI,EAAE,CAAC;KACjF;AACJ;;;;"}