@microsoft/feature-management 1.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.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +1 -0
- package/dist-esm/featureManager.js +60 -0
- package/dist-esm/featureManager.js.map +1 -0
- package/dist-esm/featureProvider.js +37 -0
- package/dist-esm/featureProvider.js.map +1 -0
- package/dist-esm/filter/FeatureFilter.js +4 -0
- package/dist-esm/filter/FeatureFilter.js.map +1 -0
- package/dist-esm/filter/TargetingFilter.js +104 -0
- package/dist-esm/filter/TargetingFilter.js.map +1 -0
- package/dist-esm/filter/TimeWindowFilter.js +18 -0
- package/dist-esm/filter/TimeWindowFilter.js.map +1 -0
- package/dist-esm/gettable.js +6 -0
- package/dist-esm/gettable.js.map +1 -0
- package/dist-esm/index.js +5 -0
- package/dist-esm/index.js.map +1 -0
- package/dist-esm/model.js +12 -0
- package/dist-esm/model.js.map +1 -0
- package/package.json +57 -0
- package/types/index.d.ts +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Microsoft Feature Management for JavaScript
|
|
2
|
+
|
|
3
|
+
Feature Management is a library for enabling/disabling features at runtime.
|
|
4
|
+
Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.
|
|
5
|
+
|
|
6
|
+
## Getting Started
|
|
7
|
+
|
|
8
|
+
### Prerequisites
|
|
9
|
+
|
|
10
|
+
- Node.js LTS version
|
|
11
|
+
|
|
12
|
+
### Usage
|
|
13
|
+
|
|
14
|
+
You can use feature flags from the Azure App Configuration service, local files or any other sources.
|
|
15
|
+
|
|
16
|
+
#### Use feature flags from Azure App Configuration
|
|
17
|
+
|
|
18
|
+
The App Configuration JavaScript provider provides feature flags in as a `Map` object.
|
|
19
|
+
A builtin `ConfigurationMapFeatureFlagProvider` helps to load feature flags in this case.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const appConfig = load(connectionString, {featureFlagOptions}); // load feature flags from Azure App Configuration service
|
|
23
|
+
const featureProvider = new ConfigurationMapFeatureFlagProvider(appConfig);
|
|
24
|
+
const featureManager = new FeatureManager(featureProvider);
|
|
25
|
+
const isAlphaEnabled = await featureManager.isEnabled("Alpha");
|
|
26
|
+
console.log("Feature Alpha is:", isAlphaEnabled);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### Use feature flags from a json file
|
|
30
|
+
|
|
31
|
+
A sample JSON file with the following format can be used to load feature flags.
|
|
32
|
+
The JSON file can be read and parsed as an object as a whole.
|
|
33
|
+
A builtin `ConfigurationObjectFeatureFlagProvider` helps to load feature flags in this case.
|
|
34
|
+
|
|
35
|
+
Content of `sample.json`:
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"feature_management": {
|
|
39
|
+
"feature_flags": [
|
|
40
|
+
{
|
|
41
|
+
"id": "Alpha",
|
|
42
|
+
"description": "",
|
|
43
|
+
"enabled": "true",
|
|
44
|
+
"conditions": {
|
|
45
|
+
"client_filters": []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Load feature flags from `sample.json` file.
|
|
54
|
+
```js
|
|
55
|
+
const config = JSON.parse(await fs.readFile("path/to/sample.json"));
|
|
56
|
+
const featureProvider = new ConfigurationObjectFeatureFlagProvider(config);
|
|
57
|
+
const featureManager = new FeatureManager(featureProvider);
|
|
58
|
+
const isAlphaEnabled = await featureManager.isEnabled("Alpha");
|
|
59
|
+
console.log("Feature Alpha is:", isAlphaEnabled);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Contributing
|
|
63
|
+
|
|
64
|
+
This project welcomes contributions and suggestions. Most contributions require you to agree to a
|
|
65
|
+
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
|
|
66
|
+
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
|
|
67
|
+
|
|
68
|
+
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
|
|
69
|
+
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
|
|
70
|
+
provided by the bot. You will only need to do this once across all repos using our CLA.
|
|
71
|
+
|
|
72
|
+
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
|
73
|
+
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
|
74
|
+
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
|
75
|
+
|
|
76
|
+
## Trademarks
|
|
77
|
+
|
|
78
|
+
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
|
|
79
|
+
trademarks or logos is subject to and must follow
|
|
80
|
+
[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
|
|
81
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
82
|
+
Any use of third-party trademarks or logos are subject to those third-party's policies.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// Copyright (c) Microsoft Corporation.
|
|
6
|
+
// Licensed under the MIT license.
|
|
7
|
+
class TimeWindowFilter {
|
|
8
|
+
name = "Microsoft.TimeWindow";
|
|
9
|
+
evaluate(context) {
|
|
10
|
+
const { featureName, parameters } = context;
|
|
11
|
+
const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;
|
|
12
|
+
const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;
|
|
13
|
+
if (startTime === undefined && endTime === undefined) {
|
|
14
|
+
// If neither start nor end time is specified, then the filter is not applicable.
|
|
15
|
+
console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const now = new Date();
|
|
19
|
+
return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Copyright (c) Microsoft Corporation.
|
|
24
|
+
// Licensed under the MIT license.
|
|
25
|
+
var RequirementType;
|
|
26
|
+
(function (RequirementType) {
|
|
27
|
+
RequirementType["Any"] = "Any";
|
|
28
|
+
RequirementType["All"] = "All";
|
|
29
|
+
})(RequirementType || (RequirementType = {}));
|
|
30
|
+
// Feature Management Section fed into feature manager.
|
|
31
|
+
// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json
|
|
32
|
+
const FEATURE_MANAGEMENT_KEY = "feature_management";
|
|
33
|
+
const FEATURE_FLAGS_KEY = "feature_flags";
|
|
34
|
+
|
|
35
|
+
// Copyright (c) Microsoft Corporation.
|
|
36
|
+
// Licensed under the MIT license.
|
|
37
|
+
class TargetingFilter {
|
|
38
|
+
name = "Microsoft.Targeting";
|
|
39
|
+
evaluate(context, appContext) {
|
|
40
|
+
const { featureName, parameters } = context;
|
|
41
|
+
TargetingFilter.#validateParameters(parameters);
|
|
42
|
+
if (appContext === undefined) {
|
|
43
|
+
throw new Error("The app context is required for targeting filter.");
|
|
44
|
+
}
|
|
45
|
+
if (parameters.Audience.Exclusion !== undefined) {
|
|
46
|
+
// check if the user is in the exclusion list
|
|
47
|
+
if (appContext?.userId !== undefined &&
|
|
48
|
+
parameters.Audience.Exclusion.Users !== undefined &&
|
|
49
|
+
parameters.Audience.Exclusion.Users.includes(appContext.userId)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// check if the user is in a group within exclusion list
|
|
53
|
+
if (appContext?.groups !== undefined &&
|
|
54
|
+
parameters.Audience.Exclusion.Groups !== undefined) {
|
|
55
|
+
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
|
|
56
|
+
if (appContext.groups.includes(excludedGroup)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// check if the user is being targeted directly
|
|
63
|
+
if (appContext?.userId !== undefined &&
|
|
64
|
+
parameters.Audience.Users !== undefined &&
|
|
65
|
+
parameters.Audience.Users.includes(appContext.userId)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// check if the user is in a group that is being targeted
|
|
69
|
+
if (appContext?.groups !== undefined &&
|
|
70
|
+
parameters.Audience.Groups !== undefined) {
|
|
71
|
+
for (const group of parameters.Audience.Groups) {
|
|
72
|
+
if (appContext.groups.includes(group.Name)) {
|
|
73
|
+
const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name);
|
|
74
|
+
const rolloutPercentage = group.RolloutPercentage;
|
|
75
|
+
if (TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// check if the user is being targeted by a default rollout percentage
|
|
82
|
+
const defaultContextId = constructAudienceContextId(featureName, appContext?.userId);
|
|
83
|
+
return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage);
|
|
84
|
+
}
|
|
85
|
+
static #isTargeted(audienceContextId, rolloutPercentage) {
|
|
86
|
+
if (rolloutPercentage === 100) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// Cryptographic hashing algorithms ensure adequate entropy across hash values.
|
|
90
|
+
const contextMarker = stringToUint32(audienceContextId);
|
|
91
|
+
const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
|
|
92
|
+
return contextPercentage < rolloutPercentage;
|
|
93
|
+
}
|
|
94
|
+
static #validateParameters(parameters) {
|
|
95
|
+
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
|
|
96
|
+
throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100.");
|
|
97
|
+
}
|
|
98
|
+
// validate RolloutPercentage for each group
|
|
99
|
+
if (parameters.Audience.Groups !== undefined) {
|
|
100
|
+
for (const group of parameters.Audience.Groups) {
|
|
101
|
+
if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {
|
|
102
|
+
throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Constructs the context id for the audience.
|
|
110
|
+
* The context id is used to determine if the user is part of the audience for a feature.
|
|
111
|
+
* If groupName is provided, the context id is constructed as follows:
|
|
112
|
+
* userId + "\n" + featureName + "\n" + groupName
|
|
113
|
+
* Otherwise, the context id is constructed as follows:
|
|
114
|
+
* userId + "\n" + featureName
|
|
115
|
+
*
|
|
116
|
+
* @param featureName name of the feature
|
|
117
|
+
* @param userId userId from app context
|
|
118
|
+
* @param groupName group name from app context
|
|
119
|
+
* @returns a string that represents the context id for the audience
|
|
120
|
+
*/
|
|
121
|
+
function constructAudienceContextId(featureName, userId, groupName) {
|
|
122
|
+
let contextId = `${userId ?? ""}\n${featureName}`;
|
|
123
|
+
if (groupName !== undefined) {
|
|
124
|
+
contextId += `\n${groupName}`;
|
|
125
|
+
}
|
|
126
|
+
return contextId;
|
|
127
|
+
}
|
|
128
|
+
function stringToUint32(str) {
|
|
129
|
+
// Create a SHA-256 hash of the string
|
|
130
|
+
const hash = crypto.createHash("sha256").update(str).digest();
|
|
131
|
+
// Get the first 4 bytes of the hash
|
|
132
|
+
const first4Bytes = hash.subarray(0, 4);
|
|
133
|
+
// Convert the 4 bytes to a uint32 with little-endian encoding
|
|
134
|
+
const uint32 = first4Bytes.readUInt32LE(0);
|
|
135
|
+
return uint32;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Copyright (c) Microsoft Corporation.
|
|
139
|
+
// Licensed under the MIT license.
|
|
140
|
+
class FeatureManager {
|
|
141
|
+
#provider;
|
|
142
|
+
#featureFilters = new Map();
|
|
143
|
+
constructor(provider, options) {
|
|
144
|
+
this.#provider = provider;
|
|
145
|
+
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];
|
|
146
|
+
// If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.
|
|
147
|
+
for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
|
|
148
|
+
this.#featureFilters.set(filter.name, filter);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async listFeatureNames() {
|
|
152
|
+
const features = await this.#provider.getFeatureFlags();
|
|
153
|
+
const featureNameSet = new Set(features.map((feature) => feature.id));
|
|
154
|
+
return Array.from(featureNameSet);
|
|
155
|
+
}
|
|
156
|
+
// If multiple feature flags are found, the first one takes precedence.
|
|
157
|
+
async isEnabled(featureName, context) {
|
|
158
|
+
const featureFlag = await this.#provider.getFeatureFlag(featureName);
|
|
159
|
+
if (featureFlag === undefined) {
|
|
160
|
+
// If the feature is not found, then it is disabled.
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (featureFlag.enabled === false) {
|
|
164
|
+
// If the feature is explicitly disabled, then it is disabled.
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const clientFilters = featureFlag.conditions?.client_filters;
|
|
168
|
+
if (clientFilters === undefined || clientFilters.length <= 0) {
|
|
169
|
+
// If there are no client filters, then the feature is enabled.
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
const requirementType = featureFlag.conditions?.requirement_type ?? RequirementType.Any; // default to any.
|
|
173
|
+
/**
|
|
174
|
+
* While iterating through the client filters, we short-circuit the evaluation based on the requirement type.
|
|
175
|
+
* - 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.
|
|
176
|
+
* - 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.
|
|
177
|
+
*/
|
|
178
|
+
const shortCircuitEvaluationResult = requirementType === RequirementType.Any;
|
|
179
|
+
for (const clientFilter of clientFilters) {
|
|
180
|
+
const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
|
|
181
|
+
const contextWithFeatureName = { featureName, parameters: clientFilter.parameters };
|
|
182
|
+
if (matchedFeatureFilter === undefined) {
|
|
183
|
+
console.warn(`Feature filter ${clientFilter.name} is not found.`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) {
|
|
187
|
+
return shortCircuitEvaluationResult;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// If we get here, then we have not found a client filter that matches the requirement type.
|
|
191
|
+
return !shortCircuitEvaluationResult;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Copyright (c) Microsoft Corporation.
|
|
196
|
+
// Licensed under the MIT license.
|
|
197
|
+
/**
|
|
198
|
+
* A feature flag provider that uses a map-like configuration to provide feature flags.
|
|
199
|
+
*/
|
|
200
|
+
class ConfigurationMapFeatureFlagProvider {
|
|
201
|
+
#configuration;
|
|
202
|
+
constructor(configuration) {
|
|
203
|
+
this.#configuration = configuration;
|
|
204
|
+
}
|
|
205
|
+
async getFeatureFlag(featureName) {
|
|
206
|
+
const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
|
|
207
|
+
return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
|
|
208
|
+
}
|
|
209
|
+
async getFeatureFlags() {
|
|
210
|
+
const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
|
|
211
|
+
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A feature flag provider that uses an object-like configuration to provide feature flags.
|
|
216
|
+
*/
|
|
217
|
+
class ConfigurationObjectFeatureFlagProvider {
|
|
218
|
+
#configuration;
|
|
219
|
+
constructor(configuration) {
|
|
220
|
+
this.#configuration = configuration;
|
|
221
|
+
}
|
|
222
|
+
async getFeatureFlag(featureName) {
|
|
223
|
+
const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];
|
|
224
|
+
return featureFlags?.find((feature) => feature.id === featureName);
|
|
225
|
+
}
|
|
226
|
+
async getFeatureFlags() {
|
|
227
|
+
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
exports.ConfigurationMapFeatureFlagProvider = ConfigurationMapFeatureFlagProvider;
|
|
232
|
+
exports.ConfigurationObjectFeatureFlagProvider = ConfigurationObjectFeatureFlagProvider;
|
|
233
|
+
exports.FeatureManager = FeatureManager;
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/filter/TimeWindowFilter.ts","../src/model.ts","../src/filter/TargetingFilter.ts","../src/featureManager.ts","../src/featureProvider.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IFeatureFilter } from \"./FeatureFilter\";\r\n\r\n// [Start, End)\r\ntype TimeWindowParameters = {\r\n Start?: string;\r\n End?: string;\r\n}\r\n\r\ntype TimeWindowFilterEvaluationContext = {\r\n featureName: string;\r\n parameters: TimeWindowParameters;\r\n}\r\n\r\nexport class TimeWindowFilter implements IFeatureFilter {\r\n name: string = \"Microsoft.TimeWindow\";\r\n\r\n evaluate(context: TimeWindowFilterEvaluationContext): boolean {\r\n const {featureName, parameters} = context;\r\n const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;\r\n const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;\r\n\r\n if (startTime === undefined && endTime === undefined) {\r\n // If neither start nor end time is specified, then the filter is not applicable.\r\n console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);\r\n return false;\r\n }\r\n const now = new Date();\r\n return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);\r\n }\r\n}\r\n","// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\n// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json\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 /**\r\n * A description of the feature.\r\n */\r\n description?: string\r\n\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 /**\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 /**\r\n * The declaration of conditions used to dynamically enable features.\r\n */\r\n conditions?: FeatureEnablementConditions\r\n}\r\n\r\nexport enum RequirementType {\r\n Any = \"Any\",\r\n All = \"All\"\r\n}\r\n\r\nexport interface 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 /**\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 interface ClientFilter {\r\n /**\r\n * The name used to refer to and require a client filter.\r\n */\r\n name: string\r\n /**\r\n * Custom parameters for a given client filter. A client filter can require any set of parameters of any type.\r\n */\r\n parameters?: unknown\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","// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IFeatureFilter } from \"./FeatureFilter\";\r\nimport { createHash } from \"crypto\";\r\n\r\ntype TargetingFilterParameters = {\r\n Audience: {\r\n DefaultRolloutPercentage: number;\r\n Users?: string[];\r\n Groups?: {\r\n Name: string;\r\n RolloutPercentage: number;\r\n }[];\r\n Exclusion?: {\r\n Users?: string[];\r\n Groups?: string[];\r\n };\r\n }\r\n}\r\n\r\ntype TargetingFilterEvaluationContext = {\r\n featureName: string;\r\n parameters: TargetingFilterParameters;\r\n}\r\n\r\ntype TargetingFilterAppContext = {\r\n userId?: string;\r\n groups?: string[];\r\n}\r\n\r\nexport class TargetingFilter implements IFeatureFilter {\r\n name: string = \"Microsoft.Targeting\";\r\n\r\n evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): boolean {\r\n const { featureName, parameters } = context;\r\n TargetingFilter.#validateParameters(parameters);\r\n\r\n if (appContext === undefined) {\r\n throw new Error(\"The app context is required for targeting filter.\");\r\n }\r\n\r\n if (parameters.Audience.Exclusion !== undefined) {\r\n // check if the user is in the exclusion list\r\n if (appContext?.userId !== undefined &&\r\n parameters.Audience.Exclusion.Users !== undefined &&\r\n parameters.Audience.Exclusion.Users.includes(appContext.userId)) {\r\n return false;\r\n }\r\n // check if the user is in a group within exclusion list\r\n if (appContext?.groups !== undefined &&\r\n parameters.Audience.Exclusion.Groups !== undefined) {\r\n for (const excludedGroup of parameters.Audience.Exclusion.Groups) {\r\n if (appContext.groups.includes(excludedGroup)) {\r\n return false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // check if the user is being targeted directly\r\n if (appContext?.userId !== undefined &&\r\n parameters.Audience.Users !== undefined &&\r\n parameters.Audience.Users.includes(appContext.userId)) {\r\n return true;\r\n }\r\n\r\n // check if the user is in a group that is being targeted\r\n if (appContext?.groups !== undefined &&\r\n parameters.Audience.Groups !== undefined) {\r\n for (const group of parameters.Audience.Groups) {\r\n if (appContext.groups.includes(group.Name)) {\r\n const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name);\r\n const rolloutPercentage = group.RolloutPercentage;\r\n if (TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {\r\n return true;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // check if the user is being targeted by a default rollout percentage\r\n const defaultContextId = constructAudienceContextId(featureName, appContext?.userId);\r\n return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage);\r\n }\r\n\r\n static #isTargeted(audienceContextId: string, rolloutPercentage: number): boolean {\r\n if (rolloutPercentage === 100) {\r\n return true;\r\n }\r\n // Cryptographic hashing algorithms ensure adequate entropy across hash values.\r\n const contextMarker = stringToUint32(audienceContextId);\r\n const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;\r\n return contextPercentage < rolloutPercentage;\r\n }\r\n\r\n static #validateParameters(parameters: TargetingFilterParameters): void {\r\n if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {\r\n throw new Error(\"Audience.DefaultRolloutPercentage must be a number between 0 and 100.\");\r\n }\r\n // validate RolloutPercentage for each group\r\n if (parameters.Audience.Groups !== undefined) {\r\n for (const group of parameters.Audience.Groups) {\r\n if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {\r\n throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Constructs the context id for the audience.\r\n * The context id is used to determine if the user is part of the audience for a feature.\r\n * If groupName is provided, the context id is constructed as follows:\r\n * userId + \"\\n\" + featureName + \"\\n\" + groupName\r\n * Otherwise, the context id is constructed as follows:\r\n * userId + \"\\n\" + featureName\r\n *\r\n * @param featureName name of the feature\r\n * @param userId userId from app context\r\n * @param groupName group name from app context\r\n * @returns a string that represents the context id for the audience\r\n */\r\nfunction constructAudienceContextId(featureName: string, userId: string | undefined, groupName?: string) {\r\n let contextId = `${userId ?? \"\"}\\n${featureName}`;\r\n if (groupName !== undefined) {\r\n contextId += `\\n${groupName}`;\r\n }\r\n return contextId\r\n}\r\n\r\nfunction stringToUint32(str: string): number {\r\n // Create a SHA-256 hash of the string\r\n const hash = createHash(\"sha256\").update(str).digest();\r\n\r\n // Get the first 4 bytes of the hash\r\n const first4Bytes = hash.subarray(0, 4);\r\n\r\n // Convert the 4 bytes to a uint32 with little-endian encoding\r\n const uint32 = first4Bytes.readUInt32LE(0);\r\n return uint32;\r\n}\r\n","// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { TimeWindowFilter } from \"./filter/TimeWindowFilter\";\r\nimport { IFeatureFilter } from \"./filter/FeatureFilter\";\r\nimport { RequirementType } from \"./model\";\r\nimport { IFeatureFlagProvider } from \"./featureProvider\";\r\nimport { TargetingFilter } from \"./filter/TargetingFilter\";\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 if (featureFlag.enabled === false) {\r\n // If the feature is explicitly disabled, then it is disabled.\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 = featureFlag.conditions?.requirement_type ?? RequirementType.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 === 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","// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IGettable } from \"./gettable\";\r\nimport { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from \"./model\";\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]?.find((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?.find((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":["createHash"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { TimeWindowFilter } from "./filter/TimeWindowFilter";
|
|
4
|
+
import { RequirementType } from "./model";
|
|
5
|
+
import { TargetingFilter } from "./filter/TargetingFilter";
|
|
6
|
+
export class FeatureManager {
|
|
7
|
+
#provider;
|
|
8
|
+
#featureFilters = new Map();
|
|
9
|
+
constructor(provider, options) {
|
|
10
|
+
this.#provider = provider;
|
|
11
|
+
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];
|
|
12
|
+
// If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.
|
|
13
|
+
for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
|
|
14
|
+
this.#featureFilters.set(filter.name, filter);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async listFeatureNames() {
|
|
18
|
+
const features = await this.#provider.getFeatureFlags();
|
|
19
|
+
const featureNameSet = new Set(features.map((feature) => feature.id));
|
|
20
|
+
return Array.from(featureNameSet);
|
|
21
|
+
}
|
|
22
|
+
// If multiple feature flags are found, the first one takes precedence.
|
|
23
|
+
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
|
+
}
|
|
29
|
+
if (featureFlag.enabled === false) {
|
|
30
|
+
// If the feature is explicitly disabled, then it is disabled.
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const clientFilters = featureFlag.conditions?.client_filters;
|
|
34
|
+
if (clientFilters === undefined || clientFilters.length <= 0) {
|
|
35
|
+
// If there are no client filters, then the feature is enabled.
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const requirementType = featureFlag.conditions?.requirement_type ?? RequirementType.Any; // default to any.
|
|
39
|
+
/**
|
|
40
|
+
* While iterating through the client filters, we short-circuit the evaluation based on the requirement type.
|
|
41
|
+
* - 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.
|
|
42
|
+
* - 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.
|
|
43
|
+
*/
|
|
44
|
+
const shortCircuitEvaluationResult = requirementType === RequirementType.Any;
|
|
45
|
+
for (const clientFilter of clientFilters) {
|
|
46
|
+
const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
|
|
47
|
+
const contextWithFeatureName = { featureName, parameters: clientFilter.parameters };
|
|
48
|
+
if (matchedFeatureFilter === undefined) {
|
|
49
|
+
console.warn(`Feature filter ${clientFilter.name} is not found.`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) {
|
|
53
|
+
return shortCircuitEvaluationResult;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// If we get here, then we have not found a client filter that matches the requirement type.
|
|
57
|
+
return !shortCircuitEvaluationResult;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=featureManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featureManager.js","sourceRoot":"","sources":["../src/featureManager.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAElC,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE3D,MAAM,OAAO,cAAc;IACvB,SAAS,CAAuB;IAChC,eAAe,GAAgC,IAAI,GAAG,EAAE,CAAC;IAEzD,YAAY,QAA8B,EAAE,OAA+B;QACvE,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAE1B,MAAM,cAAc,GAAG,CAAC,IAAI,gBAAgB,EAAE,EAAE,IAAI,eAAe,EAAE,CAAC,CAAC;QAEvE,0GAA0G;QAC1G,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,cAAc,EAAE,GAAG,CAAC,OAAO,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;YAC1E,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAClD,CAAC;IACL,CAAC;IAED,KAAK,CAAC,gBAAgB;QAClB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,eAAe,EAAE,CAAC;QACxD,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QACtE,OAAO,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACtC,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,SAAS,CAAC,WAAmB,EAAE,OAAiB;QAClD,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC;QACrE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC5B,oDAAoD;YACpD,OAAO,KAAK,CAAC;QACjB,CAAC;QAED,IAAI,WAAW,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YAChC,8DAA8D;YAC9D,OAAO,KAAK,CAAC;QACjB,CAAC;QAED,MAAM,aAAa,GAAG,WAAW,CAAC,UAAU,EAAE,cAAc,CAAC;QAC7D,IAAI,aAAa,KAAK,SAAS,IAAI,aAAa,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAC3D,+DAA+D;YAC/D,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,eAAe,GAAG,WAAW,CAAC,UAAU,EAAE,gBAAgB,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,kBAAkB;QAE3G;;;;WAIG;QACH,MAAM,4BAA4B,GAAY,eAAe,KAAK,eAAe,CAAC,GAAG,CAAC;QAEtF,KAAK,MAAM,YAAY,IAAI,aAAa,EAAE,CAAC;YACvC,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;YACpF,IAAI,oBAAoB,KAAK,SAAS,EAAE,CAAC;gBACrC,OAAO,CAAC,IAAI,CAAC,kBAAkB,YAAY,CAAC,IAAI,gBAAgB,CAAC,CAAC;gBAClE,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,IAAI,MAAM,oBAAoB,CAAC,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC,KAAK,4BAA4B,EAAE,CAAC;gBACxG,OAAO,4BAA4B,CAAC;YACxC,CAAC;QACL,CAAC;QAED,4FAA4F;QAC5F,OAAO,CAAC,4BAA4B,CAAC;IACzC,CAAC;CAEJ","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { TimeWindowFilter } from \"./filter/TimeWindowFilter\";\r\nimport { IFeatureFilter } from \"./filter/FeatureFilter\";\r\nimport { RequirementType } from \"./model\";\r\nimport { IFeatureFlagProvider } from \"./featureProvider\";\r\nimport { TargetingFilter } from \"./filter/TargetingFilter\";\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 if (featureFlag.enabled === false) {\r\n // If the feature is explicitly disabled, then it is disabled.\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 = featureFlag.conditions?.requirement_type ?? RequirementType.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 === 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"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from "./model";
|
|
4
|
+
/**
|
|
5
|
+
* A feature flag provider that uses a map-like configuration to provide feature flags.
|
|
6
|
+
*/
|
|
7
|
+
export class ConfigurationMapFeatureFlagProvider {
|
|
8
|
+
#configuration;
|
|
9
|
+
constructor(configuration) {
|
|
10
|
+
this.#configuration = configuration;
|
|
11
|
+
}
|
|
12
|
+
async getFeatureFlag(featureName) {
|
|
13
|
+
const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
|
|
14
|
+
return featureConfig?.[FEATURE_FLAGS_KEY]?.find((feature) => feature.id === featureName);
|
|
15
|
+
}
|
|
16
|
+
async getFeatureFlags() {
|
|
17
|
+
const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY);
|
|
18
|
+
return featureConfig?.[FEATURE_FLAGS_KEY] ?? [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A feature flag provider that uses an object-like configuration to provide feature flags.
|
|
23
|
+
*/
|
|
24
|
+
export class ConfigurationObjectFeatureFlagProvider {
|
|
25
|
+
#configuration;
|
|
26
|
+
constructor(configuration) {
|
|
27
|
+
this.#configuration = configuration;
|
|
28
|
+
}
|
|
29
|
+
async getFeatureFlag(featureName) {
|
|
30
|
+
const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY];
|
|
31
|
+
return featureFlags?.find((feature) => feature.id === featureName);
|
|
32
|
+
}
|
|
33
|
+
async getFeatureFlags() {
|
|
34
|
+
return this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=featureProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featureProvider.js","sourceRoot":"","sources":["../src/featureProvider.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAGlC,OAAO,EAA+C,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAejH;;GAEG;AACH,MAAM,OAAO,mCAAmC;IAC5C,cAAc,CAAY;IAE1B,YAAY,aAAwB;QAChC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;IACxC,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,WAAmB;QACpC,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAiC,sBAAsB,CAAC,CAAC;QACtG,OAAO,aAAa,EAAE,CAAC,iBAAiB,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;IAC7F,CAAC;IAED,KAAK,CAAC,eAAe;QACjB,MAAM,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAiC,sBAAsB,CAAC,CAAC;QACtG,OAAO,aAAa,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;IACpD,CAAC;CACJ;AAED;;GAEG;AACH,MAAM,OAAO,sCAAsC;IAC/C,cAAc,CAA0B;IAExC,YAAY,aAAsC;QAC9C,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,WAAmB;QACpC,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,sBAAsB,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC;QACtF,OAAO,YAAY,EAAE,IAAI,CAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,WAAW,CAAC,CAAC;IACpF,CAAC;IAED,KAAK,CAAC,eAAe;QACjB,OAAO,IAAI,CAAC,cAAc,CAAC,sBAAsB,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;IAClF,CAAC;CACJ","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IGettable } from \"./gettable\";\r\nimport { FeatureFlag, FeatureManagementConfiguration, FEATURE_MANAGEMENT_KEY, FEATURE_FLAGS_KEY } from \"./model\";\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]?.find((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?.find((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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureFilter.js","sourceRoot":"","sources":["../../src/filter/FeatureFilter.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nexport interface IFeatureFilter {\r\n name: string; // e.g. Microsoft.TimeWindow\r\n evaluate(context: IFeatureFilterEvaluationContext, appContext?: unknown): boolean | Promise<boolean>;\r\n}\r\n\r\nexport interface IFeatureFilterEvaluationContext {\r\n featureName: string;\r\n parameters?: unknown;\r\n}\r\n"]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
export class TargetingFilter {
|
|
5
|
+
name = "Microsoft.Targeting";
|
|
6
|
+
evaluate(context, appContext) {
|
|
7
|
+
const { featureName, parameters } = context;
|
|
8
|
+
TargetingFilter.#validateParameters(parameters);
|
|
9
|
+
if (appContext === undefined) {
|
|
10
|
+
throw new Error("The app context is required for targeting filter.");
|
|
11
|
+
}
|
|
12
|
+
if (parameters.Audience.Exclusion !== undefined) {
|
|
13
|
+
// check if the user is in the exclusion list
|
|
14
|
+
if (appContext?.userId !== undefined &&
|
|
15
|
+
parameters.Audience.Exclusion.Users !== undefined &&
|
|
16
|
+
parameters.Audience.Exclusion.Users.includes(appContext.userId)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// check if the user is in a group within exclusion list
|
|
20
|
+
if (appContext?.groups !== undefined &&
|
|
21
|
+
parameters.Audience.Exclusion.Groups !== undefined) {
|
|
22
|
+
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
|
|
23
|
+
if (appContext.groups.includes(excludedGroup)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// check if the user is being targeted directly
|
|
30
|
+
if (appContext?.userId !== undefined &&
|
|
31
|
+
parameters.Audience.Users !== undefined &&
|
|
32
|
+
parameters.Audience.Users.includes(appContext.userId)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// check if the user is in a group that is being targeted
|
|
36
|
+
if (appContext?.groups !== undefined &&
|
|
37
|
+
parameters.Audience.Groups !== undefined) {
|
|
38
|
+
for (const group of parameters.Audience.Groups) {
|
|
39
|
+
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
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// 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;
|
|
60
|
+
}
|
|
61
|
+
static #validateParameters(parameters) {
|
|
62
|
+
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
|
|
63
|
+
throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100.");
|
|
64
|
+
}
|
|
65
|
+
// validate RolloutPercentage for each group
|
|
66
|
+
if (parameters.Audience.Groups !== undefined) {
|
|
67
|
+
for (const group of parameters.Audience.Groups) {
|
|
68
|
+
if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {
|
|
69
|
+
throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TargetingFilter.js","sourceRoot":"","sources":["../../src/filter/TargetingFilter.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IFeatureFilter } from \"./FeatureFilter\";\r\nimport { createHash } from \"crypto\";\r\n\r\ntype TargetingFilterParameters = {\r\n Audience: {\r\n DefaultRolloutPercentage: number;\r\n Users?: string[];\r\n Groups?: {\r\n Name: string;\r\n RolloutPercentage: number;\r\n }[];\r\n Exclusion?: {\r\n Users?: string[];\r\n Groups?: string[];\r\n };\r\n }\r\n}\r\n\r\ntype TargetingFilterEvaluationContext = {\r\n featureName: string;\r\n parameters: TargetingFilterParameters;\r\n}\r\n\r\ntype TargetingFilterAppContext = {\r\n userId?: string;\r\n groups?: string[];\r\n}\r\n\r\nexport class TargetingFilter implements IFeatureFilter {\r\n name: string = \"Microsoft.Targeting\";\r\n\r\n evaluate(context: TargetingFilterEvaluationContext, appContext?: TargetingFilterAppContext): boolean {\r\n const { featureName, parameters } = context;\r\n TargetingFilter.#validateParameters(parameters);\r\n\r\n if (appContext === undefined) {\r\n throw new Error(\"The app context is required for targeting filter.\");\r\n }\r\n\r\n if (parameters.Audience.Exclusion !== undefined) {\r\n // check if the user is in the exclusion list\r\n if (appContext?.userId !== undefined &&\r\n parameters.Audience.Exclusion.Users !== undefined &&\r\n parameters.Audience.Exclusion.Users.includes(appContext.userId)) {\r\n return false;\r\n }\r\n // check if the user is in a group within exclusion list\r\n if (appContext?.groups !== undefined &&\r\n parameters.Audience.Exclusion.Groups !== undefined) {\r\n for (const excludedGroup of parameters.Audience.Exclusion.Groups) {\r\n if (appContext.groups.includes(excludedGroup)) {\r\n return false;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // check if the user is being targeted directly\r\n if (appContext?.userId !== undefined &&\r\n parameters.Audience.Users !== undefined &&\r\n parameters.Audience.Users.includes(appContext.userId)) {\r\n return true;\r\n }\r\n\r\n // check if the user is in a group that is being targeted\r\n if (appContext?.groups !== undefined &&\r\n parameters.Audience.Groups !== undefined) {\r\n for (const group of parameters.Audience.Groups) {\r\n if (appContext.groups.includes(group.Name)) {\r\n const audienceContextId = constructAudienceContextId(featureName, appContext.userId, group.Name);\r\n const rolloutPercentage = group.RolloutPercentage;\r\n if (TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {\r\n return true;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // check if the user is being targeted by a default rollout percentage\r\n const defaultContextId = constructAudienceContextId(featureName, appContext?.userId);\r\n return TargetingFilter.#isTargeted(defaultContextId, parameters.Audience.DefaultRolloutPercentage);\r\n }\r\n\r\n static #isTargeted(audienceContextId: string, rolloutPercentage: number): boolean {\r\n if (rolloutPercentage === 100) {\r\n return true;\r\n }\r\n // Cryptographic hashing algorithms ensure adequate entropy across hash values.\r\n const contextMarker = stringToUint32(audienceContextId);\r\n const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;\r\n return contextPercentage < rolloutPercentage;\r\n }\r\n\r\n static #validateParameters(parameters: TargetingFilterParameters): void {\r\n if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {\r\n throw new Error(\"Audience.DefaultRolloutPercentage must be a number between 0 and 100.\");\r\n }\r\n // validate RolloutPercentage for each group\r\n if (parameters.Audience.Groups !== undefined) {\r\n for (const group of parameters.Audience.Groups) {\r\n if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) {\r\n throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Constructs the context id for the audience.\r\n * The context id is used to determine if the user is part of the audience for a feature.\r\n * If groupName is provided, the context id is constructed as follows:\r\n * userId + \"\\n\" + featureName + \"\\n\" + groupName\r\n * Otherwise, the context id is constructed as follows:\r\n * userId + \"\\n\" + featureName\r\n *\r\n * @param featureName name of the feature\r\n * @param userId userId from app context\r\n * @param groupName group name from app context\r\n * @returns a string that represents the context id for the audience\r\n */\r\nfunction constructAudienceContextId(featureName: string, userId: string | undefined, groupName?: string) {\r\n let contextId = `${userId ?? \"\"}\\n${featureName}`;\r\n if (groupName !== undefined) {\r\n contextId += `\\n${groupName}`;\r\n }\r\n return contextId\r\n}\r\n\r\nfunction stringToUint32(str: string): number {\r\n // Create a SHA-256 hash of the string\r\n const hash = createHash(\"sha256\").update(str).digest();\r\n\r\n // Get the first 4 bytes of the hash\r\n const first4Bytes = hash.subarray(0, 4);\r\n\r\n // Convert the 4 bytes to a uint32 with little-endian encoding\r\n const uint32 = first4Bytes.readUInt32LE(0);\r\n return uint32;\r\n}\r\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
export class TimeWindowFilter {
|
|
4
|
+
name = "Microsoft.TimeWindow";
|
|
5
|
+
evaluate(context) {
|
|
6
|
+
const { featureName, parameters } = context;
|
|
7
|
+
const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;
|
|
8
|
+
const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;
|
|
9
|
+
if (startTime === undefined && endTime === undefined) {
|
|
10
|
+
// If neither start nor end time is specified, then the filter is not applicable.
|
|
11
|
+
console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const now = new Date();
|
|
15
|
+
return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=TimeWindowFilter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimeWindowFilter.js","sourceRoot":"","sources":["../../src/filter/TimeWindowFilter.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAelC,MAAM,OAAO,gBAAgB;IACzB,IAAI,GAAW,sBAAsB,CAAC;IAEtC,QAAQ,CAAC,OAA0C;QAC/C,MAAM,EAAC,WAAW,EAAE,UAAU,EAAC,GAAG,OAAO,CAAC;QAC1C,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1F,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAEpF,IAAI,SAAS,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YACnD,iFAAiF;YACjF,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,4CAA4C,WAAW,mDAAmD,CAAC,CAAC;YACzI,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,OAAO,CAAC,SAAS,KAAK,SAAS,IAAI,SAAS,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,GAAG,GAAG,OAAO,CAAC,CAAC;IACrG,CAAC;CACJ","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nimport { IFeatureFilter } from \"./FeatureFilter\";\r\n\r\n// [Start, End)\r\ntype TimeWindowParameters = {\r\n Start?: string;\r\n End?: string;\r\n}\r\n\r\ntype TimeWindowFilterEvaluationContext = {\r\n featureName: string;\r\n parameters: TimeWindowParameters;\r\n}\r\n\r\nexport class TimeWindowFilter implements IFeatureFilter {\r\n name: string = \"Microsoft.TimeWindow\";\r\n\r\n evaluate(context: TimeWindowFilterEvaluationContext): boolean {\r\n const {featureName, parameters} = context;\r\n const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;\r\n const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;\r\n\r\n if (startTime === undefined && endTime === undefined) {\r\n // If neither start nor end time is specified, then the filter is not applicable.\r\n console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);\r\n return false;\r\n }\r\n const now = new Date();\r\n return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gettable.js","sourceRoot":"","sources":["../src/gettable.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAMlC,MAAM,UAAU,UAAU,CAAC,MAAe;IACtC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,OAAQ,MAAoB,CAAC,GAAG,KAAK,UAAU,CAAC;AAC5G,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nexport interface IGettable {\r\n get<T>(key: string): T | undefined;\r\n}\r\n\r\nexport function isGettable(object: unknown): object is IGettable {\r\n return typeof object === \"object\" && object !== null && typeof (object as IGettable).get === \"function\";\r\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
export { FeatureManager } from "./featureManager";
|
|
4
|
+
export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider } from "./featureProvider";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAElC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mCAAmC,EAAE,sCAAsC,EAAwB,MAAM,mBAAmB,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\nexport { FeatureManager } from \"./featureManager\";\r\nexport { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from \"./featureProvider\";\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
export var RequirementType;
|
|
4
|
+
(function (RequirementType) {
|
|
5
|
+
RequirementType["Any"] = "Any";
|
|
6
|
+
RequirementType["All"] = "All";
|
|
7
|
+
})(RequirementType || (RequirementType = {}));
|
|
8
|
+
// Feature Management Section fed into feature manager.
|
|
9
|
+
// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json
|
|
10
|
+
export const FEATURE_MANAGEMENT_KEY = "feature_management";
|
|
11
|
+
export const FEATURE_FLAGS_KEY = "feature_flags";
|
|
12
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AA+BlC,MAAM,CAAN,IAAY,eAGX;AAHD,WAAY,eAAe;IACzB,8BAAW,CAAA;IACX,8BAAW,CAAA;AACb,CAAC,EAHW,eAAe,KAAf,eAAe,QAG1B;AAyBD,uDAAuD;AACvD,iIAAiI;AAEjI,MAAM,CAAC,MAAM,sBAAsB,GAAG,oBAAoB,CAAA;AAC1D,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAA","sourcesContent":["// Copyright (c) Microsoft Corporation.\r\n// Licensed under the MIT license.\r\n\r\n// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureFlag.v1.1.0.schema.json\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 /**\r\n * A description of the feature.\r\n */\r\n description?: string\r\n\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 /**\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 /**\r\n * The declaration of conditions used to dynamically enable features.\r\n */\r\n conditions?: FeatureEnablementConditions\r\n}\r\n\r\nexport enum RequirementType {\r\n Any = \"Any\",\r\n All = \"All\"\r\n}\r\n\r\nexport interface 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 /**\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 interface ClientFilter {\r\n /**\r\n * The name used to refer to and require a client filter.\r\n */\r\n name: string\r\n /**\r\n * Custom parameters for a given client filter. A client filter can require any set of parameters of any type.\r\n */\r\n parameters?: unknown\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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@microsoft/feature-management",
|
|
3
|
+
"version": "1.0.0-preview.1",
|
|
4
|
+
"description": "Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "./dist-esm/index.js",
|
|
7
|
+
"types": "types/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/**/*.js",
|
|
10
|
+
"dist/**/*.map",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
12
|
+
"dist-esm/**/*.js",
|
|
13
|
+
"dist-esm/**/*.map",
|
|
14
|
+
"dist-esm/**/*.d.ts",
|
|
15
|
+
"types/**/*.d.ts",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npm run clean && npm run build-cjs && npm run build-esm && npm run build-test",
|
|
21
|
+
"build-cjs": "rollup --config",
|
|
22
|
+
"build-esm": "tsc -p ./tsconfig.json",
|
|
23
|
+
"build-test": "tsc -p ./tsconfig.test.json",
|
|
24
|
+
"clean": "rimraf dist dist-esm out types",
|
|
25
|
+
"dev": "rollup --config --watch",
|
|
26
|
+
"lint": "eslint src/ test/",
|
|
27
|
+
"fix-lint": "eslint src/ test/ --fix",
|
|
28
|
+
"test": "mocha out/test/*.test.{js,cjs,mjs} --parallel"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/microsoft/FeatureManagement-JavaScript#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@rollup/plugin-typescript": "^11.1.5",
|
|
41
|
+
"@types/mocha": "^10.0.6",
|
|
42
|
+
"@types/node": "^20.10.7",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
|
44
|
+
"@typescript-eslint/parser": "^6.18.1",
|
|
45
|
+
"chai": "^4.4.0",
|
|
46
|
+
"eslint": "^8.56.0",
|
|
47
|
+
"mocha": "^10.2.0",
|
|
48
|
+
"rimraf": "^5.0.5",
|
|
49
|
+
"rollup": "^4.9.4",
|
|
50
|
+
"rollup-plugin-dts": "^6.1.0",
|
|
51
|
+
"tslib": "^2.6.2",
|
|
52
|
+
"typescript": "^5.3.3"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"chai-as-promised": "^7.1.1"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
interface IFeatureFilter {
|
|
2
|
+
name: string;
|
|
3
|
+
evaluate(context: IFeatureFilterEvaluationContext, appContext?: unknown): boolean | Promise<boolean>;
|
|
4
|
+
}
|
|
5
|
+
interface IFeatureFilterEvaluationContext {
|
|
6
|
+
featureName: string;
|
|
7
|
+
parameters?: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface IGettable {
|
|
11
|
+
get<T>(key: string): T | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FeatureFlag {
|
|
15
|
+
/**
|
|
16
|
+
* An ID used to uniquely identify and reference the feature.
|
|
17
|
+
*/
|
|
18
|
+
id: string;
|
|
19
|
+
/**
|
|
20
|
+
* A description of the feature.
|
|
21
|
+
*/
|
|
22
|
+
description?: string;
|
|
23
|
+
/**
|
|
24
|
+
* A display name for the feature to use for display rather than the ID.
|
|
25
|
+
*/
|
|
26
|
+
display_name?: string;
|
|
27
|
+
/**
|
|
28
|
+
* 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.
|
|
29
|
+
*/
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The declaration of conditions used to dynamically enable features.
|
|
33
|
+
*/
|
|
34
|
+
conditions?: FeatureEnablementConditions;
|
|
35
|
+
}
|
|
36
|
+
declare enum RequirementType {
|
|
37
|
+
Any = "Any",
|
|
38
|
+
All = "All"
|
|
39
|
+
}
|
|
40
|
+
interface FeatureEnablementConditions {
|
|
41
|
+
/**
|
|
42
|
+
* Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled.
|
|
43
|
+
*/
|
|
44
|
+
requirement_type?: RequirementType;
|
|
45
|
+
/**
|
|
46
|
+
* Filters that must run on the client and be evaluated as true for the feature to be considered enabled.
|
|
47
|
+
*/
|
|
48
|
+
client_filters?: ClientFilter[];
|
|
49
|
+
}
|
|
50
|
+
interface ClientFilter {
|
|
51
|
+
/**
|
|
52
|
+
* The name used to refer to and require a client filter.
|
|
53
|
+
*/
|
|
54
|
+
name: string;
|
|
55
|
+
/**
|
|
56
|
+
* Custom parameters for a given client filter. A client filter can require any set of parameters of any type.
|
|
57
|
+
*/
|
|
58
|
+
parameters?: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface IFeatureFlagProvider {
|
|
62
|
+
/**
|
|
63
|
+
* Get all feature flags.
|
|
64
|
+
*/
|
|
65
|
+
getFeatureFlags(): Promise<FeatureFlag[]>;
|
|
66
|
+
/**
|
|
67
|
+
* Get a feature flag by name.
|
|
68
|
+
* @param featureName The name of the feature flag.
|
|
69
|
+
*/
|
|
70
|
+
getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A feature flag provider that uses a map-like configuration to provide feature flags.
|
|
74
|
+
*/
|
|
75
|
+
declare class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider {
|
|
76
|
+
#private;
|
|
77
|
+
constructor(configuration: IGettable);
|
|
78
|
+
getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;
|
|
79
|
+
getFeatureFlags(): Promise<FeatureFlag[]>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A feature flag provider that uses an object-like configuration to provide feature flags.
|
|
83
|
+
*/
|
|
84
|
+
declare class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvider {
|
|
85
|
+
#private;
|
|
86
|
+
constructor(configuration: Record<string, unknown>);
|
|
87
|
+
getFeatureFlag(featureName: string): Promise<FeatureFlag | undefined>;
|
|
88
|
+
getFeatureFlags(): Promise<FeatureFlag[]>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
declare class FeatureManager {
|
|
92
|
+
#private;
|
|
93
|
+
constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions);
|
|
94
|
+
listFeatureNames(): Promise<string[]>;
|
|
95
|
+
isEnabled(featureName: string, context?: unknown): Promise<boolean>;
|
|
96
|
+
}
|
|
97
|
+
interface FeatureManagerOptions {
|
|
98
|
+
customFilters?: IFeatureFilter[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, FeatureManager, type IFeatureFlagProvider };
|