Skip to content

Commit cee2d95

Browse files
Merge branch 'zhiyuanliang/targeting-context-accessor' of https://github.com/microsoft/FeatureManagement-JavaScript into zhiyuanliang/ambient-targeting-example
2 parents f02ea6e + 73353fa commit cee2d95

File tree

7 files changed

+59
-42
lines changed

7 files changed

+59
-42
lines changed

src/feature-management/src/common/targetingContext.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export interface ITargetingContext {
1616
}
1717

1818
/**
19-
* Type definition for a function that, when invoked, returns the @see ITargetingContext for targeting evaluation.
19+
* Provides access to the current targeting context.
2020
*/
21-
export type TargetingContextAccessor = () => ITargetingContext;
21+
export interface ITargetingContextAccessor {
22+
/**
23+
* Retrieves the current targeting context.
24+
*/
25+
getTargetingContext: () => ITargetingContext | undefined;
26+
}

src/feature-management/src/featureManager.ts

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,25 @@ 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, TargetingContextAccessor } from "./common/targetingContext.js";
11+
import { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js";
1212
import { isTargetedGroup, isTargetedPercentile, isTargetedUser } from "./common/targetingEvaluator.js";
1313

1414
export class FeatureManager implements IFeatureManager {
15-
#provider: IFeatureFlagProvider;
16-
#featureFilters: Map<string, IFeatureFilter> = new Map();
17-
#onFeatureEvaluated?: (event: EvaluationResult) => void;
18-
#targetingContextAccessor?: TargetingContextAccessor;
15+
readonly #provider: IFeatureFlagProvider;
16+
readonly #featureFilters: Map<string, IFeatureFilter> = new Map();
17+
readonly #onFeatureEvaluated?: (event: EvaluationResult) => void;
18+
readonly #targetingContextAccessor?: ITargetingContextAccessor;
1919

2020
constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) {
2121
this.#provider = provider;
22+
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
23+
this.#targetingContextAccessor = options?.targetingContextAccessor;
2224

23-
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()];
24-
25+
const builtinFilters = [new TimeWindowFilter(), new TargetingFilter(options?.targetingContextAccessor)];
2526
// If a custom filter shares a name with an existing filter, the custom filter overrides the existing one.
2627
for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) {
2728
this.#featureFilters.set(filter.name, filter);
2829
}
29-
30-
this.#onFeatureEvaluated = options?.onFeatureEvaluated;
31-
this.#targetingContextAccessor = options?.targetingContextAccessor;
3230
}
3331

3432
async listFeatureNames(): Promise<string[]> {
@@ -104,19 +102,11 @@ export class FeatureManager implements IFeatureManager {
104102
for (const clientFilter of clientFilters) {
105103
const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name);
106104
const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters };
107-
let clientFilterEvaluationResult: boolean;
108105
if (matchedFeatureFilter === undefined) {
109106
console.warn(`Feature filter ${clientFilter.name} is not found.`);
110-
clientFilterEvaluationResult = false;
111-
}
112-
else {
113-
if (clientFilter.name === "Microsoft.Targeting") {
114-
clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, this.#getTargetingContext(appContext));
115-
} else {
116-
clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext);
117-
}
107+
return false;
118108
}
119-
if (clientFilterEvaluationResult === shortCircuitEvaluationResult) {
109+
if (await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext) === shortCircuitEvaluationResult) {
120110
return shortCircuitEvaluationResult;
121111
}
122112
}
@@ -202,9 +192,11 @@ export class FeatureManager implements IFeatureManager {
202192
}
203193

204194
#getTargetingContext(context: unknown): ITargetingContext | undefined {
205-
let targetingContext = context as ITargetingContext;
206-
if (targetingContext === undefined && this.#targetingContextAccessor !== undefined) {
207-
targetingContext = this.#targetingContextAccessor();
195+
let targetingContext: ITargetingContext | undefined = context as ITargetingContext;
196+
if (targetingContext?.userId === undefined &&
197+
targetingContext?.groups === undefined &&
198+
this.#targetingContextAccessor !== undefined) {
199+
targetingContext = this.#targetingContextAccessor.getTargetingContext();
208200
}
209201
return targetingContext;
210202
}
@@ -225,7 +217,7 @@ export interface FeatureManagerOptions {
225217
/**
226218
* The accessor function that provides the @see ITargetingContext for targeting evaluation.
227219
*/
228-
targetingContextAccessor?: TargetingContextAccessor;
220+
targetingContextAccessor?: ITargetingContextAccessor;
229221
}
230222

