Skip to content

Commit f7a306a

Browse files
support targeting context accessor
1 parent 42e48b5 commit f7a306a

File tree

6 files changed

+28
-25
lines changed

6 files changed

+28
-25
lines changed

src/feature-management/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/feature-management/src/IFeatureManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { ITargetingContext } from "./common/ITargetingContext";
4+
import { ITargetingContext } from "./common/targetingContext";
55
import { Variant } from "./variant/Variant";
66

77
export interface IFeatureManager {

src/feature-management/src/common/ITargetingContext.ts renamed to src/feature-management/src/common/targetingContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export interface ITargetingContext {
66
groups?: string[];
77
}
88

9+
export type TargetingContextAccessor = () => ITargetingContext;

src/feature-management/src/featureManager.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { IFeatureFlagProvider } from "./featureProvider.js";
88
import { TargetingFilter } from "./filter/TargetingFilter.js";
99
import { Variant } from "./variant/Variant.js";
1010
import { IFeatureManager } from "./IFeatureManager.js";
11-
import { ITargetingContext } from "./common/ITargetingContext.js";
11+
import { ITargetingContext, TargetingContextAccessor } from "./common/targetingContext.js";
1212
import { isTargetedGroup, isTargetedPercentile, isTargetedUser } from "./common/targetingEvaluator.js";
1313

1414
export class FeatureManager implements IFeatureManager {
1515
#provider: IFeatureFlagProvider;
1616
#featureFilters: Map<string, IFeatureFilter> = new Map();
1717
#onFeatureEvaluated?: (event: EvaluationResult) => void;
18+
#targetingContextAccessor?: TargetingContextAccessor;
1819

1920
constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) {
2021
this.#provider = provider;
@@ -27,6 +28,7 @@ export class FeatureManager implements IFeatureManager {
2728
}
2829

2930
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
31+
this.#targetingContextAccessor = options?.targetingContextAccessor;
3032
}
3133

3234
async listFeatureNames(): Promise<string[]> {
@@ -102,11 +104,19 @@ export class FeatureManager implements IFeatureManager {
102104
for (const clientFilter of clientFilters) {
103105
const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
104106
const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters };
107+
let clientFilterEvaluationResult: boolean;
105108
if (matchedFeatureFilter === undefined) {
106109
console.warn(`Feature filter ${clientFilter.name} is not found.`);
107-
return false;
110+
clientFilterEvaluationResult = false;
108111
}
109-
if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) {
112+
else {
113+
let appContext = context;
114+
if (clientFilter.name === "Microsoft.Targeting" && this.#targetingContextAccessor !== undefined) {
115+
appContext = this.#targetingContextAccessor();
116+
}
117+
clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext);
118+
}
119+
if (clientFilterEvaluationResult === shortCircuitEvaluationResult) {
110120
return shortCircuitEvaluationResult;
111121
}
112122
}
@@ -130,7 +140,10 @@ export class FeatureManager implements IFeatureManager {
130140
// Evaluate if the feature is enabled.
131141
result.enabled = await this.#isEnabled(featureFlag, context);
132142

133-
const targetingContext = context as ITargetingContext;
143+
let targetingContext = context as ITargetingContext;
144+
if (this.#targetingContextAccessor !== undefined) {
145+
targetingContext = this.#targetingContextAccessor();
146+
}
134147
result.targetingId = targetingContext?.userId;
135148

136149
// Determine Variant
@@ -151,7 +164,7 @@ export class FeatureManager implements IFeatureManager {
151164
}
152165
} else {
153166
// enabled, assign based on allocation
154-
if (context !== undefined && featureFlag.allocation !== undefined) {
167+
if (targetingContext !== undefined && featureFlag.allocation !== undefined) {
155168
const variantAndReason = await this.#assignVariant(featureFlag, targetingContext);
156169
variantDef = variantAndReason.variant;
157170
reason = variantAndReason.reason;
@@ -202,6 +215,11 @@ export interface FeatureManagerOptions {
202215
* The callback function is called only when telemetry is enabled for the feature flag.
203216
*/
204217
onFeatureEvaluated?: (event: EvaluationResult) => void;
218+
219+
/**
220+
* The accessor function that provides the @see ITargetingContext for targeting evaluation.
221+
*/
222+
targetingContextAccessor?: TargetingContextAccessor;
205223
}
206224

207225
export class EvaluationResult {

src/feature-management/src/filter/TargetingFilter.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { IFeatureFilter } from "./FeatureFilter.js";
55
import { isTargetedPercentile } from "../common/targetingEvaluator.js";
6-
import { ITargetingContext } from "../common/ITargetingContext.js";
6+
import { ITargetingContext } from "../common/targetingContext.js";
77

88
type TargetingFilterParameters = {
99
Audience: {
@@ -32,10 +32,6 @@ export class TargetingFilter implements IFeatureFilter {
3232
const { featureName, parameters } = context;
3333
TargetingFilter.#validateParameters(featureName, parameters);
3434

35-
if (appContext === undefined) {
36-
throw new Error("The app context is required for targeting filter.");
37-
}
38-
3935
if (parameters.Audience.Exclusion !== undefined) {
4036
// check if the user is in the exclusion list
4137
if (appContext?.userId !== undefined &&

src/feature-management/test/targetingFilter.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,4 @@ describe("targeting filter", () => {
130130
expect(featureManager.isEnabled("ComplexTargeting", { userId: "Dave", groups: ["Stage1"] })).eventually.eq(false, "Dave is excluded because he is in the exclusion list"),
131131
]);
132132
});
133-
134-
it("should throw error if app context is not provided", () => {
135-
const dataSource = new Map();
136-
dataSource.set("feature_management", {
137-
feature_flags: [complexTargetingFeature]
138-
});
139-
140-
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
141-
const featureManager = new FeatureManager(provider);
142-
143-
return expect(featureManager.isEnabled("ComplexTargeting")).eventually.rejectedWith("The app context is required for targeting filter.");
144-
});
145133
});

0 commit comments

Comments
 (0)