From ddee6107acf7b7e7fbbc5cc205596466daaa4b6a Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:47:41 +0800 Subject: [PATCH 1/8] add publish tag (#77) --- .../package.json | 3 +++ sdk/feature-management-applicationinsights-node/package.json | 3 +++ sdk/feature-management/package.json | 3 +++ 3 files changed, 9 insertions(+) diff --git a/sdk/feature-management-applicationinsights-browser/package.json b/sdk/feature-management-applicationinsights-browser/package.json index 8970137..946bf95 100644 --- a/sdk/feature-management-applicationinsights-browser/package.json +++ b/sdk/feature-management-applicationinsights-browser/package.json @@ -24,6 +24,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "preview" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, diff --git a/sdk/feature-management-applicationinsights-node/package.json b/sdk/feature-management-applicationinsights-node/package.json index 975199a..e883d31 100644 --- a/sdk/feature-management-applicationinsights-node/package.json +++ b/sdk/feature-management-applicationinsights-node/package.json @@ -24,6 +24,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "preview" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, diff --git a/sdk/feature-management/package.json b/sdk/feature-management/package.json index 3e77c26..6e75d3e 100644 --- a/sdk/feature-management/package.json +++ b/sdk/feature-management/package.json @@ -26,6 +26,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "preview" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, From 712387ffb80d376c390921b6ed5f303ce81c2ab9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 8 Jan 2025 13:59:21 +0800 Subject: [PATCH 2/8] Revert "remove exp telemetry & stable v2 (#76)" This reverts commit add9e26d23c3802fe9124fddd5cdcbb550e2aeac. --- .../package.json | 4 +-- .../src/version.ts | 2 +- .../package.json | 4 +-- .../src/version.ts | 2 +- sdk/feature-management/package-lock.json | 4 +-- sdk/feature-management/package.json | 2 +- .../src/telemetry/featureEvaluationEvent.ts | 29 ++++++++++++++++++- sdk/feature-management/src/version.ts | 2 +- 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/sdk/feature-management-applicationinsights-browser/package.json b/sdk/feature-management-applicationinsights-browser/package.json index 9358273..946bf95 100644 --- a/sdk/feature-management-applicationinsights-browser/package.json +++ b/sdk/feature-management-applicationinsights-browser/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management-applicationinsights-browser", - "version": "2.0.0", + "version": "2.0.0-preview.3", "description": "Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library.", "main": "./dist/umd/index.js", "module": "./dist/esm/index.js", @@ -45,7 +45,7 @@ }, "dependencies": { "@microsoft/applicationinsights-web": "^3.3.2", - "@microsoft/feature-management": "2.0.0" + "@microsoft/feature-management": "2.0.0-preview.3" } } \ No newline at end of file diff --git a/sdk/feature-management-applicationinsights-browser/src/version.ts b/sdk/feature-management-applicationinsights-browser/src/version.ts index b4709c9..074200b 100644 --- a/sdk/feature-management-applicationinsights-browser/src/version.ts +++ b/sdk/feature-management-applicationinsights-browser/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0"; +export const VERSION = "2.0.0-preview.3"; diff --git a/sdk/feature-management-applicationinsights-node/package.json b/sdk/feature-management-applicationinsights-node/package.json index d0cd0c6..e883d31 100644 --- a/sdk/feature-management-applicationinsights-node/package.json +++ b/sdk/feature-management-applicationinsights-node/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management-applicationinsights-node", - "version": "2.0.0", + "version": "2.0.0-preview.3", "description": "Feature Management Application Insights Plugin for Node.js provides a solution for sending feature flag evaluation events produced by the Feature Management library.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", @@ -45,7 +45,7 @@ }, "dependencies": { "applicationinsights": "^2.9.6", - "@microsoft/feature-management": "2.0.0" + "@microsoft/feature-management": "2.0.0-preview.3" } } \ No newline at end of file diff --git a/sdk/feature-management-applicationinsights-node/src/version.ts b/sdk/feature-management-applicationinsights-node/src/version.ts index b4709c9..074200b 100644 --- a/sdk/feature-management-applicationinsights-node/src/version.ts +++ b/sdk/feature-management-applicationinsights-node/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0"; +export const VERSION = "2.0.0-preview.3"; diff --git a/sdk/feature-management/package-lock.json b/sdk/feature-management/package-lock.json index a69abd1..a840a41 100644 --- a/sdk/feature-management/package-lock.json +++ b/sdk/feature-management/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0", + "version": "2.0.0-preview.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/feature-management", - "version": "2.0.0", + "version": "2.0.0-preview.3", "license": "MIT", "devDependencies": { "@playwright/test": "^1.46.1", diff --git a/sdk/feature-management/package.json b/sdk/feature-management/package.json index 5217463..6e75d3e 100644 --- a/sdk/feature-management/package.json +++ b/sdk/feature-management/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0", + "version": "2.0.0-preview.3", "description": "Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", diff --git a/sdk/feature-management/src/telemetry/featureEvaluationEvent.ts b/sdk/feature-management/src/telemetry/featureEvaluationEvent.ts index b76f46b..2c195c6 100644 --- a/sdk/feature-management/src/telemetry/featureEvaluationEvent.ts +++ b/sdk/feature-management/src/telemetry/featureEvaluationEvent.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { EvaluationResult } from "../featureManager"; +import { EvaluationResult, VariantAssignmentReason } from "../featureManager"; import { EVALUATION_EVENT_VERSION } from "../version.js"; const VERSION = "Version"; @@ -10,6 +10,8 @@ const ENABLED = "Enabled"; const TARGETING_ID = "TargetingId"; const VARIANT = "Variant"; const VARIANT_ASSIGNMENT_REASON = "VariantAssignmentReason"; +const DEFAULT_WHEN_ENABLED = "DefaultWhenEnabled"; +const VARIANT_ASSIGNMENT_PERCENTAGE = "VariantAssignmentPercentage"; export function createFeatureEvaluationEventProperties(result: EvaluationResult): any { if (result.feature === undefined) { @@ -26,6 +28,31 @@ export function createFeatureEvaluationEventProperties(result: EvaluationResult) [VARIANT_ASSIGNMENT_REASON]: result.variantAssignmentReason, }; + if (result.feature.allocation?.default_when_enabled) { + eventProperties[DEFAULT_WHEN_ENABLED] = result.feature.allocation.default_when_enabled; + } + + if (result.variantAssignmentReason === VariantAssignmentReason.DefaultWhenEnabled) { + let percentileAllocationPercentage = 0; + if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) { + for (const percentile of result.feature.allocation.percentile) { + percentileAllocationPercentage += percentile.to - percentile.from; + } + } + eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = (100 - percentileAllocationPercentage).toString(); + } + else if (result.variantAssignmentReason === VariantAssignmentReason.Percentile) { + let percentileAllocationPercentage = 0; + if (result.variant !== undefined && result.feature.allocation !== undefined && result.feature.allocation.percentile !== undefined) { + for (const percentile of result.feature.allocation.percentile) { + if (percentile.variant === result.variant.name) { + percentileAllocationPercentage += percentile.to - percentile.from; + } + } + } + eventProperties[VARIANT_ASSIGNMENT_PERCENTAGE] = percentileAllocationPercentage.toString(); + } + const metadata = result.feature.telemetry?.metadata; if (metadata) { for (const key in metadata) { diff --git a/sdk/feature-management/src/version.ts b/sdk/feature-management/src/version.ts index aaae469..65fbbf3 100644 --- a/sdk/feature-management/src/version.ts +++ b/sdk/feature-management/src/version.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0"; +export const VERSION = "2.0.0-preview.3"; export const EVALUATION_EVENT_VERSION = "1.0.0"; From 3cfa8ea9bcf9150ff4a67172921e07563023eafc Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:00:08 +0800 Subject: [PATCH 3/8] version bump v3 preview (#82) --- .../package.json | 4 ++-- .../src/version.ts | 2 +- sdk/feature-management-applicationinsights-node/package.json | 4 ++-- .../src/version.ts | 2 +- sdk/feature-management/package-lock.json | 4 ++-- sdk/feature-management/package.json | 2 +- sdk/feature-management/src/version.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sdk/feature-management-applicationinsights-browser/package.json b/sdk/feature-management-applicationinsights-browser/package.json index 946bf95..2f3d831 100644 --- a/sdk/feature-management-applicationinsights-browser/package.json +++ b/sdk/feature-management-applicationinsights-browser/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management-applicationinsights-browser", - "version": "2.0.0-preview.3", + "version": "3.0.0-preview.1", "description": "Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library.", "main": "./dist/umd/index.js", "module": "./dist/esm/index.js", @@ -45,7 +45,7 @@ }, "dependencies": { "@microsoft/applicationinsights-web": "^3.3.2", - "@microsoft/feature-management": "2.0.0-preview.3" + "@microsoft/feature-management": "3.0.0-preview.1" } } \ No newline at end of file diff --git a/sdk/feature-management-applicationinsights-browser/src/version.ts b/sdk/feature-management-applicationinsights-browser/src/version.ts index 074200b..83eb430 100644 --- a/sdk/feature-management-applicationinsights-browser/src/version.ts +++ b/sdk/feature-management-applicationinsights-browser/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.3"; +export const VERSION = "3.0.0-preview.1"; diff --git a/sdk/feature-management-applicationinsights-node/package.json b/sdk/feature-management-applicationinsights-node/package.json index e883d31..0a865ec 100644 --- a/sdk/feature-management-applicationinsights-node/package.json +++ b/sdk/feature-management-applicationinsights-node/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management-applicationinsights-node", - "version": "2.0.0-preview.3", + "version": "3.0.0-preview.1", "description": "Feature Management Application Insights Plugin for Node.js provides a solution for sending feature flag evaluation events produced by the Feature Management library.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", @@ -45,7 +45,7 @@ }, "dependencies": { "applicationinsights": "^2.9.6", - "@microsoft/feature-management": "2.0.0-preview.3" + "@microsoft/feature-management": "3.0.0-preview.1" } } \ No newline at end of file diff --git a/sdk/feature-management-applicationinsights-node/src/version.ts b/sdk/feature-management-applicationinsights-node/src/version.ts index 074200b..83eb430 100644 --- a/sdk/feature-management-applicationinsights-node/src/version.ts +++ b/sdk/feature-management-applicationinsights-node/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.3"; +export const VERSION = "3.0.0-preview.1"; diff --git a/sdk/feature-management/package-lock.json b/sdk/feature-management/package-lock.json index a840a41..32e1c19 100644 --- a/sdk/feature-management/package-lock.json +++ b/sdk/feature-management/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.3", + "version": "3.0.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.3", + "version": "3.0.0-preview.1", "license": "MIT", "devDependencies": { "@playwright/test": "^1.46.1", diff --git a/sdk/feature-management/package.json b/sdk/feature-management/package.json index 6e75d3e..5657601 100644 --- a/sdk/feature-management/package.json +++ b/sdk/feature-management/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.3", + "version": "3.0.0-preview.1", "description": "Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", diff --git a/sdk/feature-management/src/version.ts b/sdk/feature-management/src/version.ts index 65fbbf3..3c6d999 100644 --- a/sdk/feature-management/src/version.ts +++ b/sdk/feature-management/src/version.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.3"; +export const VERSION = "3.0.0-preview.1"; export const EVALUATION_EVENT_VERSION = "1.0.0"; From a7694d6700d130c01ab22707788ea19bd6b1e133 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:39:27 +0800 Subject: [PATCH 4/8] Support targeting context accessor (#93) * support targeting context accessor * add test * fix lint * update * update * export targeting context * add comments * update * update * fix lint --- src/feature-management/src/IFeatureManager.ts | 2 +- .../src/common/ITargetingContext.ts | 8 ---- .../src/common/targetingContext.ts | 26 +++++++++++ src/feature-management/src/featureManager.ts | 44 +++++++++++++------ .../src/filter/TargetingFilter.ts | 36 +++++++++------ .../src/filter/TimeWindowFilter.ts | 2 +- src/feature-management/src/index.ts | 1 + .../test/targetingFilter.test.ts | 24 ++++++++-- src/feature-management/test/variant.test.ts | 27 ++++++++++++ 9 files changed, 128 insertions(+), 42 deletions(-) delete mode 100644 src/feature-management/src/common/ITargetingContext.ts create mode 100644 src/feature-management/src/common/targetingContext.ts 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/ITargetingContext.ts deleted file mode 100644 index 1d5a426..0000000 --- a/src/feature-management/src/common/ITargetingContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -export interface ITargetingContext { - userId?: string; - groups?: string[]; -} - diff --git a/src/feature-management/src/common/targetingContext.ts b/src/feature-management/src/common/targetingContext.ts new file mode 100644 index 0000000..a133f15 --- /dev/null +++ b/src/feature-management/src/common/targetingContext.ts @@ -0,0 +1,26 @@ +// 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[]; +} + +/** + * Provides access to the current targeting context. + */ +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 a035b5d..69fc6cb 100644 --- a/src/feature-management/src/featureManager.ts +++ b/src/feature-management/src/featureManager.ts @@ -8,25 +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 } from "./common/ITargetingContext.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; + 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; } async listFeatureNames(): Promise { @@ -78,7 +78,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; @@ -106,7 +106,7 @@ export class FeatureManager implements IFeatureManager { console.warn(`Feature filter ${clientFilter.name} is not found.`); return false; } - if (await matchedFeatureFilter.evaluate(contextWithFeatureName, context) === shortCircuitEvaluationResult) { + if (await matchedFeatureFilter.evaluate(contextWithFeatureName, appContext) === shortCircuitEvaluationResult) { return shortCircuitEvaluationResult; } } @@ -115,7 +115,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); @@ -128,9 +128,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); - const targetingContext = context as ITargetingContext; + // Get targeting context from the app context or the targeting context accessor + const targetingContext = this.#getTargetingContext(appContext); result.targetingId = targetingContext?.userId; // Determine Variant @@ -151,7 +152,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; @@ -189,6 +190,16 @@ export class FeatureManager implements IFeatureManager { return result; } + + #getTargetingContext(context: unknown): ITargetingContext | undefined { + let targetingContext: ITargetingContext | undefined = context as ITargetingContext; + if (targetingContext?.userId === undefined && + targetingContext?.groups === undefined && + this.#targetingContextAccessor !== undefined) { + targetingContext = this.#targetingContextAccessor.getTargetingContext(); + } + return targetingContext; + } } export interface FeatureManagerOptions { @@ -202,6 +213,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?: ITargetingContextAccessor; } export class EvaluationResult { diff --git a/src/feature-management/src/filter/TargetingFilter.ts b/src/feature-management/src/filter/TargetingFilter.ts index 2d7220e..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/ITargetingContext.js"; +import { ITargetingContext, ITargetingContextAccessor } from "../common/targetingContext.js"; type TargetingFilterParameters = { Audience: { @@ -26,28 +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); - if (appContext === undefined) { - throw new Error("The app context is required for targeting filter."); + 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; } } @@ -55,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; } } @@ -76,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 77d18c5..093d3fd 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 { 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 91fe81b..6da25f6 100644 --- a/src/feature-management/test/targetingFilter.test.ts +++ b/src/feature-management/test/targetingFilter.test.ts @@ -131,15 +131,31 @@ describe("targeting filter", () => { ]); }); - it("should throw error if app context is not provided", () => { + 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 = { + getTargetingContext: () => { + return { userId: userId, groups: groups }; + } + }; 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."); + 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(false); // targeting id will be overridden + 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 118fc03..edff9a0 100644 --- a/src/feature-management/test/variant.test.ts +++ b/src/feature-management/test/variant.test.ts @@ -90,5 +90,32 @@ 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 testTargetingContextAccessor = { + getTargetingContext: () => { + return { userId: userId, groups: 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 + 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; + expect(variant?.name).eq("Small"); + }); }); From 6adbb93c2a0158ed5d34bec1613551d355c7429e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Fri, 11 Apr 2025 00:18:07 +0800 Subject: [PATCH 5/8] Add an express example to demostrate ambient targeting usage (#105) * support targeting context accessor * add test * fix lint * update * update * export targeting context * add comments * update * add express example * update * update * fix lint * update * update example in README * update script * update --- examples/express-app/README.md | 80 +++++++++++++++++++++++++++++++ examples/express-app/config.json | 31 ++++++++++++ examples/express-app/package.json | 9 ++++ examples/express-app/server.mjs | 60 +++++++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 examples/express-app/README.md 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/README.md b/examples/express-app/README.md new file mode 100644 index 0000000..5cd07b0 --- /dev/null +++ b/examples/express-app/README.md @@ -0,0 +1,80 @@ +# 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. Go to `src/feature-management` under the root folder and run: + + ```bash + npm run install + npm run build + ``` + +1. Go back to `examples/express-app` and 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 = { + 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(",") : [] }; + } +}; +``` + +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/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..2b8ea53 --- /dev/null +++ b/examples/express-app/server.mjs @@ -0,0 +1,60 @@ +// 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 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(",") : [] }; + } +}; + +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 (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 happens here) +server.use((req, res, next) => { + requestAccessor.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 dfbe35e9930e29889ebe3c20323db9366b49c9a3 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:26:43 +0800 Subject: [PATCH 6/8] Support telemetry processor & initializer (#98) * support targeting context accessor * add test * fix lint * update * update * export targeting context * add comments * support telemetry processor & initializer * update * update --- .gitignore | 2 ++ .../src/index.ts | 2 +- .../src/telemetry.ts | 21 +++++++++++++++++-- .../src/index.ts | 2 +- .../src/telemetry.ts | 21 ++++++++++++++++++- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 250bd47..e8b8fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -413,3 +413,5 @@ examples/**/**/package-lock.json # playwright test result test-results + +**/public \ No newline at end of file diff --git a/src/feature-management-applicationinsights-browser/src/index.ts b/src/feature-management-applicationinsights-browser/src/index.ts index 6b53335..efbc293 100644 --- a/src/feature-management-applicationinsights-browser/src/index.ts +++ b/src/feature-management-applicationinsights-browser/src/index.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { createTelemetryPublisher, trackEvent } from "./telemetry.js"; +export { createTargetingTelemetryInitializer, createTelemetryPublisher, trackEvent } from "./telemetry.js"; export { VERSION } from "./version.js"; diff --git a/src/feature-management-applicationinsights-browser/src/telemetry.ts b/src/feature-management-applicationinsights-browser/src/telemetry.ts index 6877c0f..3b9cfad 100644 --- a/src/feature-management-applicationinsights-browser/src/telemetry.ts +++ b/src/feature-management-applicationinsights-browser/src/telemetry.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { EvaluationResult, createFeatureEvaluationEventProperties } from "@microsoft/feature-management"; -import { ApplicationInsights, IEventTelemetry } from "@microsoft/applicationinsights-web"; +import { EvaluationResult, createFeatureEvaluationEventProperties, ITargetingContextAccessor } from "@microsoft/feature-management"; +import { ApplicationInsights, IEventTelemetry, ITelemetryItem } from "@microsoft/applicationinsights-web"; const TARGETING_ID = "TargetingId"; const FEATURE_EVALUATION_EVENT_NAME = "FeatureEvaluation"; @@ -39,3 +39,20 @@ export function trackEvent(client: ApplicationInsights, targetingId: string, eve properties[TARGETING_ID] = targetingId ? targetingId.toString() : ""; client.trackEvent(event, properties); } + +/** + * Creates a telemetry initializer that adds targeting id to telemetry item's custom properties. + * @param targetingContextAccessor The accessor function to get the targeting context. + * @returns A telemetry initializer that attaches targeting id to telemetry items. + */ +export function createTargetingTelemetryInitializer(targetingContextAccessor: ITargetingContextAccessor): (item: ITelemetryItem) => void { + return (item: ITelemetryItem) => { + const targetingContext = targetingContextAccessor.getTargetingContext(); + if (targetingContext !== undefined) { + if (targetingContext?.userId === undefined) { + console.warn("Targeting id is undefined."); + } + item.data = {...item.data, [TARGETING_ID]: targetingContext?.userId || ""}; + } + }; +} diff --git a/src/feature-management-applicationinsights-node/src/index.ts b/src/feature-management-applicationinsights-node/src/index.ts index 6b53335..e8509a8 100644 --- a/src/feature-management-applicationinsights-node/src/index.ts +++ b/src/feature-management-applicationinsights-node/src/index.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { createTelemetryPublisher, trackEvent } from "./telemetry.js"; +export { createTargetingTelemetryProcessor, createTelemetryPublisher, trackEvent } from "./telemetry.js"; export { VERSION } from "./version.js"; diff --git a/src/feature-management-applicationinsights-node/src/telemetry.ts b/src/feature-management-applicationinsights-node/src/telemetry.ts index 11030e6..48f3320 100644 --- a/src/feature-management-applicationinsights-node/src/telemetry.ts +++ b/src/feature-management-applicationinsights-node/src/telemetry.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { EvaluationResult, createFeatureEvaluationEventProperties } from "@microsoft/feature-management"; +import { EvaluationResult, createFeatureEvaluationEventProperties, ITargetingContextAccessor } from "@microsoft/feature-management"; import { TelemetryClient, Contracts } from "applicationinsights"; const TARGETING_ID = "TargetingId"; @@ -39,3 +39,22 @@ export function trackEvent(client: TelemetryClient, targetingId: string, event: }; client.trackEvent(event); } + +/** + * Creates a telemetry processor that adds targeting id to telemetry envelope's custom properties. + * @param targetingContextAccessor The accessor function to get the targeting context. + * @returns A telemetry processor that attaches targeting id to telemetry envelopes. + */ +export function createTargetingTelemetryProcessor(targetingContextAccessor: ITargetingContextAccessor): (envelope: Contracts.EnvelopeTelemetry) => boolean { + return (envelope: Contracts.EnvelopeTelemetry) => { + const targetingContext = targetingContextAccessor.getTargetingContext(); + if (targetingContext !== undefined) { + if (targetingContext?.userId === undefined) { + console.warn("Targeting id is undefined."); + } + envelope.data.baseData = envelope.data.baseData || {}; + envelope.data.baseData.properties = {...envelope.data.baseData.properties, [TARGETING_ID]: targetingContext?.userId || ""}; + } + return true; + }; +} From 17fc7787038a224317153bdafc483b0b85f1c68c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:14:27 +0800 Subject: [PATCH 7/8] not log warning for node (#111) --- .../src/telemetry.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/feature-management-applicationinsights-node/src/telemetry.ts b/src/feature-management-applicationinsights-node/src/telemetry.ts index 48f3320..825db5b 100644 --- a/src/feature-management-applicationinsights-node/src/telemetry.ts +++ b/src/feature-management-applicationinsights-node/src/telemetry.ts @@ -48,10 +48,7 @@ export function trackEvent(client: TelemetryClient, targetingId: string, event: export function createTargetingTelemetryProcessor(targetingContextAccessor: ITargetingContextAccessor): (envelope: Contracts.EnvelopeTelemetry) => boolean { return (envelope: Contracts.EnvelopeTelemetry) => { const targetingContext = targetingContextAccessor.getTargetingContext(); - if (targetingContext !== undefined) { - if (targetingContext?.userId === undefined) { - console.warn("Targeting id is undefined."); - } + if (targetingContext?.userId !== undefined) { envelope.data.baseData = envelope.data.baseData || {}; envelope.data.baseData.properties = {...envelope.data.baseData.properties, [TARGETING_ID]: targetingContext?.userId || ""}; } From d93defec52a44f61ac5885a1b89a6f778ca7900e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 22 Apr 2025 11:46:37 +0800 Subject: [PATCH 8/8] version bump 2.1.0-preview.1 (#113) --- .../package.json | 4 ++-- .../src/version.ts | 2 +- src/feature-management-applicationinsights-node/package.json | 4 ++-- .../src/version.ts | 2 +- src/feature-management/package-lock.json | 4 ++-- src/feature-management/package.json | 2 +- src/feature-management/src/version.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/feature-management-applicationinsights-browser/package.json b/src/feature-management-applicationinsights-browser/package.json index b98463f..01e6a00 100644 --- a/src/feature-management-applicationinsights-browser/package.json +++ b/src/feature-management-applicationinsights-browser/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management-applicationinsights-browser", - "version": "2.0.2", + "version": "2.1.0-preview.1", "description": "Feature Management Application Insights Plugin for Browser provides a solution for sending feature flag evaluation events produced by the Feature Management library.", "main": "./dist/esm/index.js", "module": "./dist/esm/index.js", @@ -46,7 +46,7 @@ }, "dependencies": { "@microsoft/applicationinsights-web": "^3.3.2", - "@microsoft/feature-management": "2.0.2" + "@microsoft/feature-management": "2.1.0-preview.1" } } \ No newline at end of file diff --git a/src/feature-management-applicationinsights-browser/src/version.ts b/src/feature-management-applicationinsights-browser/src/version.ts index 92cdac8..4a665a7 100644 --- a/src/feature-management-applicationinsights-browser/src/version.ts +++ b/src/feature-management-applicationinsights-browser/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.2"; +export const VERSION = "2.1.0-preview.1"; diff --git a/src/feature-management-applicationinsights-node/package.json b/src/feature-management-applicationinsights-node/package.json index 708cbda..1e79509 100644 --- a/src/feature-management-applicationinsights-node/package.json +++ b/src/feature-management-applicationinsights-node/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management-applicationinsights-node", - "version": "2.0.2", + "version": "2.1.0-preview.1", "description": "Feature Management Application Insights Plugin for Node.js provides a solution for sending feature flag evaluation events produced by the Feature Management library.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", @@ -45,7 +45,7 @@ }, "dependencies": { "applicationinsights": "^2.9.6", - "@microsoft/feature-management": "2.0.2" + "@microsoft/feature-management": "2.1.0-preview.1" } } \ No newline at end of file diff --git a/src/feature-management-applicationinsights-node/src/version.ts b/src/feature-management-applicationinsights-node/src/version.ts index 92cdac8..4a665a7 100644 --- a/src/feature-management-applicationinsights-node/src/version.ts +++ b/src/feature-management-applicationinsights-node/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.2"; +export const VERSION = "2.1.0-preview.1"; diff --git a/src/feature-management/package-lock.json b/src/feature-management/package-lock.json index f399cf0..923d15c 100644 --- a/src/feature-management/package-lock.json +++ b/src/feature-management/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/feature-management", - "version": "2.0.2", + "version": "2.1.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/feature-management", - "version": "2.0.2", + "version": "2.1.0-preview.1", "license": "MIT", "devDependencies": { "@playwright/test": "^1.46.1", diff --git a/src/feature-management/package.json b/src/feature-management/package.json index a5c09b1..f68c288 100644 --- a/src/feature-management/package.json +++ b/src/feature-management/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management", - "version": "2.0.2", + "version": "2.1.0-preview.1", "description": "Feature Management is a library for enabling/disabling features at runtime. Developers can use feature flags in simple use cases like conditional statement to more advanced scenarios like conditionally adding routes.", "main": "./dist/commonjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/feature-management/src/version.ts b/src/feature-management/src/version.ts index 6a06472..1174509 100644 --- a/src/feature-management/src/version.ts +++ b/src/feature-management/src/version.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.2"; +export const VERSION = "2.1.0-preview.1"; export const EVALUATION_EVENT_VERSION = "1.0.0";