@microsoft/feature-management 1.0.0 → 2.0.0-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/common/targetingEvaluator.js +119 -0
- package/dist/commonjs/common/targetingEvaluator.js.map +1 -0
- package/dist/commonjs/featureManager.js +183 -7
- package/dist/commonjs/featureManager.js.map +1 -1
- package/dist/commonjs/featureProvider.js +2 -2
- package/dist/commonjs/featureProvider.js.map +1 -1
- package/dist/commonjs/filter/TargetingFilter.js +6 -74
- package/dist/commonjs/filter/TargetingFilter.js.map +1 -1
- package/dist/commonjs/filter/TimeWindowFilter.js.map +1 -1
- package/dist/commonjs/index.js +5 -0
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/model.js.map +1 -1
- package/dist/commonjs/variant/Variant.js +15 -0
- package/dist/commonjs/variant/Variant.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/esm/common/targetingEvaluator.js +115 -0
- package/dist/esm/common/targetingEvaluator.js.map +1 -0
- package/dist/esm/featureManager.js +183 -8
- package/dist/esm/featureManager.js.map +1 -1
- package/dist/esm/featureProvider.js +2 -2
- package/dist/esm/featureProvider.js.map +1 -1
- package/dist/esm/filter/TargetingFilter.js +6 -74
- package/dist/esm/filter/TargetingFilter.js.map +1 -1
- package/dist/esm/filter/TimeWindowFilter.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/variant/Variant.js +13 -0
- package/dist/esm/variant/Variant.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/umd/index.js +310 -82
- package/dist/umd/index.js.map +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +82 -13
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isTargetedPercentile } from '../common/targetingEvaluator.js';
|
|
2
|
+
|
|
1
3
|
// Copyright (c) Microsoft Corporation.
|
|
2
4
|
// Licensed under the MIT license.
|
|
3
5
|
class TargetingFilter {
|
|
@@ -36,26 +38,16 @@ class TargetingFilter {
|
|
|
36
38
|
parameters.Audience.Groups !== undefined) {
|
|
37
39
|
for (const group of parameters.Audience.Groups) {
|
|
38
40
|
if (appContext.groups.includes(group.Name)) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
if (await TargetingFilter.#isTargeted(audienceContextId, rolloutPercentage)) {
|
|
41
|
+
const hint = `${featureName}\n${group.Name}`;
|
|
42
|
+
if (await isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) {
|
|
42
43
|
return true;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
// check if the user is being targeted by a default rollout percentage
|
|
48
|
-
const
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
static async #isTargeted(audienceContextId, rolloutPercentage) {
|
|
52
|
-
if (rolloutPercentage === 100) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
// Cryptographic hashing algorithms ensure adequate entropy across hash values.
|
|
56
|
-
const contextMarker = await stringToUint32(audienceContextId);
|
|
57
|
-
const contextPercentage = (contextMarker / 0xFFFFFFFF) * 100;
|
|
58
|
-
return contextPercentage < rolloutPercentage;
|
|
49
|
+
const hint = featureName;
|
|
50
|
+
return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
|
|
59
51
|
}
|
|
60
52
|
static #validateParameters(parameters) {
|
|
61
53
|
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
|
|
@@ -71,66 +63,6 @@ class TargetingFilter {
|
|
|
71
63
|
}
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Constructs the context id for the audience.
|
|
76
|
-
* The context id is used to determine if the user is part of the audience for a feature.
|
|
77
|
-
* If groupName is provided, the context id is constructed as follows:
|
|
78
|
-
* userId + "\n" + featureName + "\n" + groupName
|
|
79
|
-
* Otherwise, the context id is constructed as follows:
|
|
80
|
-
* userId + "\n" + featureName
|
|
81
|
-
*
|
|
82
|
-
* @param featureName name of the feature
|
|
83
|
-
* @param userId userId from app context
|
|
84
|
-
* @param groupName group name from app context
|
|
85
|
-
* @returns a string that represents the context id for the audience
|
|
86
|
-
*/
|
|
87
|
-
function constructAudienceContextId(featureName, userId, groupName) {
|
|
88
|
-
let contextId = `${userId ?? ""}\n${featureName}`;
|
|
89
|
-
if (groupName !== undefined) {
|
|
90
|
-
contextId += `\n${groupName}`;
|
|
91
|
-
}
|
|
92
|
-
return contextId;
|
|
93
|
-
}
|
|
94
|
-
async function stringToUint32(str) {
|
|
95
|
-
let crypto;
|
|
96
|
-
// Check for browser environment
|
|
97
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
98
|
-
crypto = window.crypto;
|
|
99
|
-
}
|
|
100
|
-
// Check for Node.js environment
|
|
101
|
-
else if (typeof global !== "undefined" && global.crypto) {
|
|
102
|
-
crypto = global.crypto;
|
|
103
|
-
}
|
|
104
|
-
// Fallback to native Node.js crypto module
|
|
105
|
-
else {
|
|
106
|
-
try {
|
|
107
|
-
if (typeof module !== "undefined" && module.exports) {
|
|
108
|
-
crypto = require("crypto");
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
crypto = await import('crypto');
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
console.error("Failed to load the crypto module:", error.message);
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// In the browser, use crypto.subtle.digest
|
|
120
|
-
if (crypto.subtle) {
|
|
121
|
-
const data = new TextEncoder().encode(str);
|
|
122
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
123
|
-
const dataView = new DataView(hashBuffer);
|
|
124
|
-
const uint32 = dataView.getUint32(0, true);
|
|
125
|
-
return uint32;
|
|
126
|
-
}
|
|
127
|
-
// In Node.js, use the crypto module's hash function
|
|
128
|
-
else {
|
|
129
|
-
const hash = crypto.createHash("sha256").update(str).digest();
|
|
130
|
-
const uint32 = hash.readUInt32LE(0);
|
|
131
|
-
return uint32;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
66
|
|
|
135
67
|
export { TargetingFilter };
|
|
136
68
|
//# 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":[],"mappings":";;AAAA;AACA;MA0Ba,eAAe,CAAA;IACxB,IAAI,GAAW,qBAAqB,CAAC;AAErC,IAAA,MAAM,QAAQ,CAAC,OAAyC,EAAE,UAA8B,EAAA;AACpF,QAAA,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;AAC5C,QAAA,eAAe,CAAC,mBAAmB,CAAC,UAAU,CAAC,CAAC;AAEhD,QAAA,IAAI,UAAU,KAAK,SAAS,EAAE;AAC1B,YAAA,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;SACxE;QAED,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS,EAAE;;AAE7C,YAAA,IAAI,UAAU,EAAE,MAAM,KAAK,SAAS;AAChC,gBAAA,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,KAAK,SAAS;AACjD,gBAAA,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;AACjE,gBAAA,OAAO,KAAK,CAAC;aAChB;;AAED,YAAA,IAAI,UAAU,EAAE,MAAM,KAAK,SAAS;gBAChC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,KAAK,SAAS,EAAE;gBACpD,KAAK,MAAM,aAAa,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE;oBAC9D,IAAI,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AAC3C,wBAAA,OAAO,KAAK,CAAC;qBAChB;iBACJ;aACJ;SACJ;;AAGD,QAAA,IAAI,UAAU,EAAE,MAAM,KAAK,SAAS;AAChC,YAAA,UAAU,CAAC,QAAQ,CAAC,KAAK,KAAK,SAAS;AACvC,YAAA,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;AACvD,YAAA,OAAO,IAAI,CAAC;SACf;;AAGD,QAAA,IAAI,UAAU,EAAE,MAAM,KAAK,SAAS;AAChC,YAAA,UAAU,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE;YAC1C,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE;gBAC5C,IAAI,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;oBACxC,MAAM,IAAI,GAAG,CAAG,EAAA,WAAW,KAAK,KAAK,CAAC,IAAI,CAAA,CAAE,CAAC;AAC7C,oBAAA,IAAI,MAAM,oBAAoB,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE;AACjF,wBAAA,OAAO,IAAI,CAAC;qBACf;iBACJ;aACJ;SACJ;;QAGD,MAAM,IAAI,GAAG,WAAW,CAAC;AACzB,QAAA,OAAO,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;KAC1G;IAED,OAAO,mBAAmB,CAAC,UAAqC,EAAA;AAC5D,QAAA,IAAI,UAAU,CAAC,QAAQ,CAAC,wBAAwB,GAAG,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,wBAAwB,GAAG,GAAG,EAAE;AACxG,YAAA,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;SAC5F;;QAED,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,EAAE;YAC1C,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE;AAC5C,gBAAA,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,IAAI,KAAK,CAAC,iBAAiB,GAAG,GAAG,EAAE;oBAC9D,MAAM,IAAI,KAAK,CAAC,CAAA,2BAAA,EAA8B,KAAK,CAAC,IAAI,CAAsC,oCAAA,CAAA,CAAC,CAAC;iBACnG;aACJ;SACJ;KACJ;AACJ;;;;"}
|
|
@@ -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;;;;"}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { FeatureManager } from './featureManager.js';
|
|
1
|
+
export { EvaluationResult, FeatureManager, VariantAssignmentReason } from './featureManager.js';
|
|
2
2
|
export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider } from './featureProvider.js';
|
|
3
3
|
export { VERSION } from './version.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/dist/esm/model.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.js","sources":["../../src/model.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\
|
|
1
|
+
{"version":3,"file":"model.js","sources":["../../src/model.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\n// Converted from:\n// https://github.com/Azure/AppConfiguration/blob/6e544296a5607f922a423df165f60801717c7800/docs/FeatureManagement/FeatureFlag.v2.0.0.schema.json\n\n/**\n * A feature flag is a named property that can be toggled to enable or disable some feature of an application.\n */\nexport interface FeatureFlag {\n /**\n * An ID used to uniquely identify and reference the feature.\n */\n id: string;\n /**\n * A description of the feature.\n */\n description?: string;\n /**\n * A display name for the feature to use for display rather than the ID.\n */\n display_name?: string;\n /**\n * A feature is OFF if enabled is false. If enabled is true, then the feature is ON if there are no conditions (null or empty) or if the conditions are satisfied.\n */\n enabled?: boolean;\n /**\n * The declaration of conditions used to dynamically enable the feature.\n */\n conditions?: FeatureEnablementConditions;\n /**\n * The list of variants defined for this feature. A variant represents a configuration value of a feature flag that can be a string, a number, a boolean, or a JSON object.\n */\n variants?: VariantDefinition[];\n /**\n * Determines how variants should be allocated for the feature to various users.\n */\n allocation?: VariantAllocation;\n /**\n * The declaration of options used to configure telemetry for this feature.\n */\n telemetry?: TelemetryOptions\n}\n\n/**\n* The declaration of conditions used to dynamically enable the feature\n*/\ninterface FeatureEnablementConditions {\n /**\n * Determines whether any or all registered client filters must be evaluated as true for the feature to be considered enabled.\n */\n requirement_type?: RequirementType;\n /**\n * Filters that must run on the client and be evaluated as true for the feature to be considered enabled.\n */\n client_filters?: ClientFilter[];\n}\n\nexport type RequirementType = \"Any\" | \"All\";\n\ninterface ClientFilter {\n /**\n * The name used to refer to a client filter.\n */\n name: string;\n /**\n * Parameters for a given client filter. A client filter can require any set of parameters of any type.\n */\n parameters?: Record<string, unknown>;\n}\n\nexport interface VariantDefinition {\n /**\n * The name used to refer to a feature variant.\n */\n name: string;\n /**\n * The configuration value for this feature variant.\n */\n configuration_value?: unknown;\n /**\n * Overrides the enabled state of the feature if the given variant is assigned. Does not override the state if value is None.\n */\n status_override?: \"None\" | \"Enabled\" | \"Disabled\";\n}\n\n/**\n* Determines how variants should be allocated for the feature to various users.\n*/\ninterface VariantAllocation {\n /**\n * Specifies which variant should be used when the feature is considered disabled.\n */\n default_when_disabled?: string;\n /**\n * Specifies which variant should be used when the feature is considered enabled and no other allocation rules are applicable.\n */\n default_when_enabled?: string;\n /**\n * A list of objects, each containing a variant name and list of users for whom that variant should be used.\n */\n user?: UserAllocation[];\n /**\n * A list of objects, each containing a variant name and list of groups for which that variant should be used.\n */\n group?: GroupAllocation[];\n /**\n * A list of objects, each containing a variant name and percentage range for which that variant should be used.\n */\n percentile?: PercentileAllocation[]\n /**\n * The value percentile calculations are based on. The calculated percentile is consistent across features for a given user if the same nonempty seed is used.\n */\n seed?: string;\n}\n\ninterface UserAllocation {\n /**\n * The name of the variant to use if the user allocation matches the current user.\n */\n variant: string;\n /**\n * Collection of users where if any match the current user, the variant specified in the user allocation is used.\n */\n users: string[];\n}\n\ninterface GroupAllocation {\n /**\n * The name of the variant to use if the group allocation matches a group the current user is in.\n */\n variant: string;\n /**\n * Collection of groups where if the current user is in any of these groups, the variant specified in the group allocation is used.\n */\n groups: string[];\n}\n\ninterface PercentileAllocation {\n /**\n * The name of the variant to use if the calculated percentile for the current user falls in the provided range.\n */\n variant: string;\n /**\n * The lower end of the percentage range for which this variant will be used.\n */\n from: number;\n /**\n * The upper end of the percentage range for which this variant will be used.\n */\n to: number;\n}\n\n/**\n* The declaration of options used to configure telemetry for this feature.\n*/\ninterface TelemetryOptions {\n /**\n * Indicates if telemetry is enabled.\n */\n enabled?: boolean;\n /**\n * A container for metadata that should be bundled with flag telemetry.\n */\n metadata?: Record<string, string>;\n}\n\n// Feature Management Section fed into feature manager.\n// Converted from https://github.com/Azure/AppConfiguration/blob/main/docs/FeatureManagement/FeatureManagement.v1.0.0.schema.json\n\nexport const FEATURE_MANAGEMENT_KEY = \"feature_management\";\nexport const FEATURE_FLAGS_KEY = \"feature_flags\";\n\nexport interface FeatureManagementConfiguration {\n feature_management: FeatureManagement\n}\n\n/**\n * Declares feature management configuration.\n */\nexport interface FeatureManagement {\n feature_flags: FeatureFlag[];\n}\n"],"names":[],"mappings":"AAAA;AACA;AAsKA;AACA;AAEO,MAAM,sBAAsB,GAAG,qBAAqB;AACpD,MAAM,iBAAiB,GAAG;;;;"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
class Variant {
|
|
4
|
+
name;
|
|
5
|
+
configuration;
|
|
6
|
+
constructor(name, configuration) {
|
|
7
|
+
this.name = name;
|
|
8
|
+
this.configuration = configuration;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { Variant };
|
|
13
|
+
//# sourceMappingURL=Variant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Variant.js","sources":["../../../src/variant/Variant.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nexport class Variant {\n constructor(\n public name: string,\n public configuration: unknown\n ) {}\n}\n"],"names":[],"mappings":"AAAA;AACA;MAEa,OAAO,CAAA;AAEL,IAAA,IAAA,CAAA;AACA,IAAA,aAAA,CAAA;IAFX,WACW,CAAA,IAAY,EACZ,aAAsB,EAAA;QADtB,IAAI,CAAA,IAAA,GAAJ,IAAI,CAAQ;QACZ,IAAa,CAAA,aAAA,GAAb,aAAa,CAAS;KAC7B;AACP;;;;"}
|
package/dist/esm/version.js
CHANGED
package/dist/esm/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sources":["../../src/version.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nexport const VERSION = \"
|
|
1
|
+
{"version":3,"file":"version.js","sources":["../../src/version.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nexport const VERSION = \"2.0.0-preview.2\";\n"],"names":[],"mappings":"AAAA;AACA;AAEO,MAAM,OAAO,GAAG;;;;"}
|