diff --git a/.eslintrc b/.eslintrc index 27a13ccf..7956229a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -33,6 +33,13 @@ "@typescript-eslint" ], "rules": { + "keyword-spacing": [ + "error", + { + "before": true, + "after": true + } + ], "quotes": [ "error", "double", diff --git a/package-lock.json b/package-lock.json index 2b821f0f..5ce195df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/app-configuration-provider", - "version": "2.0.0-preview.2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/app-configuration-provider", - "version": "2.0.0-preview.2", + "version": "2.0.0", "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", @@ -1444,7 +1444,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -2629,7 +2628,6 @@ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2675,8 +2673,7 @@ "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==", - "license": "MIT" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -3482,8 +3479,7 @@ "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" + "dev": true }, "node_modules/wrap-ansi": { "version": "7.0.0", diff --git a/package.json b/package.json index 16148a76..04b4e526 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/app-configuration-provider", - "version": "2.0.0-preview.2", + "version": "2.0.0", "description": "The JavaScript configuration provider for Azure App Configuration", "main": "dist/index.js", "module": "./dist-esm/index.js", diff --git a/rollup.config.mjs b/rollup.config.mjs index 8ad51640..1fa9626f 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -4,7 +4,7 @@ import dts from "rollup-plugin-dts"; export default [ { - external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises"], + external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises", "@microsoft/feature-management"], input: "src/index.ts", output: [ { diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 916ece49..90e283f0 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -33,6 +33,7 @@ import { CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants.js"; +import { FM_PACKAGE_NAME } from "./requestTracing/constants.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; @@ -65,6 +66,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #isInitialLoadCompleted: boolean = false; #isFailoverRequest: boolean = false; #featureFlagTracing: FeatureFlagTracingOptions | undefined; + #fmVersion: string | undefined; // Refresh #refreshInProgress: boolean = false; @@ -184,7 +186,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { initialLoadCompleted: this.#isInitialLoadCompleted, replicaCount: this.#clientManager.getReplicaCount(), isFailoverRequest: this.#isFailoverRequest, - featureFlagTracing: this.#featureFlagTracing + featureFlagTracing: this.#featureFlagTracing, + fmVersion: this.#fmVersion }; } @@ -226,6 +229,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { * Loads the configuration store for the first time. */ async load() { + await this.#inspectFmPackage(); await this.#loadSelectedAndWatchedKeyValues(); if (this.#featureFlagEnabled) { await this.#loadFeatureFlags(); @@ -316,6 +320,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return new Disposable(remove); } + /** + * Inspects the feature management package version. + */ + async #inspectFmPackage() { + if (this.#requestTracingEnabled && !this.#fmVersion) { + try { + // get feature management package version + const fmPackage = await import(FM_PACKAGE_NAME); + this.#fmVersion = fmPackage?.VERSION; + } catch (error) { + // ignore the error + } + } + } + async #refreshTasks(): Promise { const refreshTasks: Promise[] = []; if (this.#refreshEnabled) { @@ -898,14 +917,13 @@ function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelect function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] { if (selectors === undefined || selectors.length === 0) { - // selectors must be explicitly provided. - throw new Error("Feature flag selectors must be provided."); - } else { - selectors.forEach(selector => { - selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; - }); - return getValidSelectors(selectors); + // Default selector: key: *, label: \0 + return [{ keyFilter: `${featureFlagPrefix}${KeyFilter.Any}`, labelFilter: LabelFilter.Null }]; } + selectors.forEach(selector => { + selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; + }); + return getValidSelectors(selectors); } function isFailoverableError(error: any): boolean { diff --git a/src/ConfigurationClientManager.ts b/src/ConfigurationClientManager.ts index 6510b7ff..7ad8e597 100644 --- a/src/ConfigurationClientManager.ts +++ b/src/ConfigurationClientManager.ts @@ -85,10 +85,13 @@ export class ConfigurationClientManager { this.#isFailoverable = false; return; } + if (this.#dns) { + return; + } try { this.#dns = await import("dns/promises"); - }catch (error) { + } catch (error) { this.#isFailoverable = false; console.warn("Failed to load the dns module:", error.message); return; diff --git a/src/featureManagement/FeatureFlagOptions.ts b/src/featureManagement/FeatureFlagOptions.ts index eedf9ec7..55ceda4d 100644 --- a/src/featureManagement/FeatureFlagOptions.ts +++ b/src/featureManagement/FeatureFlagOptions.ts @@ -10,16 +10,15 @@ import { SettingSelector } from "../types.js"; export interface FeatureFlagOptions { /** * Specifies whether feature flags will be loaded from Azure App Configuration. - */ enabled: boolean; /** - * Specifies the selectors used to filter feature flags. + * Specifies what feature flags to include in the configuration provider. * * @remarks * keyFilter of selector will be prefixed with "appconfig.featureflag/" when request is sent. - * If no selectors are specified then no feature flags will be retrieved. + * If no selectors are specified then all feature flags with no label will be included. */ selectors?: SettingSelector[]; diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 5a88b0fc..74ca58bb 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -55,6 +55,10 @@ export const FAILOVER_REQUEST_TAG = "Failover"; export const FEATURES_KEY = "Features"; export const LOAD_BALANCE_CONFIGURED_TAG = "LB"; +// Feature management package +export const FM_PACKAGE_NAME = "@microsoft/feature-management"; +export const FM_VERSION_KEY = "FMJsVer"; + // Feature flag usage tracing export const FEATURE_FILTER_TYPE_KEY = "Filter"; export const CUSTOM_FILTER_KEY = "CSTM"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index b56c460c..2e8b1124 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -27,7 +27,8 @@ import { REPLICA_COUNT_KEY, FAILOVER_REQUEST_TAG, FEATURES_KEY, - LOAD_BALANCE_CONFIGURED_TAG + LOAD_BALANCE_CONFIGURED_TAG, + FM_VERSION_KEY } from "./constants"; export interface RequestTracingOptions { @@ -37,6 +38,7 @@ export interface RequestTracingOptions { replicaCount: number; isFailoverRequest: boolean; featureFlagTracing: FeatureFlagTracingOptions | undefined; + fmVersion: string | undefined; } // Utils @@ -119,6 +121,9 @@ export function createCorrelationContextHeader(requestTracingOptions: RequestTra if (requestTracingOptions.replicaCount > 0) { keyValues.set(REPLICA_COUNT_KEY, requestTracingOptions.replicaCount.toString()); } + if (requestTracingOptions.fmVersion) { + keyValues.set(FM_VERSION_KEY, requestTracingOptions.fmVersion); + } // Compact tags: Features=LB+... if (appConfigOptions?.loadBalancingEnabled) { diff --git a/src/version.ts b/src/version.ts index bb9c7aa8..dba07670 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export const VERSION = "2.0.0-preview.2"; +export const VERSION = "2.0.0"; diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 2544e617..2d6a7e02 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -3,6 +3,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; +import { featureFlagContentType } from "@azure/app-configuration"; import { load } from "./exportedApi.js"; import { MAX_TIME_OUT, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper.js"; chai.use(chaiAsPromised); @@ -49,9 +50,9 @@ const mockedKVs = [{ }, { key: ".appconfig.featureflag/variant", value: sampleVariantValue, - contentType: "application/vnd.microsoft.appconfig.ff+json;charset=utf-8", + contentType: featureFlagContentType, }].map(createMockedKeyValue).concat([ - createMockedFeatureFlag("Beta", { enabled: true }), + createMockedFeatureFlag("FlagWithTestLabel", { enabled: true }, {label: "Test"}), createMockedFeatureFlag("Alpha_1", { enabled: true }), createMockedFeatureFlag("Alpha_2", { enabled: false }), createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "ETag"}), @@ -213,15 +214,22 @@ describe("feature flags", function () { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { featureFlagOptions: { - enabled: true, - selectors: [{ - keyFilter: "*" - }] + enabled: true } }); expect(settings).not.undefined; expect(settings.get("feature_management")).not.undefined; expect(settings.get("feature_management").feature_flags).not.undefined; + // it should only load feature flags with no label by default + expect((settings.get("feature_management").feature_flags as any[]).find(ff => ff.id === "FlagWithTestLabel")).to.be.undefined; + + const settings2 = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ { keyFilter: "*", labelFilter: "Test" } ] + } + }); + expect((settings2.get("feature_management").feature_flags as any[]).find(ff => ff.id === "FlagWithTestLabel")).not.undefined; }); it("should not load feature flags if disabled", async () => { @@ -242,15 +250,6 @@ describe("feature flags", function () { expect(settings.get("feature_management")).undefined; }); - it("should throw error if selectors not specified", async () => { - const connectionString = createMockedConnectionString(); - return expect(load(connectionString, { - featureFlagOptions: { - enabled: true - } - })).eventually.rejectedWith("Feature flag selectors must be provided."); - }); - it("should load feature flags with custom selector", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index a5812694..85f7ac80 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as sinon from "sinon"; -import { AppConfigurationClient, ConfigurationSetting } from "@azure/app-configuration"; +import { AppConfigurationClient, ConfigurationSetting, featureFlagContentType } from "@azure/app-configuration"; import { ClientSecretCredential } from "@azure/identity"; import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets"; import * as uuid from "uuid"; @@ -240,7 +240,7 @@ const createMockedFeatureFlag = (name: string, flagProps?: any, props?: any) => "client_filters": [] } }, flagProps)), - contentType: "application/vnd.microsoft.appconfig.ff+json;charset=utf-8", + contentType: featureFlagContentType, lastModified: new Date(), tags: {}, etag: uuid.v4(),