From f7a306a33c1a3cb1eb1bbda0d29109c35aeb86a9 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 6 Feb 2025 15:31:50 +0800 Subject: [PATCH 01/16] support targeting context accessor --- src/feature-management/package-lock.json | 4 +-- src/feature-management/src/IFeatureManager.ts | 2 +- ...argetingContext.ts => targetingContext.ts} | 1 + src/feature-management/src/featureManager.ts | 28 +++++++++++++++---- .../src/filter/TargetingFilter.ts | 6 +--- .../test/targetingFilter.test.ts | 12 -------- 6 files changed, 28 insertions(+), 25 deletions(-) rename src/feature-management/src/common/{ITargetingContext.ts => targetingContext.ts} (71%) diff --git a/src/feature-management/package-lock.json b/src/feature-management/package-lock.json index 74c975d..f8c9a0a 100644 --- a/src/feature-management/package-lock.json +++ b/src/feature-management/package-lock.json @@ -20,7 +20,7 @@ "eslint": "^8.56.0", "mocha": "^10.2.0", "rimraf": "^5.0.5", - "rollup": "^4.9.4", + "rollup": "^4.22.4", "rollup-plugin-dts": "^6.1.0", "tslib": "^2.6.2", "typescript": "^5.3.3" @@ -3212,4 +3212,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/feature-management/src/IFeatureManager.ts b/src/feature-management/src/IFeatureManager.ts index d673dce..f982a6c 100644 --- a/src/feature-management/src/IFeatureManager.ts +++ b/src/feature-management/src/IFeatureManager.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ITargetingContext } from "./common/ITargetingContext"; +import { ITargetingContext } from "./common/targetingContext"; import { Variant } from "./variant/Variant"; export interface IFeatureManager { diff --git a/src/feature-management/src/common/ITargetingContext.ts b/src/feature-management/src/common/targetingContext.ts similarity index 71% rename from src/feature-management/src/common/ITargetingContext.ts rename to src/feature-management/src/common/targetingContext.ts index 1d5a426..ccc8dfb 100644 --- a/src/feature-management/src/common/ITargetingContext.ts +++ b/src/feature-management/src/common/targetingContext.ts @@ -6,3 +6,4 @@ export interface ITargetingContext { groups?: string[]; } +export type TargetingContextAccessor = () => ITargetingContext; diff --git a/src/feature-management/src/featureManager.ts b/src/feature-management/src/featureManager.ts index a035b5d..9c2581f 100644 --- a/src/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -8,13 +8,14 @@ import { IFeatureFlagProvider } from "./featureProvider.js"; import { TargetingFilter } from "./filter/TargetingFilter.js"; import { Variant } from "./variant/Variant.js"; import { IFeatureManager } from "./IFeatureManager.js"; -import { ITargetingContext } from "./common/ITargetingContext.js"; +import { ITargetingContext, TargetingContextAccessor } from "./common/targetingContext.js"; import { isTargetedGroup, isTargetedPercentile, isTargetedUser } from "./common/targetingEvaluator.js"; export class FeatureManager implements IFeatureManager { #provider: IFeatureFlagProvider; #featureFilters: Map = new Map(); #onFeatureEvaluated?: (event: EvaluationResult) => void; + #targetingContextAccessor?: TargetingContextAccessor; constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) { this.#provider = provider; @@ -27,6 +28,7 @@ export class FeatureManager implements IFeatureManager { } this.#onFeatureEvaluated = options?.onFeatureEvaluated; + this.#targetingContextAccessor = options?.targetingContextAccessor; } async listFeatureNames(): Promise { @@ -102,11 +104,19 @@ export class FeatureManager implements IFeatureManager { for (const clientFilter of clientFilters) { const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name); const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters }; + let clientFilterEvaluationResult: boolean; if (matchedFeatureFilter === undefined) { console.warn(`Feature filter ${clientFilter.name} is not found.`); - return false; + clientFilterEvaluationResult = false; } - if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) { + else { + let appContext = context; + if (clientFilter.name === "Microsoft.Targeting" && this.#targetingContextAccessor !== undefined) { + appContext = this.#targetingContextAccessor(); + } + clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext); + } + if (clientFilterEvaluationResult === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; } } @@ -130,7 +140,10 @@ export class FeatureManager implements IFeatureManager { // Evaluate if the feature is enabled. result.enabled = await this.#isEnabled(featureFlag, context); - const targetingContext = context as ITargetingContext; + let targetingContext = context as ITargetingContext; + if (this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor(); + } result.targetingId = targetingContext?.userId; // Determine Variant @@ -151,7 +164,7 @@ export class FeatureManager implements IFeatureManager { } } else { // enabled, assign based on allocation - if (context !== undefined && featureFlag.allocation !== undefined) { + if (targetingContext !== undefined && featureFlag.allocation !== undefined) { const variantAndReason = await this.#assignVariant(featureFlag, targetingContext); variantDef = variantAndReason.variant; reason = variantAndReason.reason; @@ -202,6 +215,11 @@ export interface FeatureManagerOptions { * The callback function is called only when telemetry is enabled for the feature flag. */ onFeatureEvaluated?: (event: EvaluationResult) => void; + + /** + * The accessor function that provides the @see ITargetingContext for targeting evaluation. + */ + targetingContextAccessor?: TargetingContextAccessor; } export class EvaluationResult { diff --git a/src/feature-management/src/filter/TargetingFilter.ts b/src/feature-management/src/filter/TargetingFilter.ts index 2d7220e..e6da2f7 100644 --- a/src/feature-management/src/filter/TargetingFilter.ts +++ b/src/feature-management/src/filter/TargetingFilter.ts @@ -3,7 +3,7 @@ import { IFeatureFilter } from "./FeatureFilter.js"; import { isTargetedPercentile } from "../common/targetingEvaluator.js"; -import { ITargetingContext } from "../common/ITargetingContext.js"; +import { ITargetingContext } from "../common/targetingContext.js"; type TargetingFilterParameters = { Audience: { @@ -32,10 +32,6 @@ export class TargetingFilter implements IFeatureFilter { const { featureName, parameters } = context; TargetingFilter.#validateParameters(featureName, parameters); - if (appContext === undefined) { - throw new Error("The app context is required for targeting filter."); - } - if (parameters.Audience.Exclusion !== undefined) { // check if the user is in the exclusion list if (appContext?.userId !== undefined && diff --git a/src/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts index 91fe81b..4e92c42 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -130,16 +130,4 @@ describe("targeting filter", () => { expect(featureManager.isEnabled("ComplexTargeting", { userId: "Dave", groups: ["Stage1"] })).eventually.eq(false, "Dave is excluded because he is in the exclusion list"), ]); }); - - it("should throw error if app context is not provided", () => { - const dataSource = new Map(); - dataSource.set("feature_management", { - feature_flags: [complexTargetingFeature] - }); - - const provider = new ConfigurationMapFeatureFlagProvider(dataSource); - const featureManager = new FeatureManager(provider); - - return expect(featureManager.isEnabled("ComplexTargeting")).eventually.rejectedWith("The app context is required for targeting filter."); - }); }); From aa548186182a3be3f3b253989cd51aa2addb832c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 6 Feb 2025 16:08:19 +0800 Subject: [PATCH 02/16] add test --- .../test/targetingFilter.test.ts | 24 +++++++++++++++++++ src/feature-management/test/variant.test.ts | 18 ++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts index 4e92c42..210972e 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -130,4 +130,28 @@ describe("targeting filter", () => { expect(featureManager.isEnabled("ComplexTargeting", { userId: "Dave", groups: ["Stage1"] })).eventually.eq(false, "Dave is excluded because he is in the exclusion list"), ]); }); + + it("should evaluate feature with targeting filter with targeting context accessor", async () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [complexTargetingFeature] + }); + + let userId = ""; + let groups: string[] = []; + const testTargetingContextAccessor = () => ({ userId, groups }); + const provider = new ConfigurationMapFeatureFlagProvider(dataSource); + const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); + + userId = "Aiden"; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(false); + userId = "Blossom"; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(true); + expect(await featureManager.isEnabled("ComplexTargeting", {userId: "Aiden"})).to.eq(true); // targeting id will be overridden by the context accessor + userId = "Aiden"; + groups = ["Stage2"]; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(true); + userId = "Chris"; + expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(false); + }); }); diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index ddfd90f..0484bf7 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -90,5 +90,23 @@ describe("feature variant", () => { it("throw exception for invalid doubles From and To in the Percentile section"); }); +}); +describe("variant assignment with targeting context accessor", () => { + it("should assign variant based on targeting context accessor", async () => { + let userId = ""; + let groups: string[] = []; + const testTargetingContextAccessor = () => ({ userId, groups }); + const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject); + const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); + userId = "Marsha"; + let variant = await featureManager.getVariant(Features.VariantFeatureUser); + expect(variant).not.to.be.undefined; + expect(variant?.name).eq("Small"); + userId = "Jeff"; + variant = await featureManager.getVariant(Features.VariantFeatureUser); + expect(variant).to.be.undefined; + variant = await featureManager.getVariant(Features.VariantFeatureUser, {userId: "Marsha"}); // targeting id will be overridden by the context accessor + expect(variant).to.be.undefined; + }); }); From 7e2652474f19e08b9c77b9736ccc13adaac6f150 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 6 Feb 2025 16:14:59 +0800 Subject: [PATCH 03/16] fix lint --- src/feature-management/src/featureManager.ts | 2 +- src/feature-management/test/variant.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/feature-management/src/featureManager.ts b/src/feature-management/src/featureManager.ts index 9c2581f..1d2d76c 100644 --- a/src/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -114,7 +114,7 @@ export class FeatureManager implements IFeatureManager { if (clientFilter.name === "Microsoft.Targeting" && this.#targetingContextAccessor !== undefined) { appContext = this.#targetingContextAccessor(); } - clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext); + clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext); } if (clientFilterEvaluationResult === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index 0484bf7..a610290 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -95,7 +95,7 @@ describe("feature variant", () => { describe("variant assignment with targeting context accessor", () => { it("should assign variant based on targeting context accessor", async () => { let userId = ""; - let groups: string[] = []; + const groups: string[] = []; const testTargetingContextAccessor = () => ({ userId, groups }); const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); From 0c7c1a52f2be5e83c61ad59b29c75c605c12d5cb Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 6 Feb 2025 16:18:26 +0800 Subject: [PATCH 04/16] update --- src/feature-management/test/targetingFilter.test.ts | 2 +- src/feature-management/test/variant.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts index 210972e..c6a9e22 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -139,7 +139,7 @@ describe("targeting filter", () => { let userId = ""; let groups: string[] = []; - const testTargetingContextAccessor = () => ({ userId, groups }); + const testTargetingContextAccessor = () => ({ userId: userId, groups: groups }); const provider = new ConfigurationMapFeatureFlagProvider(dataSource); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index a610290..b1b746f 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -95,8 +95,8 @@ describe("feature variant", () => { describe("variant assignment with targeting context accessor", () => { it("should assign variant based on targeting context accessor", async () => { let userId = ""; - const groups: string[] = []; - const testTargetingContextAccessor = () => ({ userId, groups }); + let groups: string[] = []; + const testTargetingContextAccessor = () => ({ userId: userId, groups: groups }); const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); userId = "Marsha"; From f612aadcab819e77c3cb000d377f7e40e2627c28 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 6 Feb 2025 16:22:25 +0800 Subject: [PATCH 05/16] update --- src/feature-management/test/variant.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index b1b746f..31d41f8 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -108,5 +108,9 @@ describe("variant assignment with targeting context accessor", () => { expect(variant).to.be.undefined; variant = await featureManager.getVariant(Features.VariantFeatureUser, {userId: "Marsha"}); // targeting id will be overridden by the context accessor expect(variant).to.be.undefined; + groups = ["Group1"]; + variant = await featureManager.getVariant(Features.VariantFeatureGroup); + expect(variant).not.to.be.undefined; + expect(variant?.name).eq("Small"); }); }); From 730bb2e34646aa89ef84a1415c2e09488d5a9b0c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 10 Feb 2025 18:22:56 +0800 Subject: [PATCH 06/16] export targeting context --- src/feature-management/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/feature-management/src/index.ts b/src/feature-management/src/index.ts index 77d18c5..24eb7a8 100644 --- a/src/feature-management/src/index.ts +++ b/src/feature-management/src/index.ts @@ -5,4 +5,5 @@ export { FeatureManager, FeatureManagerOptions, EvaluationResult, VariantAssignm export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider.js"; export { createFeatureEvaluationEventProperties } from "./telemetry/featureEvaluationEvent.js"; export { IFeatureFilter } from "./filter/FeatureFilter.js"; +export { TargetingContextAccessor, ITargetingContext } from "./common/targetingContext.js"; export { VERSION } from "./version.js"; From b9bcbc99c53ed4a040e7c0d8f20776b3d6a88c0f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 10 Feb 2025 18:36:46 +0800 Subject: [PATCH 07/16] add comments --- .../src/common/targetingContext.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/feature-management/src/common/targetingContext.ts b/src/feature-management/src/common/targetingContext.ts index ccc8dfb..db1d8d4 100644 --- a/src/feature-management/src/common/targetingContext.ts +++ b/src/feature-management/src/common/targetingContext.ts @@ -1,9 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +/** + * Contextual information that is required to perform a targeting evaluation. + */ export interface ITargetingContext { + /** + * The user id that should be considered when evaluating if the context is being targeted. + */ userId?: string; + /** + * The groups that should be considered when evaluating if the context is being targeted. + */ groups?: string[]; } +/** + * Type definition for a function that, when invoked, returns the @see ITargetingContext for targeting evaluation. + */ export type TargetingContextAccessor = () => ITargetingContext; From 1d6889ac8b2fa19aad5db2c8af27bc5c5b0f137c Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 23 Feb 2025 00:32:18 +0800 Subject: [PATCH 08/16] update --- src/feature-management/src/featureManager.ts | 28 +++++++++++-------- .../test/targetingFilter.test.ts | 2 +- src/feature-management/test/variant.test.ts | 5 ++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/feature-management/src/featureManager.ts b/src/feature-management/src/featureManager.ts index 1d2d76c..3666642 100644 --- a/src/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -80,7 +80,7 @@ export class FeatureManager implements IFeatureManager { return { variant: undefined, reason: VariantAssignmentReason.None }; } - async #isEnabled(featureFlag: FeatureFlag, context?: unknown): Promise { + async #isEnabled(featureFlag: FeatureFlag, appContext?: unknown): Promise { if (featureFlag.enabled !== true) { // If the feature is not explicitly enabled, then it is disabled by default. return false; @@ -110,11 +110,11 @@ export class FeatureManager implements IFeatureManager { clientFilterEvaluationResult = false; } else { - let appContext = context; - if (clientFilter.name === "Microsoft.Targeting" && this.#targetingContextAccessor !== undefined) { - appContext = this.#targetingContextAccessor(); + if (clientFilter.name === "Microsoft.Targeting") { + clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, this.#getTargetingContext(appContext)); + } else { + clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext); } - clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext); } if (clientFilterEvaluationResult === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; @@ -125,7 +125,7 @@ export class FeatureManager implements IFeatureManager { return !shortCircuitEvaluationResult; } - async #evaluateFeature(featureName: string, context: unknown): Promise { + async #evaluateFeature(featureName: string, appContext: unknown): Promise { const featureFlag = await this.#provider.getFeatureFlag(featureName); const result = new EvaluationResult(featureFlag); @@ -138,12 +138,10 @@ export class FeatureManager implements IFeatureManager { validateFeatureFlagFormat(featureFlag); // Evaluate if the feature is enabled. - result.enabled = await this.#isEnabled(featureFlag, context); + result.enabled = await this.#isEnabled(featureFlag, appContext); - let targetingContext = context as ITargetingContext; - if (this.#targetingContextAccessor !== undefined) { - targetingContext = this.#targetingContextAccessor(); - } + // Get targeting context from the app context or the targeting context accessor + const targetingContext = this.#getTargetingContext(appContext); result.targetingId = targetingContext?.userId; // Determine Variant @@ -202,6 +200,14 @@ export class FeatureManager implements IFeatureManager { return result; } + + #getTargetingContext(context: unknown): ITargetingContext | undefined { + let targetingContext = context as ITargetingContext; + if (targetingContext === undefined && this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor(); + } + return targetingContext; + } } export interface FeatureManagerOptions { diff --git a/src/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts index c6a9e22..6219c24 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -147,7 +147,7 @@ describe("targeting filter", () => { expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(false); userId = "Blossom"; expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(true); - expect(await featureManager.isEnabled("ComplexTargeting", {userId: "Aiden"})).to.eq(true); // targeting id will be overridden by the context accessor + expect(await featureManager.isEnabled("ComplexTargeting", {userId: "Aiden"})).to.eq(false); // targeting id will be overridden userId = "Aiden"; groups = ["Stage2"]; expect(await featureManager.isEnabled("ComplexTargeting")).to.eq(true); diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index 31d41f8..e9a9f58 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -106,8 +106,9 @@ describe("variant assignment with targeting context accessor", () => { userId = "Jeff"; variant = await featureManager.getVariant(Features.VariantFeatureUser); expect(variant).to.be.undefined; - variant = await featureManager.getVariant(Features.VariantFeatureUser, {userId: "Marsha"}); // targeting id will be overridden by the context accessor - expect(variant).to.be.undefined; + variant = await featureManager.getVariant(Features.VariantFeatureUser, {userId: "Marsha"}); // targeting id will be overridden + expect(variant).not.to.be.undefined; + expect(variant?.name).eq("Small"); groups = ["Group1"]; variant = await featureManager.getVariant(Features.VariantFeatureGroup); expect(variant).not.to.be.undefined; From 37e6c20b6da98c2a5abf1e6fa4bba07e648e938b Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sun, 23 Feb 2025 01:25:56 +0800 Subject: [PATCH 09/16] add express example --- examples/express-app/config.json | 31 +++++++++++++++++ examples/express-app/package.json | 9 +++++ examples/express-app/server.mjs | 56 +++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 examples/express-app/config.json create mode 100644 examples/express-app/package.json create mode 100644 examples/express-app/server.mjs diff --git a/examples/express-app/config.json b/examples/express-app/config.json new file mode 100644 index 0000000..f085efe --- /dev/null +++ b/examples/express-app/config.json @@ -0,0 +1,31 @@ +{ + "feature_management": { + "feature_flags": [ + { + "id": "Beta", + "enabled": true, + "conditions": { + "client_filters": [ + { + "name": "Microsoft.Targeting", + "parameters": { + "Audience": { + "Users": [ + "Jeff" + ], + "Groups": [ + { + "Name": "Admin", + "RolloutPercentage": 100 + } + ], + "DefaultRolloutPercentage": 40 + } + } + } + ] + } + } + ] + } +} diff --git a/examples/express-app/package.json b/examples/express-app/package.json new file mode 100644 index 0000000..6833583 --- /dev/null +++ b/examples/express-app/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "start": "node server.mjs" + }, + "dependencies": { + "@microsoft/feature-management": "../../src/feature-management", + "express": "^4.21.2" + } +} \ No newline at end of file diff --git a/examples/express-app/server.mjs b/examples/express-app/server.mjs new file mode 100644 index 0000000..ff57d72 --- /dev/null +++ b/examples/express-app/server.mjs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import fs from "fs/promises"; +import { ConfigurationObjectFeatureFlagProvider, FeatureManager } from "@microsoft/feature-management"; +// You can also use Azure App Configuration as the source of feature flags. +// For more information, please go to quickstart: https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-javascript + +const config = JSON.parse(await fs.readFile("config.json")); +const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); + +// https://nodejs.org/api/async_context.html +import { AsyncLocalStorage } from "async_hooks"; +const asyncLocalStorage = new AsyncLocalStorage(); +const exampleTargetingContextAccessor = () => { + const req = asyncLocalStorage.getStore(); + const { userId, groups } = req.query; + return { userId: userId, groups: groups ? groups.split(",") : [] }; +}; + +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); + +import express from "express"; +const server = express(); +const PORT = 3000; + +// Use a middleware to store the request object in async local storage. +// The async local storage allows the targeting context accessor to access the current request throughout its lifetime. +// Middleware 1 +// Middleware 2 +// Request Handler (feature flag evaluation) +server.use((req, res, next) => { + asyncLocalStorage.run(req, next); +}); + +server.get("/", (req, res) => { + res.send("Hello World!"); +}); + +server.get("/Beta", async (req, res) => { + if (await featureManager.isEnabled("Beta")) { + res.send("Welcome to the Beta page!"); + } else { + res.status(404).send("Page not found"); + } +}); + +// Start the server +server.listen(PORT, () => { + console.log(`Server is running at http://localhost:${PORT}`); +}); \ No newline at end of file From f02ea6ee6594f376eca370038ba1992699246bfc Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 5 Mar 2025 15:41:48 +0800 Subject: [PATCH 10/16] update --- examples/express-app/README.md | 69 +++++++++++++++++++++++++++++++++ examples/express-app/server.mjs | 10 ++--- 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 examples/express-app/README.md diff --git a/examples/express-app/README.md b/examples/express-app/README.md new file mode 100644 index 0000000..00fd8b5 --- /dev/null +++ b/examples/express-app/README.md @@ -0,0 +1,69 @@ +# Examples for Microsoft Feature Management for JavaScript + +These examples show how to use the Microsoft Feature Management in an express application. + +## Prerequisites + +The examples are compatible with [LTS versions of Node.js](https://github.com/nodejs/release#release-schedule). + +## Setup & Run + +1. Install the dependencies using `npm`: + + ```bash + npm install + ``` + +1. Run the examples: + + ```bash + node server.mjs + ``` + +1. Visit `http://localhost:3000/Beta` and use `userId` and `groups` query to specify the targeting context (e.g. /Beta?userId=Jeff or /Beta?groups=Admin). + + - If you are not targeted, you will get the message "Page not found". + + - If you are targeted, you will get the message "Welcome to the Beta page!". + +## Targeting + +The targeting mechanism uses the `exampleTargetingContextAccessor` to extract the targeting context from the request. This function retrieves the userId and groups from the query parameters of the request. + +```javascript +const exampleTargetingContextAccessor = () => { + const req = requestAccessor.getStore(); + const { userId, groups } = req.query; + return { userId: userId, groups: groups ? groups.split(",") : [] }; +}; +``` + +The `FeatureManager` is configured with this targeting context accessor: + +```javascript +const featureManager = new FeatureManager( + featureProvider, + { + targetingContextAccessor: exampleTargetingContextAccessor + } +); +``` + +This allows you to get ambient targeting context while doing feature flag evaluation. + +### Request Accessor + +The `requestAccessor` is an instance of `AsyncLocalStorage` from the `async_hooks` module. It is used to store the request object in asynchronous local storage, allowing it to be accessed throughout the lifetime of the request. This is particularly useful for accessing request-specific data in asynchronous operations. For more information, please go to https://nodejs.org/api/async_context.html + +```javascript +import { AsyncLocalStorage } from "async_hooks"; +const requestAccessor = new AsyncLocalStorage(); +``` + +Middleware is used to store the request object in the AsyncLocalStorage: + +```javascript +server.use((req, res, next) => { + requestAccessor.run(req, next); +}); +``` \ No newline at end of file diff --git a/examples/express-app/server.mjs b/examples/express-app/server.mjs index ff57d72..053a96c 100644 --- a/examples/express-app/server.mjs +++ b/examples/express-app/server.mjs @@ -11,9 +11,9 @@ const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); // https://nodejs.org/api/async_context.html import { AsyncLocalStorage } from "async_hooks"; -const asyncLocalStorage = new AsyncLocalStorage(); +const requestAccessor = new AsyncLocalStorage(); const exampleTargetingContextAccessor = () => { - const req = asyncLocalStorage.getStore(); + const req = requestAccessor.getStore(); const { userId, groups } = req.query; return { userId: userId, groups: groups ? groups.split(",") : [] }; }; @@ -31,11 +31,11 @@ const PORT = 3000; // Use a middleware to store the request object in async local storage. // The async local storage allows the targeting context accessor to access the current request throughout its lifetime. -// Middleware 1 +// Middleware 1 (request object is stored in async local storage here and it will be available across the following chained async operations) // Middleware 2 -// Request Handler (feature flag evaluation) +// Request Handler (feature flag evaluation happens here) server.use((req, res, next) => { - asyncLocalStorage.run(req, next); + requestAccessor.run(req, next); }); server.get("/", (req, res) => { From 0e0b3046e0a887d4b510df644d2891d7ff603252 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 6 Mar 2025 13:05:20 +0800 Subject: [PATCH 11/16] update --- .../src/common/targetingContext.ts | 9 ++++- src/feature-management/src/featureManager.ts | 40 ++++++++----------- .../src/filter/TargetingFilter.ts | 36 +++++++++++------ .../src/filter/TimeWindowFilter.ts | 2 +- src/feature-management/src/index.ts | 2 +- .../test/targetingFilter.test.ts | 6 ++- src/feature-management/test/variant.test.ts | 6 ++- 7 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/feature-management/src/common/targetingContext.ts b/src/feature-management/src/common/targetingContext.ts index db1d8d4..a133f15 100644 --- a/src/feature-management/src/common/targetingContext.ts +++ b/src/feature-management/src/common/targetingContext.ts @@ -16,6 +16,11 @@ export interface ITargetingContext { } /** - * Type definition for a function that, when invoked, returns the @see ITargetingContext for targeting evaluation. + * Provides access to the current targeting context. */ -export type TargetingContextAccessor = () => ITargetingContext; +export interface ITargetingContextAccessor { + /** + * Retrieves the current targeting context. + */ + getTargetingContext: () => ITargetingContext | undefined; +} diff --git a/src/feature-management/src/featureManager.ts b/src/feature-management/src/featureManager.ts index 3666642..69fc6cb 100644 --- a/src/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -8,27 +8,25 @@ import { IFeatureFlagProvider } from "./featureProvider.js"; import { TargetingFilter } from "./filter/TargetingFilter.js"; import { Variant } from "./variant/Variant.js"; import { IFeatureManager } from "./IFeatureManager.js"; -import { ITargetingContext, TargetingContextAccessor } from "./common/targetingContext.js"; +import { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js"; import { isTargetedGroup, isTargetedPercentile, isTargetedUser } from "./common/targetingEvaluator.js"; export class FeatureManager implements IFeatureManager { - #provider: IFeatureFlagProvider; - #featureFilters: Map = new Map(); - #onFeatureEvaluated?: (event: EvaluationResult) => void; - #targetingContextAccessor?: TargetingContextAccessor; + readonly #provider: IFeatureFlagProvider; + readonly #featureFilters: Map = new Map(); + readonly #onFeatureEvaluated?: (event: EvaluationResult) => void; + readonly #targetingContextAccessor?: ITargetingContextAccessor; constructor(provider: IFeatureFlagProvider, options?: FeatureManagerOptions) { this.#provider = provider; + this.#onFeatureEvaluated = options?.onFeatureEvaluated; + this.#targetingContextAccessor = options?.targetingContextAccessor; - const builtinFilters = [new TimeWindowFilter(), new TargetingFilter()]; - + const builtinFilters = [new TimeWindowFilter(), new TargetingFilter(options?.targetingContextAccessor)]; // If a custom filter shares a name with an existing filter, the custom filter overrides the existing one. for (const filter of [...builtinFilters, ...(options?.customFilters ?? [])]) { this.#featureFilters.set(filter.name, filter); } - - this.#onFeatureEvaluated = options?.onFeatureEvaluated; - this.#targetingContextAccessor = options?.targetingContextAccessor; } async listFeatureNames(): Promise { @@ -104,19 +102,11 @@ export class FeatureManager implements IFeatureManager { for (const clientFilter of clientFilters) { const matchedFeatureFilter = this.#featureFilters.get(clientFilter.name); const contextWithFeatureName = { featureName: featureFlag.id, parameters: clientFilter.parameters }; - let clientFilterEvaluationResult: boolean; if (matchedFeatureFilter === undefined) { console.warn(`Feature filter ${clientFilter.name} is not found.`); - clientFilterEvaluationResult = false; - } - else { - if (clientFilter.name === "Microsoft.Targeting") { - clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, this.#getTargetingContext(appContext)); - } else { - clientFilterEvaluationResult = await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext); - } + return false; } - if (clientFilterEvaluationResult === shortCircuitEvaluationResult) { + if (await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext) === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; } } @@ -202,9 +192,11 @@ export class FeatureManager implements IFeatureManager { } #getTargetingContext(context: unknown): ITargetingContext | undefined { - let targetingContext = context as ITargetingContext; - if (targetingContext === undefined && this.#targetingContextAccessor !== undefined) { - targetingContext = this.#targetingContextAccessor(); + let targetingContext: ITargetingContext | undefined = context as ITargetingContext; + if (targetingContext?.userId === undefined && + targetingContext?.groups === undefined && + this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor.getTargetingContext(); } return targetingContext; } @@ -225,7 +217,7 @@ export interface FeatureManagerOptions { /** * The accessor function that provides the @see ITargetingContext for targeting evaluation. */ - targetingContextAccessor?: TargetingContextAccessor; + targetingContextAccessor?: ITargetingContextAccessor; } export class EvaluationResult { diff --git a/src/feature-management/src/filter/TargetingFilter.ts b/src/feature-management/src/filter/TargetingFilter.ts index e6da2f7..eb4b73d 100644 --- a/src/feature-management/src/filter/TargetingFilter.ts +++ b/src/feature-management/src/filter/TargetingFilter.ts @@ -3,7 +3,7 @@ import { IFeatureFilter } from "./FeatureFilter.js"; import { isTargetedPercentile } from "../common/targetingEvaluator.js"; -import { ITargetingContext } from "../common/targetingContext.js"; +import { ITargetingContext, ITargetingContextAccessor } from "../common/targetingContext.js"; type TargetingFilterParameters = { Audience: { @@ -26,24 +26,36 @@ type TargetingFilterEvaluationContext = { } export class TargetingFilter implements IFeatureFilter { - name: string = "Microsoft.Targeting"; + readonly name: string = "Microsoft.Targeting"; + readonly #targetingContextAccessor?: ITargetingContextAccessor; + + constructor(targetingContextAccessor?: ITargetingContextAccessor) { + this.#targetingContextAccessor = targetingContextAccessor; + } async evaluate(context: TargetingFilterEvaluationContext, appContext?: ITargetingContext): Promise { const { featureName, parameters } = context; TargetingFilter.#validateParameters(featureName, parameters); + let targetingContext: ITargetingContext | undefined; + if (appContext?.userId !== undefined || appContext?.groups !== undefined) { + targetingContext = appContext; + } else if (this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor.getTargetingContext(); + } + if (parameters.Audience.Exclusion !== undefined) { // check if the user is in the exclusion list - if (appContext?.userId !== undefined && + if (targetingContext?.userId !== undefined && parameters.Audience.Exclusion.Users !== undefined && - parameters.Audience.Exclusion.Users.includes(appContext.userId)) { + parameters.Audience.Exclusion.Users.includes(targetingContext.userId)) { return false; } // check if the user is in a group within exclusion list - if (appContext?.groups !== undefined && + if (targetingContext?.groups !== undefined && parameters.Audience.Exclusion.Groups !== undefined) { for (const excludedGroup of parameters.Audience.Exclusion.Groups) { - if (appContext.groups.includes(excludedGroup)) { + if (targetingContext.groups.includes(excludedGroup)) { return false; } } @@ -51,19 +63,19 @@ export class TargetingFilter implements IFeatureFilter { } // check if the user is being targeted directly - if (appContext?.userId !== undefined && + if (targetingContext?.userId !== undefined && parameters.Audience.Users !== undefined && - parameters.Audience.Users.includes(appContext.userId)) { + parameters.Audience.Users.includes(targetingContext.userId)) { return true; } // check if the user is in a group that is being targeted - if (appContext?.groups !== undefined && + if (targetingContext?.groups !== undefined && parameters.Audience.Groups !== undefined) { for (const group of parameters.Audience.Groups) { - if (appContext.groups.includes(group.Name)) { + if (targetingContext.groups.includes(group.Name)) { const hint = `${featureName}\n${group.Name}`; - if (await isTargetedPercentile(appContext.userId, hint, 0, group.RolloutPercentage)) { + if (await isTargetedPercentile(targetingContext.userId, hint, 0, group.RolloutPercentage)) { return true; } } @@ -72,7 +84,7 @@ export class TargetingFilter implements IFeatureFilter { // check if the user is being targeted by a default rollout percentage const hint = featureName; - return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage); + return isTargetedPercentile(targetingContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage); } static #validateParameters(featureName: string, parameters: TargetingFilterParameters): void { diff --git a/src/feature-management/src/filter/TimeWindowFilter.ts b/src/feature-management/src/filter/TimeWindowFilter.ts index 3cd0ead..beb0136 100644 --- a/src/feature-management/src/filter/TimeWindowFilter.ts +++ b/src/feature-management/src/filter/TimeWindowFilter.ts @@ -15,7 +15,7 @@ type TimeWindowFilterEvaluationContext = { } export class TimeWindowFilter implements IFeatureFilter { - name: string = "Microsoft.TimeWindow"; + readonly name: string = "Microsoft.TimeWindow"; evaluate(context: TimeWindowFilterEvaluationContext): boolean { const {featureName, parameters} = context; diff --git a/src/feature-management/src/index.ts b/src/feature-management/src/index.ts index 24eb7a8..093d3fd 100644 --- a/src/feature-management/src/index.ts +++ b/src/feature-management/src/index.ts @@ -5,5 +5,5 @@ export { FeatureManager, FeatureManagerOptions, EvaluationResult, VariantAssignm export { ConfigurationMapFeatureFlagProvider, ConfigurationObjectFeatureFlagProvider, IFeatureFlagProvider } from "./featureProvider.js"; export { createFeatureEvaluationEventProperties } from "./telemetry/featureEvaluationEvent.js"; export { IFeatureFilter } from "./filter/FeatureFilter.js"; -export { TargetingContextAccessor, ITargetingContext } from "./common/targetingContext.js"; +export { ITargetingContext, ITargetingContextAccessor } from "./common/targetingContext.js"; export { VERSION } from "./version.js"; diff --git a/src/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts index 6219c24..e5d6176 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -139,7 +139,11 @@ describe("targeting filter", () => { let userId = ""; let groups: string[] = []; - const testTargetingContextAccessor = () => ({ userId: userId, groups: groups }); + const testTargetingContextAccessor = { + getTargetingContext: () => { + return { userId: userId, groups: groups }; + } + } const provider = new ConfigurationMapFeatureFlagProvider(dataSource); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index e9a9f58..3f2b000 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -96,7 +96,11 @@ describe("variant assignment with targeting context accessor", () => { it("should assign variant based on targeting context accessor", async () => { let userId = ""; let groups: string[] = []; - const testTargetingContextAccessor = () => ({ userId: userId, groups: groups }); + const testTargetingContextAccessor = { + getTargetingContext: () => { + return { userId: userId, groups: groups }; + } + } const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); userId = "Marsha"; From 73353fa5edaa6abd8181ea9bbe68448487235716 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 6 Mar 2025 13:07:30 +0800 Subject: [PATCH 12/16] fix lint --- src/feature-management/test/targetingFilter.test.ts | 2 +- src/feature-management/test/variant.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/feature-management/test/targetingFilter.test.ts b/src/feature-management/test/targetingFilter.test.ts index e5d6176..6da25f6 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -143,7 +143,7 @@ describe("targeting filter", () => { getTargetingContext: () => { return { userId: userId, groups: groups }; } - } + }; const provider = new ConfigurationMapFeatureFlagProvider(dataSource); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); diff --git a/src/feature-management/test/variant.test.ts b/src/feature-management/test/variant.test.ts index 3f2b000..684d74d 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -100,7 +100,7 @@ describe("variant assignment with targeting context accessor", () => { getTargetingContext: () => { return { userId: userId, groups: groups }; } - } + }; const provider = new ConfigurationObjectFeatureFlagProvider(featureFlagsConfigurationObject); const featureManager = new FeatureManager(provider, {targetingContextAccessor: testTargetingContextAccessor}); userId = "Marsha"; From d548b49e3a8baeafc9e4c64355cd7bc3c5e3ba2b Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 6 Mar 2025 13:12:11 +0800 Subject: [PATCH 13/16] update --- examples/express-app/server.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/express-app/server.mjs b/examples/express-app/server.mjs index 053a96c..5ba7095 100644 --- a/examples/express-app/server.mjs +++ b/examples/express-app/server.mjs @@ -12,10 +12,12 @@ const featureProvider = new ConfigurationObjectFeatureFlagProvider(config); // https://nodejs.org/api/async_context.html import { AsyncLocalStorage } from "async_hooks"; const requestAccessor = new AsyncLocalStorage(); -const exampleTargetingContextAccessor = () => { - const req = requestAccessor.getStore(); - const { userId, groups } = req.query; - return { userId: userId, groups: groups ? groups.split(",") : [] }; +const exampleTargetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + const { userId, groups } = req.query; + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } }; const featureManager = new FeatureManager( From ea74cac6b1c72dd4115e5737e0570bea2bebb835 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 7 Mar 2025 11:15:35 +0800 Subject: [PATCH 14/16] update example in README --- examples/express-app/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/express-app/README.md b/examples/express-app/README.md index 00fd8b5..20611a1 100644 --- a/examples/express-app/README.md +++ b/examples/express-app/README.md @@ -31,10 +31,12 @@ The examples are compatible with [LTS versions of Node.js](https://github.com/no The targeting mechanism uses the `exampleTargetingContextAccessor` to extract the targeting context from the request. This function retrieves the userId and groups from the query parameters of the request. ```javascript -const exampleTargetingContextAccessor = () => { - const req = requestAccessor.getStore(); - const { userId, groups } = req.query; - return { userId: userId, groups: groups ? groups.split(",") : [] }; +const exampleTargetingContextAccessor = { + getTargetingContext: () => { + const req = requestAccessor.getStore(); + const { userId, groups } = req.query; + return { userId: userId, groups: groups ? groups.split(",") : [] }; + } }; ``` From 9a806919bc5ef11a6b828011708cb9a5bd773d79 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Mon, 7 Apr 2025 15:12:20 +0800 Subject: [PATCH 15/16] update script --- examples/express-app/README.md | 6 ++++++ examples/express-app/package.json | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/express-app/README.md b/examples/express-app/README.md index 20611a1..a0056a7 100644 --- a/examples/express-app/README.md +++ b/examples/express-app/README.md @@ -8,6 +8,12 @@ The examples are compatible with [LTS versions of Node.js](https://github.com/no ## Setup & Run +1. Build: + + ```bash + npm run build + ``` + 1. Install the dependencies using `npm`: ```bash diff --git a/examples/express-app/package.json b/examples/express-app/package.json index 6833583..f4e8840 100644 --- a/examples/express-app/package.json +++ b/examples/express-app/package.json @@ -1,6 +1,7 @@ { "scripts": { - "start": "node server.mjs" + "start": "node server.mjs", + "build": "cd ../../src/feature-management && npm i && npm run build && cd ../../examples/express-app && npm i" }, "dependencies": { "@microsoft/feature-management": "../../src/feature-management", From ba8e1a42beba874522b80f95f34951d204348b92 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 8 Apr 2025 13:12:57 +0800 Subject: [PATCH 16/16] update --- examples/express-app/README.md | 7 +++++-- examples/express-app/package.json | 3 +-- examples/express-app/server.mjs | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/express-app/README.md b/examples/express-app/README.md index a0056a7..5cd07b0 100644 --- a/examples/express-app/README.md +++ b/examples/express-app/README.md @@ -8,13 +8,14 @@ The examples are compatible with [LTS versions of Node.js](https://github.com/no ## Setup & Run -1. Build: +1. Go to `src/feature-management` under the root folder and run: ```bash + npm run install npm run build ``` -1. Install the dependencies using `npm`: +1. Go back to `examples/express-app` and install the dependencies using `npm`: ```bash npm install @@ -40,7 +41,9 @@ The targeting mechanism uses the `exampleTargetingContextAccessor` to extract th const exampleTargetingContextAccessor = { getTargetingContext: () => { const req = requestAccessor.getStore(); + // read user and groups from request query data const { userId, groups } = req.query; + // return aa ITargetingContext with the appropriate user info return { userId: userId, groups: groups ? groups.split(",") : [] }; } }; diff --git a/examples/express-app/package.json b/examples/express-app/package.json index f4e8840..6833583 100644 --- a/examples/express-app/package.json +++ b/examples/express-app/package.json @@ -1,7 +1,6 @@ { "scripts": { - "start": "node server.mjs", - "build": "cd ../../src/feature-management && npm i && npm run build && cd ../../examples/express-app && npm i" + "start": "node server.mjs" }, "dependencies": { "@microsoft/feature-management": "../../src/feature-management", diff --git a/examples/express-app/server.mjs b/examples/express-app/server.mjs index 5ba7095..2b8ea53 100644 --- a/examples/express-app/server.mjs +++ b/examples/express-app/server.mjs @@ -15,7 +15,9 @@ const requestAccessor = new AsyncLocalStorage(); const exampleTargetingContextAccessor = { getTargetingContext: () => { const req = requestAccessor.getStore(); + // read user and groups from request query data const { userId, groups } = req.query; + // return an ITargetingContext with the appropriate user info return { userId: userId, groups: groups ? groups.split(",") : [] }; } };