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