Skip to content
Merged
7 changes: 7 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
"@typescript-eslint"
],
"rules": {
"keyword-spacing": [
"error",
{
"before": true,
"after": true
}
],
"quotes": [
"error",
"double",
Expand Down
12 changes: 4 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
34 changes: 26 additions & 8 deletions src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
};
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<void> {
const refreshTasks: Promise<boolean>[] = [];
if (this.#refreshEnabled) {
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion src/ConfigurationClientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 2 additions & 3 deletions src/featureManagement/FeatureFlagOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];

Expand Down
4 changes: 4 additions & 0 deletions src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
7 changes: 6 additions & 1 deletion src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -37,6 +38,7 @@ export interface RequestTracingOptions {
replicaCount: number;
isFailoverRequest: boolean;
featureFlagTracing: FeatureFlagTracingOptions | undefined;
fmVersion: string | undefined;
}

// Utils
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -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";
29 changes: 14 additions & 15 deletions test/featureFlag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"}),
Expand Down Expand Up @@ -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<any>("feature_management").feature_flags).not.undefined;
// it should only load feature flags with no label by default
expect((settings.get<any>("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<any>("feature_management").feature_flags as any[]).find(ff => ff.id === "FlagWithTestLabel")).not.undefined;
});

it("should not load feature flags if disabled", async () => {
Expand All @@ -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, {
Expand Down
4 changes: 2 additions & 2 deletions test/utils/testHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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(),
Expand Down