@microsoft/feature-management 2.0.2 → 2.1.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/featureManager.js +20 -8
- package/dist/commonjs/featureManager.js.map +1 -1
- package/dist/commonjs/filter/TargetingFilter.js +20 -12
- package/dist/commonjs/filter/TargetingFilter.js.map +1 -1
- package/dist/commonjs/filter/TimeWindowFilter.js.map +1 -1
- package/dist/commonjs/telemetry/featureEvaluationEvent.js +26 -0
- package/dist/commonjs/telemetry/featureEvaluationEvent.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/esm/featureManager.js +20 -8
- package/dist/esm/featureManager.js.map +1 -1
- package/dist/esm/filter/TargetingFilter.js +20 -12
- package/dist/esm/filter/TargetingFilter.js.map +1 -1
- package/dist/esm/filter/TimeWindowFilter.js.map +1 -1
- package/dist/esm/telemetry/featureEvaluationEvent.js +26 -0
- package/dist/esm/telemetry/featureEvaluationEvent.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/umd/index.js +66 -21
- package/dist/umd/index.js.map +1 -1
- package/package.json +2 -2
- package/types/index.d.ts +24 -2
|
@@ -4,42 +4,50 @@ import { isTargetedPercentile } from '../common/targetingEvaluator.js';
|
|
|
4
4
|
// Licensed under the MIT license.
|
|
5
5
|
class TargetingFilter {
|
|
6
6
|
name = "Microsoft.Targeting";
|
|
7
|
+
#targetingContextAccessor;
|
|
8
|
+
constructor(targetingContextAccessor) {
|
|
9
|
+
this.#targetingContextAccessor = targetingContextAccessor;
|
|
10
|
+
}
|
|
7
11
|
async evaluate(context, appContext) {
|
|
8
12
|
const { featureName, parameters } = context;
|
|
9
13
|
TargetingFilter.#validateParameters(featureName, parameters);
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
let targetingContext;
|
|
15
|
+
if (appContext?.userId !== undefined || appContext?.groups !== undefined) {
|
|
16
|
+
targetingContext = appContext;
|
|
17
|
+
}
|
|
18
|
+
else if (this.#targetingContextAccessor !== undefined) {
|
|
19
|
+
targetingContext = this.#targetingContextAccessor.getTargetingContext();
|
|
12
20
|
}
|
|
13
21
|
if (parameters.Audience.Exclusion !== undefined) {
|
|
14
22
|
// check if the user is in the exclusion list
|
|
15
|
-
if (
|
|
23
|
+
if (targetingContext?.userId !== undefined &&
|
|
16
24
|
parameters.Audience.Exclusion.Users !== undefined &&
|
|
17
|
-
parameters.Audience.Exclusion.Users.includes(
|
|
25
|
+
parameters.Audience.Exclusion.Users.includes(targetingContext.userId)) {
|
|
18
26
|
return false;
|
|
19
27
|
}
|
|
20
28
|
// check if the user is in a group within exclusion list
|
|
21
|
-
if (
|
|
29
|
+
if (targetingContext?.groups !== undefined &&
|
|
22
30
|
parameters.Audience.Exclusion.Groups !== undefined) {
|
|
23
31
|
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
|
|
24
|
-
if (
|
|
32
|
+
if (targetingContext.groups.includes(excludedGroup)) {
|
|
25
33
|
return false;
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
// check if the user is being targeted directly
|
|
31
|
-
if (
|
|
39
|
+
if (targetingContext?.userId !== undefined &&
|
|
32
40
|
parameters.Audience.Users !== undefined &&
|
|
33
|
-
parameters.Audience.Users.includes(
|
|
41
|
+
parameters.Audience.Users.includes(targetingContext.userId)) {
|
|
34
42
|
return true;
|
|
35
43
|
}
|
|
36
44
|
// check if the user is in a group that is being targeted
|
|
37
|
-
if (
|
|
45
|
+
if (targetingContext?.groups !== undefined &&
|
|
38
46
|
parameters.Audience.Groups !== undefined) {
|
|
39
47
|
for (const group of parameters.Audience.Groups) {
|
|
40
|
-
if (
|
|
48
|
+
if (targetingContext.groups.includes(group.Name)) {
|
|
41
49
|
const hint = `${featureName}\n${group.Name}`;
|
|
42
|
-
if (await isTargetedPercentile(
|
|
50
|
+
if (await isTargetedPercentile(targetingContext.userId, hint, 0, group.RolloutPercentage)) {
|
|
43
51
|
return true;
|
|
44
52
|
}
|
|
45
53
|
}
|
|
@@ -47,7 +55,7 @@ class TargetingFilter {
|
|
|
47
55
|
}
|
|
48
56
|
// check if the user is being targeted by a default rollout percentage
|
|
49
57
|
const hint = featureName;
|
|
50
|
-
return isTargetedPercentile(
|
|
58
|
+
return isTargetedPercentile(targetingContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
|
|
51
59
|
}
|
|
52
60
|
static #validateParameters(featureName, parameters) {
|
|
53
61
|
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TargetingFilter.js","sources":["../../../src/filter/TargetingFilter.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { IFeatureFilter } from \"./FeatureFilter.js\";\nimport { isTargetedPercentile } from \"../common/targetingEvaluator.js\";\nimport { ITargetingContext } from \"../common/
|
|
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, ITargetingContextAccessor } from \"../common/targetingContext.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 readonly name: string = \"Microsoft.Targeting\";\n readonly #targetingContextAccessor?: ITargetingContextAccessor;\n\n constructor(targetingContextAccessor?: ITargetingContextAccessor) {\n this.#targetingContextAccessor = targetingContextAccessor;\n }\n\n async evaluate(context: TargetingFilterEvaluationContext, appContext?: ITargetingContext): Promise<boolean> {\n const { featureName, parameters } = context;\n TargetingFilter.#validateParameters(featureName, parameters);\n\n let targetingContext: ITargetingContext | undefined;\n if (appContext?.userId !== undefined || appContext?.groups !== undefined) {\n targetingContext = appContext;\n } else if (this.#targetingContextAccessor !== undefined) {\n targetingContext = this.#targetingContextAccessor.getTargetingContext();\n }\n\n if (parameters.Audience.Exclusion !== undefined) {\n // check if the user is in the exclusion list\n if (targetingContext?.userId !== undefined &&\n parameters.Audience.Exclusion.Users !== undefined &&\n parameters.Audience.Exclusion.Users.includes(targetingContext.userId)) {\n return false;\n }\n // check if the user is in a group within exclusion list\n if (targetingContext?.groups !== undefined &&\n parameters.Audience.Exclusion.Groups !== undefined) {\n for (const excludedGroup of parameters.Audience.Exclusion.Groups) {\n if (targetingContext.groups.includes(excludedGroup)) {\n return false;\n }\n }\n }\n }\n\n // check if the user is being targeted directly\n if (targetingContext?.userId !== undefined &&\n parameters.Audience.Users !== undefined &&\n parameters.Audience.Users.includes(targetingContext.userId)) {\n return true;\n }\n\n // check if the user is in a group that is being targeted\n if (targetingContext?.groups !== undefined &&\n parameters.Audience.Groups !== undefined) {\n for (const group of parameters.Audience.Groups) {\n if (targetingContext.groups.includes(group.Name)) {\n const hint = `${featureName}\\n${group.Name}`;\n if (await isTargetedPercentile(targetingContext.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(targetingContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);\n }\n\n static #validateParameters(featureName: string, parameters: TargetingFilterParameters): void {\n if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {\n throw new Error(`Invalid feature flag: ${featureName}. 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(`Invalid feature flag: ${featureName}. RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`);\n }\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeWindowFilter.js","sources":["../../../src/filter/TimeWindowFilter.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { IFeatureFilter } from \"./FeatureFilter.js\";\n\n// [Start, End)\ntype TimeWindowParameters = {\n Start?: string;\n End?: string;\n}\n\ntype TimeWindowFilterEvaluationContext = {\n featureName: string;\n parameters: TimeWindowParameters;\n}\n\nexport class TimeWindowFilter implements IFeatureFilter {\n name: string = \"Microsoft.TimeWindow\";\n\n evaluate(context: TimeWindowFilterEvaluationContext): boolean {\n const {featureName, parameters} = context;\n const startTime = parameters.Start !== undefined ? new Date(parameters.Start) : undefined;\n const endTime = parameters.End !== undefined ? new Date(parameters.End) : undefined;\n\n if (startTime === undefined && endTime === undefined) {\n // If neither start nor end time is specified, then the filter is not applicable.\n console.warn(`The ${this.name} feature filter is not valid for feature ${featureName}. It must specify either 'Start', 'End', or both.`);\n return false;\n }\n const now = new Date();\n return (startTime === undefined || startTime <= now) && (endTime === undefined || now < endTime);\n }\n}\n"],"names":[],"mappings":"AAAA;AACA;MAea,gBAAgB,CAAA;
|
|
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 readonly 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;IAChB,IAAI,GAAW,sBAAsB,CAAC;AAE/C,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;;;;"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VariantAssignmentReason } from '../featureManager.js';
|
|
1
2
|
import { EVALUATION_EVENT_VERSION } from '../version.js';
|
|
2
3
|
|
|
3
4
|
// Copyright (c) Microsoft Corporation.
|
|
@@ -8,6 +9,8 @@ const ENABLED = "Enabled";
|
|
|
8
9
|
const TARGETING_ID = "TargetingId";
|
|
9
10
|
const VARIANT = "Variant";
|
|
10
11
|
const VARIANT_ASSIGNMENT_REASON = "VariantAssignmentReason";
|
|
12
|
+
const DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled";
|
|
13
|
+
const VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage";
|
|
11
14
|
function createFeatureEvaluationEventProperties(result) {
|
|
12
15
|
if (result.feature === undefined) {
|
|
13
16
|
return undefined;
|
|
@@ -21,6 +24,29 @@ function createFeatureEvaluationEventProperties(result) {
|
|
|
21
24
|
[VARIANT]: result.variant ? result.variant.name : "",
|
|
22
25
|
[VARIANT_ASSIGNMENT_REASON]: result.variantAssignmentReason,
|
|
23
26
|
};
|
|
27
|
+
if (result.feature.allocation?.default_when_enabled) {
|
|
28
|
+
eventProperties[DEFAULT_WHEN_ENABLED] = result.feature.allocation.default_when_enabled;
|
|
29
|
+
}
|
|
30
|
+
if (result.variantAssignmentReason === VariantAssignmentReason.DefaultWhenEnabled) {
|
|
31
|
+
let percentileAllocationPercentage = 0;
|
|
32
|
+
if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) {
|
|
33
|
+
for (const percentile of result.feature.allocation.percentile) {
|
|
34
|
+
percentileAllocationPercentage += percentile.to - percentile.from;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = (100 - percentileAllocationPercentage).toString();
|
|
38
|
+
}
|
|
39
|
+
else if (result.variantAssignmentReason === VariantAssignmentReason.Percentile) {
|
|
40
|
+
let percentileAllocationPercentage = 0;
|
|
41
|
+
if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) {
|
|
42
|
+
for (const percentile of result.feature.allocation.percentile) {
|
|
43
|
+
if (percentile.variant === result.variant.name) {
|
|
44
|
+
percentileAllocationPercentage += percentile.to - percentile.from;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = percentileAllocationPercentage.toString();
|
|
49
|
+
}
|
|
24
50
|
const metadata = result.feature.telemetry?.metadata;
|
|
25
51
|
if (metadata) {
|
|
26
52
|
for (const key in metadata) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"featureEvaluationEvent.js","sources":["../../../src/telemetry/featureEvaluationEvent.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { EvaluationResult } from \"../featureManager\";\nimport { EVALUATION_EVENT_VERSION } from \"../version.js\";\n\nconst VERSION = \"Version\";\nconst FEATURE_NAME = \"FeatureName\";\nconst ENABLED = \"Enabled\";\nconst TARGETING_ID = \"TargetingId\";\nconst VARIANT = \"Variant\";\nconst VARIANT_ASSIGNMENT_REASON = \"VariantAssignmentReason\";\n\nexport function createFeatureEvaluationEventProperties(result: EvaluationResult): any {\n if (result.feature === undefined) {\n return undefined;\n }\n\n const eventProperties = {\n [VERSION]: EVALUATION_EVENT_VERSION,\n [FEATURE_NAME]: result.feature ? result.feature.id : \"\",\n [ENABLED]: result.enabled ? \"True\" : \"False\",\n // Ensure targetingId is string so that it will be placed in customDimensions\n [TARGETING_ID]: result.targetingId ? result.targetingId.toString() : \"\",\n [VARIANT]: result.variant ? result.variant.name : \"\",\n [VARIANT_ASSIGNMENT_REASON]: result.variantAssignmentReason,\n };\n\n const metadata = result.feature.telemetry?.metadata;\n if (metadata) {\n for (const key in metadata) {\n if (!(key in eventProperties)) {\n eventProperties[key] = metadata[key];\n }\n }\n }\n\n return eventProperties;\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"featureEvaluationEvent.js","sources":["../../../src/telemetry/featureEvaluationEvent.ts"],"sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT license.\n\nimport { EvaluationResult, VariantAssignmentReason } from \"../featureManager\";\nimport { EVALUATION_EVENT_VERSION } from \"../version.js\";\n\nconst VERSION = \"Version\";\nconst FEATURE_NAME = \"FeatureName\";\nconst ENABLED = \"Enabled\";\nconst TARGETING_ID = \"TargetingId\";\nconst VARIANT = \"Variant\";\nconst VARIANT_ASSIGNMENT_REASON = \"VariantAssignmentReason\";\nconst DEFAULT_WHEN_ENABLED = \"DefaultWhenEnabled\";\nconst VARIANT_ASSIGNMENT_PERCENTAGE = \"VariantAssignmentPercentage\";\n\nexport function createFeatureEvaluationEventProperties(result: EvaluationResult): any {\n if (result.feature === undefined) {\n return undefined;\n }\n\n const eventProperties = {\n [VERSION]: EVALUATION_EVENT_VERSION,\n [FEATURE_NAME]: result.feature ? result.feature.id : \"\",\n [ENABLED]: result.enabled ? \"True\" : \"False\",\n // Ensure targetingId is string so that it will be placed in customDimensions\n [TARGETING_ID]: result.targetingId ? result.targetingId.toString() : \"\",\n [VARIANT]: result.variant ? result.variant.name : \"\",\n [VARIANT_ASSIGNMENT_REASON]: result.variantAssignmentReason,\n };\n\n if (result.feature.allocation?.default_when_enabled) {\n eventProperties[DEFAULT_WHEN_ENABLED] = result.feature.allocation.default_when_enabled;\n }\n\n if (result.variantAssignmentReason === VariantAssignmentReason.DefaultWhenEnabled) {\n let percentileAllocationPercentage = 0;\n if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) {\n for (const percentile of result.feature.allocation.percentile) {\n percentileAllocationPercentage += percentile.to - percentile.from;\n }\n }\n eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = (100 - percentileAllocationPercentage).toString();\n }\n else if (result.variantAssignmentReason === VariantAssignmentReason.Percentile) {\n let percentileAllocationPercentage = 0;\n if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) {\n for (const percentile of result.feature.allocation.percentile) {\n if (percentile.variant === result.variant.name) {\n percentileAllocationPercentage += percentile.to - percentile.from;\n }\n }\n }\n eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = percentileAllocationPercentage.toString();\n }\n\n const metadata = result.feature.telemetry?.metadata;\n if (metadata) {\n for (const key in metadata) {\n if (!(key in eventProperties)) {\n eventProperties[key] = metadata[key];\n }\n }\n }\n\n return eventProperties;\n}\n"],"names":[],"mappings":"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"}
|
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 = \"2.0.
|
|
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.1.0-preview.1\";\nexport const EVALUATION_EVENT_VERSION = \"1.0.0\";\n"],"names":[],"mappings":"AAAA;AACA;AAEO,MAAM,OAAO,GAAG,kBAAkB;AAClC,MAAM,wBAAwB,GAAG;;;;"}
|
package/dist/umd/index.js
CHANGED
|
@@ -139,42 +139,50 @@
|
|
|
139
139
|
// Licensed under the MIT license.
|
|
140
140
|
class TargetingFilter {
|
|
141
141
|
name = "Microsoft.Targeting";
|
|
142
|
+
#targetingContextAccessor;
|
|
143
|
+
constructor(targetingContextAccessor) {
|
|
144
|
+
this.#targetingContextAccessor = targetingContextAccessor;
|
|
145
|
+
}
|
|
142
146
|
async evaluate(context, appContext) {
|
|
143
147
|
const { featureName, parameters } = context;
|
|
144
148
|
TargetingFilter.#validateParameters(featureName, parameters);
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
let targetingContext;
|
|
150
|
+
if (appContext?.userId !== undefined || appContext?.groups !== undefined) {
|
|
151
|
+
targetingContext = appContext;
|
|
152
|
+
}
|
|
153
|
+
else if (this.#targetingContextAccessor !== undefined) {
|
|
154
|
+
targetingContext = this.#targetingContextAccessor.getTargetingContext();
|
|
147
155
|
}
|
|
148
156
|
if (parameters.Audience.Exclusion !== undefined) {
|
|
149
157
|
// check if the user is in the exclusion list
|
|
150
|
-
if (
|
|
158
|
+
if (targetingContext?.userId !== undefined &&
|
|
151
159
|
parameters.Audience.Exclusion.Users !== undefined &&
|
|
152
|
-
parameters.Audience.Exclusion.Users.includes(
|
|
160
|
+
parameters.Audience.Exclusion.Users.includes(targetingContext.userId)) {
|
|
153
161
|
return false;
|
|
154
162
|
}
|
|
155
163
|
// check if the user is in a group within exclusion list
|
|
156
|
-
if (
|
|
164
|
+
if (targetingContext?.groups !== undefined &&
|
|
157
165
|
parameters.Audience.Exclusion.Groups !== undefined) {
|
|
158
166
|
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
|
|
159
|
-
if (
|
|
167
|
+
if (targetingContext.groups.includes(excludedGroup)) {
|
|
160
168
|
return false;
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
}
|
|
165
173
|
// check if the user is being targeted directly
|
|
166
|
-
if (
|
|
174
|
+
if (targetingContext?.userId !== undefined &&
|
|
167
175
|
parameters.Audience.Users !== undefined &&
|
|
168
|
-
parameters.Audience.Users.includes(
|
|
176
|
+
parameters.Audience.Users.includes(targetingContext.userId)) {
|
|
169
177
|
return true;
|
|
170
178
|
}
|
|
171
179
|
// check if the user is in a group that is being targeted
|
|
172
|
-
if (
|
|
180
|
+
if (targetingContext?.groups !== undefined &&
|
|
173
181
|
parameters.Audience.Groups !== undefined) {
|
|
174
182
|
for (const group of parameters.Audience.Groups) {
|
|
175
|
-
if (
|
|
183
|
+
if (targetingContext.groups.includes(group.Name)) {
|
|
176
184
|
const hint = `${featureName}\n${group.Name}`;
|
|
177
|
-
if (await isTargetedPercentile(
|
|
185
|
+
if (await isTargetedPercentile(targetingContext.userId, hint, 0, group.RolloutPercentage)) {
|
|
178
186
|
return true;
|
|
179
187
|
}
|
|
180
188
|
}
|
|
@@ -182,7 +190,7 @@
|
|
|
182
190
|
}
|
|
183
191
|
// check if the user is being targeted by a default rollout percentage
|
|
184
192
|
const hint = featureName;
|
|
185
|
-
return isTargetedPercentile(
|
|
193
|
+
return isTargetedPercentile(targetingContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
|
|
186
194
|
}
|
|
187
195
|
static #validateParameters(featureName, parameters) {
|
|
188
196
|
if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) {
|
|
@@ -216,14 +224,16 @@
|
|
|
216
224
|
#provider;
|
|
217
225
|
#featureFilters = new Map();
|
|
218
226
|
#onFeatureEvaluated;
|
|
227
|
+
#targetingContextAccessor;
|
|
219
228
|
constructor(provider, options) {
|
|
220
229
|
this.#provider = provider;
|
|
221
|
-
|
|
230
|
+
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
|
|
231
|
+
this.#targetingContextAccessor = options?.targetingContextAccessor;
|
|
232
|
+
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter(options?.targetingContextAccessor)];
|
|
222
233
|
// If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.
|
|
223
234
|
for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
|
|
224
235
|
this.#featureFilters.set(filter.name, filter);
|
|
225
236
|
}
|
|
226
|
-
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
|
|
227
237
|
}
|
|
228
238
|
async listFeatureNames() {
|
|
229
239
|
const features = await this.#provider.getFeatureFlags();
|
|
@@ -267,7 +277,7 @@
|
|
|
267
277
|
}
|
|
268
278
|
return { variant: undefined, reason: exports.VariantAssignmentReason.None };
|
|
269
279
|
}
|
|
270
|
-
async #isEnabled(featureFlag,
|
|
280
|
+
async #isEnabled(featureFlag, appContext) {
|
|
271
281
|
if (featureFlag.enabled !== true) {
|
|
272
282
|
// If the feature is not explicitly enabled, then it is disabled by default.
|
|
273
283
|
return false;
|
|
@@ -291,14 +301,14 @@
|
|
|
291
301
|
console.warn(`Feature filter ${clientFilter.name} is not found.`);
|
|
292
302
|
return false;
|
|
293
303
|
}
|
|
294
|
-
if (await matchedFeatureFilter.evaluate(contextWithFeatureName,
|
|
304
|
+
if (await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext) === shortCircuitEvaluationResult) {
|
|
295
305
|
return shortCircuitEvaluationResult;
|
|
296
306
|
}
|
|
297
307
|
}
|
|
298
308
|
// If we get here, then we have not found a client filter that matches the requirement type.
|
|
299
309
|
return !shortCircuitEvaluationResult;
|
|
300
310
|
}
|
|
301
|
-
async #evaluateFeature(featureName,
|
|
311
|
+
async #evaluateFeature(featureName, appContext) {
|
|
302
312
|
const featureFlag = await this.#provider.getFeatureFlag(featureName);
|
|
303
313
|
const result = new EvaluationResult(featureFlag);
|
|
304
314
|
if (featureFlag === undefined) {
|
|
@@ -308,8 +318,9 @@
|
|
|
308
318
|
// TODO: move to the feature flag provider implementation.
|
|
309
319
|
validateFeatureFlagFormat(featureFlag);
|
|
310
320
|
// Evaluate if the feature is enabled.
|
|
311
|
-
result.enabled = await this.#isEnabled(featureFlag,
|
|
312
|
-
|
|
321
|
+
result.enabled = await this.#isEnabled(featureFlag, appContext);
|
|
322
|
+
// Get targeting context from the app context or the targeting context accessor
|
|
323
|
+
const targetingContext = this.#getTargetingContext(appContext);
|
|
313
324
|
result.targetingId = targetingContext?.userId;
|
|
314
325
|
// Determine Variant
|
|
315
326
|
let variantDef;
|
|
@@ -330,7 +341,7 @@
|
|
|
330
341
|
}
|
|
331
342
|
else {
|
|
332
343
|
// enabled, assign based on allocation
|
|
333
|
-
if (
|
|
344
|
+
if (targetingContext !== undefined && featureFlag.allocation !== undefined) {
|
|
334
345
|
const variantAndReason = await this.#assignVariant(featureFlag, targetingContext);
|
|
335
346
|
variantDef = variantAndReason.variant;
|
|
336
347
|
reason = variantAndReason.reason;
|
|
@@ -365,6 +376,15 @@
|
|
|
365
376
|
}
|
|
366
377
|
return result;
|
|
367
378
|
}
|
|
379
|
+
#getTargetingContext(context) {
|
|
380
|
+
let targetingContext = context;
|
|
381
|
+
if (targetingContext?.userId === undefined &&
|
|
382
|
+
targetingContext?.groups === undefined &&
|
|
383
|
+
this.#targetingContextAccessor !== undefined) {
|
|
384
|
+
targetingContext = this.#targetingContextAccessor.getTargetingContext();
|
|
385
|
+
}
|
|
386
|
+
return targetingContext;
|
|
387
|
+
}
|
|
368
388
|
}
|
|
369
389
|
class EvaluationResult {
|
|
370
390
|
feature;
|
|
@@ -676,7 +696,7 @@
|
|
|
676
696
|
|
|
677
697
|
// Copyright (c) Microsoft Corporation.
|
|
678
698
|
// Licensed under the MIT license.
|
|
679
|
-
const VERSION$1 = "2.0.
|
|
699
|
+
const VERSION$1 = "2.1.0-preview.1";
|
|
680
700
|
const EVALUATION_EVENT_VERSION = "1.0.0";
|
|
681
701
|
|
|
682
702
|
// Copyright (c) Microsoft Corporation.
|
|
@@ -687,6 +707,8 @@
|
|
|
687
707
|
const TARGETING_ID = "TargetingId";
|
|
688
708
|
const VARIANT = "Variant";
|
|
689
709
|
const VARIANT_ASSIGNMENT_REASON = "VariantAssignmentReason";
|
|
710
|
+
const DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled";
|
|
711
|
+
const VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage";
|
|
690
712
|
function createFeatureEvaluationEventProperties(result) {
|
|
691
713
|
if (result.feature === undefined) {
|
|
692
714
|
return undefined;
|
|
@@ -700,6 +722,29 @@
|
|
|
700
722
|
[VARIANT]: result.variant ? result.variant.name : "",
|
|
701
723
|
[VARIANT_ASSIGNMENT_REASON]: result.variantAssignmentReason,
|
|
702
724
|
};
|
|
725
|
+
if (result.feature.allocation?.default_when_enabled) {
|
|
726
|
+
eventProperties[DEFAULT_WHEN_ENABLED] = result.feature.allocation.default_when_enabled;
|
|
727
|
+
}
|
|
728
|
+
if (result.variantAssignmentReason === exports.VariantAssignmentReason.DefaultWhenEnabled) {
|
|
729
|
+
let percentileAllocationPercentage = 0;
|
|
730
|
+
if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) {
|
|
731
|
+
for (const percentile of result.feature.allocation.percentile) {
|
|
732
|
+
percentileAllocationPercentage += percentile.to - percentile.from;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = (100 - percentileAllocationPercentage).toString();
|
|
736
|
+
}
|
|
737
|
+
else if (result.variantAssignmentReason === exports.VariantAssignmentReason.Percentile) {
|
|
738
|
+
let percentileAllocationPercentage = 0;
|
|
739
|
+
if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) {
|
|
740
|
+
for (const percentile of result.feature.allocation.percentile) {
|
|
741
|
+
if (percentile.variant === result.variant.name) {
|
|
742
|
+
percentileAllocationPercentage += percentile.to - percentile.from;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = percentileAllocationPercentage.toString();
|
|
747
|
+
}
|
|
703
748
|
const metadata = result.feature.telemetry?.metadata;
|
|
704
749
|
if (metadata) {
|
|
705
750
|
for (const key in metadata) {
|