From 7a4a6fa6b26c9e0a9f0921e7739eba273efd6284 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:37:30 +0800 Subject: [PATCH 1/4] update readme (#72) --- sdk/feature-management-applicationinsights-browser/README.md | 2 +- sdk/feature-management-applicationinsights-node/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/feature-management-applicationinsights-browser/README.md b/sdk/feature-management-applicationinsights-browser/README.md index 851126f..61622d7 100644 --- a/sdk/feature-management-applicationinsights-browser/README.md +++ b/sdk/feature-management-applicationinsights-browser/README.md @@ -9,7 +9,7 @@ Feature Management Application Insights Plugin for Browser provides a solution f ``` javascript import { ApplicationInsights } from "@microsoft/applicationinsights-web" import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "@microsoft/feature-management"; -import { trackEvent, createTelemetryPublisher } from "@microsoft/feature-management-applicationinsights-browser"; +import { createTelemetryPublisher, trackEvent } from "@microsoft/feature-management-applicationinsights-browser"; const appInsights = new ApplicationInsights({ config: { connectionString: CONNECTION_STRING diff --git a/sdk/feature-management-applicationinsights-node/README.md b/sdk/feature-management-applicationinsights-node/README.md index 5c60461..695a421 100644 --- a/sdk/feature-management-applicationinsights-node/README.md +++ b/sdk/feature-management-applicationinsights-node/README.md @@ -13,7 +13,7 @@ Feature Management Application Insights Plugin for Browser provides a solution f ``` javascript import appInsights from "applicationinsights"; import { FeatureManager, ConfigurationObjectFeatureFlagProvider } from "@microsoft/feature-management"; -import { trackEvent, publishTelemetry } from "@microsoft/feature-management-applicationinsights-node"; +import { createTelemetryPublisher, trackEvent } from "@microsoft/feature-management-applicationinsights-node"; appInsights.setup(CONNECTION_STRING) .start(); From add9e26d23c3802fe9124fddd5cdcbb550e2aeac Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:36:26 +0800 Subject: [PATCH 2/4] remove exp telemetry & stable v2 (#76) --- .../package.json | 7 +++-- .../src/version.ts | 2 +- .../package.json | 7 +++-- .../src/version.ts | 2 +- sdk/feature-management/package-lock.json | 2 +- sdk/feature-management/package.json | 5 +++- .../src/telemetry/featureEvaluationEvent.ts | 29 +------------------ sdk/feature-management/src/version.ts | 2 +- 8 files changed, 19 insertions(+), 37 deletions(-) diff --git a/sdk/feature-management-applicationinsights-browser/package.json b/sdk/feature-management-applicationinsights-browser/package.json index 8970137..3f3cf6d 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": "2.0.0", "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", @@ -24,6 +24,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "stable" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, @@ -42,7 +45,7 @@ }, "dependencies": { "@microsoft/applicationinsights-web": "^3.3.2", - "@microsoft/feature-management": "2.0.0-preview.3" + "@microsoft/feature-management": "2.0.0" } } \ 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..b4709c9 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 = "2.0.0"; diff --git a/sdk/feature-management-applicationinsights-node/package.json b/sdk/feature-management-applicationinsights-node/package.json index 975199a..7d6a1e6 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": "2.0.0", "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", @@ -24,6 +24,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "stable" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, @@ -42,7 +45,7 @@ }, "dependencies": { "applicationinsights": "^2.9.6", - "@microsoft/feature-management": "2.0.0-preview.3" + "@microsoft/feature-management": "2.0.0" } } \ 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..b4709c9 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 = "2.0.0"; diff --git a/sdk/feature-management/package-lock.json b/sdk/feature-management/package-lock.json index 57830e9..393b536 100644 --- a/sdk/feature-management/package-lock.json +++ b/sdk/feature-management/package-lock.json @@ -1,6 +1,6 @@ { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.3", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/sdk/feature-management/package.json b/sdk/feature-management/package.json index 3e77c26..78a4a0f 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": "2.0.0", "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", @@ -26,6 +26,9 @@ "url": "git+https://github.com/microsoft/FeatureManagement-JavaScript.git" }, "license": "MIT", + "publishConfig": { + "tag": "stable" + }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" }, diff --git a/sdk/feature-management/src/telemetry/featureEvaluationEvent.ts b/sdk/feature-management/src/telemetry/featureEvaluationEvent.ts index 2c195c6..b76f46b 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, VariantAssignmentReason } from "../featureManager"; +import { EvaluationResult } from "../featureManager"; import { EVALUATION_EVENT_VERSION } from "../version.js"; const VERSION = "Version"; @@ -10,8 +10,6 @@ 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) { @@ -28,31 +26,6 @@ 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 65fbbf3..aaae469 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 = "2.0.0"; export const EVALUATION_EVENT_VERSION = "1.0.0"; From f247557471e6f3d020493126ccc0d95cc8be421d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:43:29 +0800 Subject: [PATCH 3/4] fix bug (#78) --- sdk/feature-management/.eslintrc | 7 + sdk/feature-management/package-lock.json | 183 +++++++----------- sdk/feature-management/src/featureProvider.ts | 16 +- .../src/filter/TargetingFilter.ts | 8 +- .../src/schema/validator.ts | 97 +++++----- .../test/featureManager.test.ts | 55 +++++- sdk/feature-management/test/noFilters.test.ts | 2 +- .../test/targetingFilter.test.ts | 8 +- 8 files changed, 196 insertions(+), 180 deletions(-) diff --git a/sdk/feature-management/.eslintrc b/sdk/feature-management/.eslintrc index 27a13cc..7956229 100644 --- a/sdk/feature-management/.eslintrc +++ b/sdk/feature-management/.eslintrc @@ -33,6 +33,13 @@ "@typescript-eslint" ], "rules": { + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], "quotes": [ "error", "double", diff --git a/sdk/feature-management/package-lock.json b/sdk/feature-management/package-lock.json index 393b536..a69abd1 100644 --- a/sdk/feature-management/package-lock.json +++ b/sdk/feature-management/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@microsoft/feature-management", - "version": "2.0.0-preview.1", + "version": "2.0.0", "license": "MIT", "devDependencies": { "@playwright/test": "^1.46.1", @@ -1028,9 +1028,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -1294,9 +1294,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -1308,12 +1308,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1355,9 +1355,9 @@ "dev": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -2225,12 +2225,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2259,32 +2259,31 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -2292,48 +2291,41 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -2342,21 +2334,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -2373,23 +2350,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2843,9 +2808,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -3096,9 +3061,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -3176,9 +3141,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" diff --git a/sdk/feature-management/src/featureProvider.ts b/sdk/feature-management/src/featureProvider.ts index 4860421..d29eade 100644 --- a/sdk/feature-management/src/featureProvider.ts +++ b/sdk/feature-management/src/featureProvider.ts @@ -36,9 +36,11 @@ export class ConfigurationMapFeatureFlagProvider implements IFeatureFlagProvider async getFeatureFlags(): Promise { const featureConfig = this.#configuration.get(FEATURE_MANAGEMENT_KEY); - const featureFlag = featureConfig?.[FEATURE_FLAGS_KEY] ?? []; - validateFeatureFlag(featureFlag); - return featureFlag; + const featureFlags = featureConfig?.[FEATURE_FLAGS_KEY] ?? []; + featureFlags.forEach(featureFlag => { + validateFeatureFlag(featureFlag); + }); + return featureFlags; } } @@ -60,8 +62,10 @@ export class ConfigurationObjectFeatureFlagProvider implements IFeatureFlagProvi } async getFeatureFlags(): Promise { - const featureFlag = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; - validateFeatureFlag(featureFlag); - return featureFlag; + const featureFlags = this.#configuration[FEATURE_MANAGEMENT_KEY]?.[FEATURE_FLAGS_KEY] ?? []; + featureFlags.forEach(featureFlag => { + validateFeatureFlag(featureFlag); + }); + return featureFlags; } } diff --git a/sdk/feature-management/src/filter/TargetingFilter.ts b/sdk/feature-management/src/filter/TargetingFilter.ts index b675d87..2d7220e 100644 --- a/sdk/feature-management/src/filter/TargetingFilter.ts +++ b/sdk/feature-management/src/filter/TargetingFilter.ts @@ -30,7 +30,7 @@ export class TargetingFilter implements IFeatureFilter { async evaluate(context: TargetingFilterEvaluationContext, appContext?: ITargetingContext): Promise { const { featureName, parameters } = context; - TargetingFilter.#validateParameters(parameters); + TargetingFilter.#validateParameters(featureName, parameters); if (appContext === undefined) { throw new Error("The app context is required for targeting filter."); @@ -79,15 +79,15 @@ export class TargetingFilter implements IFeatureFilter { return isTargetedPercentile(appContext?.userId, hint, 0, parameters.Audience.DefaultRolloutPercentage); } - static #validateParameters(parameters: TargetingFilterParameters): void { + static #validateParameters(featureName: string, parameters: TargetingFilterParameters): void { if (parameters.Audience.DefaultRolloutPercentage < 0 || parameters.Audience.DefaultRolloutPercentage > 100) { - throw new Error("Audience.DefaultRolloutPercentage must be a number between 0 and 100."); + throw new Error(`Invalid feature flag: ${featureName}. Audience.DefaultRolloutPercentage must be a number between 0 and 100.`); } // validate RolloutPercentage for each group if (parameters.Audience.Groups !== undefined) { for (const group of parameters.Audience.Groups) { if (group.RolloutPercentage < 0 || group.RolloutPercentage > 100) { - throw new Error(`RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`); + throw new Error(`Invalid feature flag: ${featureName}. RolloutPercentage of group ${group.Name} must be a number between 0 and 100.`); } } } diff --git a/sdk/feature-management/src/schema/validator.ts b/sdk/feature-management/src/schema/validator.ts index 97bf5ed..2b805b7 100644 --- a/sdk/feature-management/src/schema/validator.ts +++ b/sdk/feature-management/src/schema/validator.ts @@ -15,172 +15,173 @@ export function validateFeatureFlag(featureFlag: any): void { if (typeof featureFlag.id !== "string") { throw new TypeError("Feature flag 'id' must be a string."); } + if (featureFlag.enabled !== undefined && typeof featureFlag.enabled !== "boolean") { - throw new TypeError("Feature flag 'enabled' must be a boolean."); + throw new TypeError(`Invalid feature flag: ${featureFlag.id}. Feature flag 'enabled' must be a boolean.`); } if (featureFlag.conditions !== undefined) { - validateFeatureEnablementConditions(featureFlag.conditions); + validateFeatureEnablementConditions(featureFlag.id, featureFlag.conditions); } if (featureFlag.variants !== undefined) { - validateVariants(featureFlag.variants); + validateVariants(featureFlag.id, featureFlag.variants); } if (featureFlag.allocation !== undefined) { - validateVariantAllocation(featureFlag.allocation); + validateVariantAllocation(featureFlag.id, featureFlag.allocation); } if (featureFlag.telemetry !== undefined) { - validateTelemetryOptions(featureFlag.telemetry); + validateTelemetryOptions(featureFlag.id, featureFlag.telemetry); } } -function validateFeatureEnablementConditions(conditions: any) { +function validateFeatureEnablementConditions(id: string, conditions: any) { if (typeof conditions !== "object") { - throw new TypeError("Feature flag 'conditions' must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Feature flag 'conditions' must be an object.`); } if (conditions.requirement_type !== undefined && conditions.requirement_type !== "Any" && conditions.requirement_type !== "All") { - throw new TypeError("'requirement_type' must be 'Any' or 'All'."); + throw new TypeError(`Invalid feature flag: ${id}. 'requirement_type' must be 'Any' or 'All'.`); } if (conditions.client_filters !== undefined) { - validateClientFilters(conditions.client_filters); + validateClientFilters(id, conditions.client_filters); } } -function validateClientFilters(client_filters: any) { +function validateClientFilters(id: string, client_filters: any) { if (!Array.isArray(client_filters)) { - throw new TypeError("Feature flag conditions 'client_filters' must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. Feature flag conditions 'client_filters' must be an array.`); } for (const filter of client_filters) { if (typeof filter.name !== "string") { - throw new TypeError("Client filter 'name' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Client filter 'name' must be a string.`); } if (filter.parameters !== undefined && typeof filter.parameters !== "object") { - throw new TypeError("Client filter 'parameters' must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Client filter 'parameters' must be an object.`); } } } -function validateVariants(variants: any) { +function validateVariants(id: string, variants: any) { if (!Array.isArray(variants)) { - throw new TypeError("Feature flag 'variants' must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. Feature flag 'variants' must be an array.`); } for (const variant of variants) { if (typeof variant.name !== "string") { - throw new TypeError("Variant 'name' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'name' must be a string.`); } // skip configuration_value validation as it accepts any type if (variant.status_override !== undefined && typeof variant.status_override !== "string") { - throw new TypeError("Variant 'status_override' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'status_override' must be a string.`); } if (variant.status_override !== undefined && variant.status_override !== "None" && variant.status_override !== "Enabled" && variant.status_override !== "Disabled") { - throw new TypeError("Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'status_override' must be 'None', 'Enabled', or 'Disabled'.`); } } } -function validateVariantAllocation(allocation: any) { +function validateVariantAllocation(id: string, allocation: any) { if (typeof allocation !== "object") { - throw new TypeError("Variant 'allocation' must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'allocation' must be an object.`); } if (allocation.default_when_disabled !== undefined && typeof allocation.default_when_disabled !== "string") { - throw new TypeError("Variant allocation 'default_when_disabled' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Variant allocation 'default_when_disabled' must be a string.`); } if (allocation.default_when_enabled !== undefined && typeof allocation.default_when_enabled !== "string") { - throw new TypeError("Variant allocation 'default_when_enabled' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Variant allocation 'default_when_enabled' must be a string.`); } if (allocation.user !== undefined) { - validateUserVariantAllocation(allocation.user); + validateUserVariantAllocation(id, allocation.user); } if (allocation.group !== undefined) { - validateGroupVariantAllocation(allocation.group); + validateGroupVariantAllocation(id, allocation.group); } if (allocation.percentile !== undefined) { - validatePercentileVariantAllocation(allocation.percentile); + validatePercentileVariantAllocation(id, allocation.percentile); } if (allocation.seed !== undefined && typeof allocation.seed !== "string") { - throw new TypeError("Variant allocation 'seed' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Variant allocation 'seed' must be a string.`); } } -function validateUserVariantAllocation(UserAllocations: any) { +function validateUserVariantAllocation(id: string, UserAllocations: any) { if (!Array.isArray(UserAllocations)) { - throw new TypeError("Variant 'user' allocation must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'user' allocation must be an array.`); } for (const allocation of UserAllocations) { if (typeof allocation !== "object") { - throw new TypeError("Elements in variant 'user' allocation must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Elements in variant 'user' allocation must be an object.`); } if (typeof allocation.variant !== "string") { - throw new TypeError("User allocation 'variant' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. User allocation 'variant' must be a string.`); } if (!Array.isArray(allocation.users)) { - throw new TypeError("User allocation 'users' must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. User allocation 'users' must be an array.`); } for (const user of allocation.users) { if (typeof user !== "string") { - throw new TypeError("Elements in user allocation 'users' must be strings."); + throw new TypeError(`Invalid feature flag: ${id}. Elements in user allocation 'users' must be strings.`); } } } } -function validateGroupVariantAllocation(groupAllocations: any) { +function validateGroupVariantAllocation(id: string, groupAllocations: any) { if (!Array.isArray(groupAllocations)) { - throw new TypeError("Variant 'group' allocation must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'group' allocation must be an array.`); } for (const allocation of groupAllocations) { if (typeof allocation !== "object") { - throw new TypeError("Elements in variant 'group' allocation must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Elements in variant 'group' allocation must be an object.`); } if (typeof allocation.variant !== "string") { - throw new TypeError("Group allocation 'variant' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Group allocation 'variant' must be a string.`); } if (!Array.isArray(allocation.groups)) { - throw new TypeError("Group allocation 'groups' must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. Group allocation 'groups' must be an array.`); } for (const group of allocation.groups) { if (typeof group !== "string") { - throw new TypeError("Elements in group allocation 'groups' must be strings."); + throw new TypeError(`Invalid feature flag: ${id}. Elements in group allocation 'groups' must be strings.`); } } } } -function validatePercentileVariantAllocation(percentileAllocations: any) { +function validatePercentileVariantAllocation(id: string, percentileAllocations: any) { if (!Array.isArray(percentileAllocations)) { - throw new TypeError("Variant 'percentile' allocation must be an array."); + throw new TypeError(`Invalid feature flag: ${id}. Variant 'percentile' allocation must be an array.`); } for (const allocation of percentileAllocations) { if (typeof allocation !== "object") { - throw new TypeError("Elements in variant 'percentile' allocation must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Elements in variant 'percentile' allocation must be an object.`); } if (typeof allocation.variant !== "string") { - throw new TypeError("Percentile allocation 'variant' must be a string."); + throw new TypeError(`Invalid feature flag: ${id}. Percentile allocation 'variant' must be a string.`); } if (typeof allocation.from !== "number" || allocation.from < 0 || allocation.from > 100) { - throw new TypeError("Percentile allocation 'from' must be a number between 0 and 100."); + throw new TypeError(`Invalid feature flag: ${id}. Percentile allocation 'from' must be a number between 0 and 100.`); } if (typeof allocation.to !== "number" || allocation.to < 0 || allocation.to > 100) { - throw new TypeError("Percentile allocation 'to' must be a number between 0 and 100."); + throw new TypeError(`Invalid feature flag: ${id}. Percentile allocation 'to' must be a number between 0 and 100.`); } } } // #endregion // #region Telemetry -function validateTelemetryOptions(telemetry: any) { +function validateTelemetryOptions(id: string, telemetry: any) { if (typeof telemetry !== "object") { - throw new TypeError("Feature flag 'telemetry' must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Feature flag 'telemetry' must be an object.`); } if (telemetry.enabled !== undefined && typeof telemetry.enabled !== "boolean") { - throw new TypeError("Telemetry 'enabled' must be a boolean."); + throw new TypeError(`Invalid feature flag: ${id}. Telemetry 'enabled' must be a boolean.`); } if (telemetry.metadata !== undefined && typeof telemetry.metadata !== "object") { - throw new TypeError("Telemetry 'metadata' must be an object."); + throw new TypeError(`Invalid feature flag: ${id}. Telemetry 'metadata' must be an object.`); } } // #endregion diff --git a/sdk/feature-management/test/featureManager.test.ts b/sdk/feature-management/test/featureManager.test.ts index 37243a9..4c235b6 100644 --- a/sdk/feature-management/test/featureManager.test.ts +++ b/sdk/feature-management/test/featureManager.test.ts @@ -9,31 +9,70 @@ const expect = chai.expect; import { FeatureManager, ConfigurationObjectFeatureFlagProvider, ConfigurationMapFeatureFlagProvider } from "../"; describe("feature manager", () => { - it("should load from json string", () => { + it("should load from json string", async () => { const jsonObject = { "feature_management": { "feature_flags": [ - { "id": "Alpha", "description": "", "enabled": true} + { "id": "Alpha", "description": "", "enabled": true}, + { "id": "Beta", "description": "", "enabled": false} ] } }; const provider = new ConfigurationObjectFeatureFlagProvider(jsonObject); const featureManager = new FeatureManager(provider); - return expect(featureManager.isEnabled("Alpha")).eventually.eq(true); + expect(await featureManager.isEnabled("Alpha")).to.eq(true); + expect(await featureManager.isEnabled("Beta")).to.eq(false); + expect(await featureManager.isEnabled("not existed")).to.eq(false); + + const featureFlags = await featureManager.listFeatureNames(); + expect(featureFlags.length).to.eq(2); + expect(featureFlags[0]).to.eq("Alpha"); + expect(featureFlags[1]).to.eq("Beta"); }); - it("should load from map", () => { + it("should load from map", async () => { const dataSource = new Map(); dataSource.set("feature_management", { feature_flags: [ - { id: "Alpha", enabled: true } + { id: "Alpha", enabled: true }, + { id: "Beta", enabled: false} ], }); const provider = new ConfigurationMapFeatureFlagProvider(dataSource); const featureManager = new FeatureManager(provider); - return expect(featureManager.isEnabled("Alpha")).eventually.eq(true); + expect(await featureManager.isEnabled("Alpha")).to.eq(true); + expect(await featureManager.isEnabled("Beta")).to.eq(false); + expect(await featureManager.isEnabled("not existed")).to.eq(false); + + const featureFlags = await featureManager.listFeatureNames(); + expect(featureFlags.length).to.eq(2); + expect(featureFlags[0]).to.eq("Alpha"); + expect(featureFlags[1]).to.eq("Beta"); + }); + + it("should fail when feature flag is invalid", async () => { + const dataSource = new Map(); + dataSource.set("feature_management", { + feature_flags: [ + { id: "Alpha", enabled: "true" } + ], + }); + const mapProvider = new ConfigurationMapFeatureFlagProvider(dataSource); + let featureManager = new FeatureManager(mapProvider); + await expect(featureManager.isEnabled("Alpha")).to.eventually.be.rejectedWith("Invalid feature flag: Alpha. Feature flag 'enabled' must be a boolean."); + + const jsonObject = { + "feature_management": { + "feature_flags": [ + { "id": 123, "enabled": true} + ] + } + }; + const objectProvider = new ConfigurationObjectFeatureFlagProvider(jsonObject); + featureManager = new FeatureManager(objectProvider); + await expect(featureManager.listFeatureNames()).to.eventually.be.rejectedWith("Feature flag 'id' must be a string."); }); it("should load latest data if source is updated after initialization", () => { @@ -104,8 +143,8 @@ describe("feature manager", () => { const provider = new ConfigurationMapFeatureFlagProvider(dataSource); const featureManager = new FeatureManager(provider); return Promise.all([ - expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("'requirement_type' must be 'Any' or 'All'."), - expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Client filter 'parameters' must be an object.") + expect(featureManager.isEnabled("Gamma")).eventually.rejectedWith("Invalid feature flag: Gamma. 'requirement_type' must be 'Any' or 'All'."), + expect(featureManager.isEnabled("Delta")).eventually.rejectedWith("Invalid feature flag: Delta. Client filter 'parameters' must be an object.") ]); }); diff --git a/sdk/feature-management/test/noFilters.test.ts b/sdk/feature-management/test/noFilters.test.ts index efba770..889a8d9 100644 --- a/sdk/feature-management/test/noFilters.test.ts +++ b/sdk/feature-management/test/noFilters.test.ts @@ -61,7 +61,7 @@ describe("feature flags with no filters", () => { return Promise.all([ expect(featureManager.isEnabled("BooleanTrue")).eventually.eq(true), expect(featureManager.isEnabled("BooleanFalse")).eventually.eq(false), - expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Feature flag 'enabled' must be a boolean."), + expect(featureManager.isEnabled("InvalidEnabled")).eventually.rejectedWith("Invalid feature flag: InvalidEnabled. Feature flag 'enabled' must be a boolean."), expect(featureManager.isEnabled("Minimal")).eventually.eq(true), expect(featureManager.isEnabled("NoEnabled")).eventually.eq(false), expect(featureManager.isEnabled("EmptyConditions")).eventually.eq(true) diff --git a/sdk/feature-management/test/targetingFilter.test.ts b/sdk/feature-management/test/targetingFilter.test.ts index c752f67..91fe81b 100644 --- a/sdk/feature-management/test/targetingFilter.test.ts +++ b/sdk/feature-management/test/targetingFilter.test.ts @@ -83,10 +83,10 @@ describe("targeting filter", () => { const featureManager = new FeatureManager(provider); return Promise.all([ - expect(featureManager.isEnabled("InvalidTargeting1", {})).eventually.rejectedWith("Audience.DefaultRolloutPercentage must be a number between 0 and 100."), - expect(featureManager.isEnabled("InvalidTargeting2", {})).eventually.rejectedWith("Audience.DefaultRolloutPercentage must be a number between 0 and 100."), - expect(featureManager.isEnabled("InvalidTargeting3", {})).eventually.rejectedWith("RolloutPercentage of group Stage1 must be a number between 0 and 100."), - expect(featureManager.isEnabled("InvalidTargeting4", {})).eventually.rejectedWith("RolloutPercentage of group Stage1 must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting1", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting1. Audience.DefaultRolloutPercentage must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting2", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting2. Audience.DefaultRolloutPercentage must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting3", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting3. RolloutPercentage of group Stage1 must be a number between 0 and 100."), + expect(featureManager.isEnabled("InvalidTargeting4", {})).eventually.rejectedWith("Invalid feature flag: InvalidTargeting4. RolloutPercentage of group Stage1 must be a number between 0 and 100."), ]); }); From bcd18b6b4eb2cfcebaa6a9097e1a58cf9c63b187 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:24:50 +0800 Subject: [PATCH 4/4] correct package tag (#80) --- sdk/feature-management-applicationinsights-browser/package.json | 2 +- sdk/feature-management-applicationinsights-node/package.json | 2 +- sdk/feature-management/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/feature-management-applicationinsights-browser/package.json b/sdk/feature-management-applicationinsights-browser/package.json index 3f3cf6d..a524013 100644 --- a/sdk/feature-management-applicationinsights-browser/package.json +++ b/sdk/feature-management-applicationinsights-browser/package.json @@ -25,7 +25,7 @@ }, "license": "MIT", "publishConfig": { - "tag": "stable" + "tag": "latest" }, "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 7d6a1e6..21de0fd 100644 --- a/sdk/feature-management-applicationinsights-node/package.json +++ b/sdk/feature-management-applicationinsights-node/package.json @@ -25,7 +25,7 @@ }, "license": "MIT", "publishConfig": { - "tag": "stable" + "tag": "latest" }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues" diff --git a/sdk/feature-management/package.json b/sdk/feature-management/package.json index 78a4a0f..ea8ed6f 100644 --- a/sdk/feature-management/package.json +++ b/sdk/feature-management/package.json @@ -27,7 +27,7 @@ }, "license": "MIT", "publishConfig": { - "tag": "stable" + "tag": "latest" }, "bugs": { "url": "https://github.com/microsoft/FeatureManagement-JavaScript/issues"