231223
export class EvaluationResult {

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

Lines changed: 24 additions & 12 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/targetingContext.js";
6+
import { ITargetingContext, ITargetingContextAccessor } from "../common/targetingContext.js";
77

88
type TargetingFilterParameters = {
99
Audience: {
@@ -26,44 +26,56 @@ type TargetingFilterEvaluationContext = {
2626
}
2727

2828
export class TargetingFilter implements IFeatureFilter {
29-
name: string = "Microsoft.Targeting";
29+
readonly name: string = "Microsoft.Targeting";
30+
readonly #targetingContextAccessor?: ITargetingContextAccessor;
31+
32+
constructor(targetingContextAccessor?: ITargetingContextAccessor) {
33+
this.#targetingContextAccessor = targetingContextAccessor;
34+
}
3035

3136
async evaluate(context: TargetingFilterEvaluationContext, appContext?: ITargetingContext): Promise<boolean> {
3237
const { featureName, parameters } = context;
3338
TargetingFilter.#validateParameters(featureName, parameters);
3439

40+
let targetingContext: ITargetingContext | undefined;
41+
if (appContext?.userId !== undefined || appContext?.groups !== undefined) {
42+
targetingContext = appContext;
43+
} else if (this.#targetingContextAccessor !== undefined) {
44+
targetingContext = this.#targetingContextAccessor.getTargetingContext();
45+
}
46+
3547
if (parameters.Audience.Exclusion !== undefined) {
3648
// check if the user is in the exclusion list
37-
if (appContext?.userId !== undefined &&
49+
if (targetingContext?.userId !== undefined &&
3850
parameters.Audience.Exclusion.Users !== undefined &&
39-
parameters.Audience.Exclusion.Users.includes(appContext.userId)) {
51+
parameters.Audience.Exclusion.Users.includes(targetingContext.userId)) {
4052
return false;
4153
}
4254
// check if the user is in a group within exclusion list
43-
if (appContext?.groups !== undefined &&
55+
if (targetingContext?.groups !== undefined &&
4456
parameters.Audience.Exclusion.Groups !== undefined) {
4557
for (const excludedGroup of parameters.Audience.Exclusion.Groups) {
46-
if (appContext.groups.includes(excludedGroup)) {
58+
if (targetingContext.groups.includes(excludedGroup)) {
4759
return false;
4860
}
4961
}
5062
}
5163
}
5264

5365
// check if the user is being targeted directly
54-
if (appContext?.userId !== undefined &&
66+
if (targetingContext?.userId !== undefined &&
5567
parameters.Audience.Users !== undefined &&
56-
parameters.Audience.Users.includes(appContext.userId)) {
68+
parameters.Audience.Users.includes(targetingContext.userId)) {
5769
return true;
5870
}
5971

6072
// check if the user is in a group that is being targeted
61-
if (appContext?.groups !== undefined &&
73+
if (targetingContext?.groups !== undefined &&
6274
parameters.Audience.Groups !== undefined) {
6375
for (const group of parameters.Audience.Groups) {
64-
if (appContext.groups.includes(group.Name)) {
76+
if (targetingContext.groups.includes(group.Name)) {
6577
const hint = `${featureName}\n${group.Name}`;
66-
if (await isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) {
78+
if (await isTargetedPercentile(targetingContext.userId, hint, 0, group.RolloutPercentage)) {
6779
return true;
6880
}
6981
}
@@ -72,7 +84,7 @@ export class TargetingFilter implements IFeatureFilter {
7284

7385
// check if the user is being targeted by a default rollout percentage
7486
const hint = featureName;
75-
return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
87+
return isTargetedPercentile(targetingContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage);
7688
}
7789

7890
static #validateParameters(featureName: string, parameters: TargetingFilterParameters): void {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type TimeWindowFilterEvaluationContext = {
1515
}
1616

1717
export class TimeWindowFilter implements IFeatureFilter {
18-
name: string = "Microsoft.TimeWindow";
18+
readonly name: string = "Microsoft.TimeWindow";
1919

2020
evaluate(context: TimeWindowFilterEvaluationContext): boolean {
2121
const {featureName, parameters} = context;

src/feature-management/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ export { FeatureManager, FeatureManagerOptions, EvaluationResult, VariantAssignm
55
export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider.js";
66
export { createFeatureEvaluationEventProperties } from "./telemetry/featureEvaluationEvent.js";
77
export { IFeatureFilter } from "./filter/FeatureFilter.js";
8-
export { TargetingContextAccessor, ITargetingContext } from "./common/targetingContext.js";
8+
export { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js";
99
export { VERSION } from "./version.js";

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ describe("targeting filter", () => {
139139

140140
let userId = "";
141141
let groups: string[] = [];
142-
const testTargetingContextAccessor = () => ({ userId: userId, groups: groups });
142+
const testTargetingContextAccessor = {
143+
getTargetingContext: () => {
144+
return { userId: userId, groups: groups };
145+
}
146+
};
143147
const provider = new ConfigurationMapFeatureFlagProvider(dataSource);
144148
const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor});
145149

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ describe("variant assignment with targeting context accessor", () => {
9696
it("should assign variant based on targeting context accessor", async () => {
9797
let userId = "";
9898
let groups: string[] = [];
99-
const testTargetingContextAccessor = () => ({ userId: userId, groups: groups });
99+
const testTargetingContextAccessor = {
100+
getTargetingContext: () => {
101+
return { userId: userId, groups: groups };
102+
}
103+
};
100104
const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject);
101105
const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor});
102106
userId = "Marsha";

0 commit comments

Comments
 (0)