From 3052c0befce35742cb5374c4cbd469a0a7940f1e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 19 Sep 2024 15:46:52 +0800 Subject: [PATCH 01/10] WIP --- src/AzureAppConfigurationImpl.ts | 45 +++++++++++++++++++++++++++--- src/featureManagement/constants.ts | 7 ++++- src/load.ts | 29 +++++++++++++++---- 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 30eba149..4e4ecbb0 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,7 +9,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; import { Disposable } from "./common/disposable"; -import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME } from "./featureManagement/constants"; +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; import { RefreshTimer } from "./refresh/RefreshTimer"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; @@ -36,6 +36,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #sortedTrimKeyPrefixes: string[] | undefined; readonly #requestTracingEnabled: boolean; #client: AppConfigurationClient; + #clientEndpoint: string | undefined; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; @@ -57,9 +58,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { constructor( client: AppConfigurationClient, + clientEndpoint: string | undefined, options: AzureAppConfigurationOptions | undefined ) { this.#client = client; + this.#clientEndpoint = clientEndpoint; this.#options = options; // Enable request tracing if not opt-out @@ -273,7 +276,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { pageEtags.push(page.etag ?? ""); for (const setting of page.items) { if (isFeatureFlag(setting)) { - featureFlagsMap.set(setting.key, setting.value); + featureFlagsMap.set(setting.key, setting); } } } @@ -281,7 +284,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } // parse feature flags - const featureFlags = Array.from(featureFlagsMap.values()).map(rawFlag => JSON.parse(rawFlag)); + const featureFlags = Array.from(featureFlagsMap.values()).map(setting => this.#parseFeatureflag(setting)); // feature_management is a reserved key, and feature_flags is an array of feature flags this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags }); @@ -532,6 +535,40 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } return response; } + + #parseFeatureflag(setting: ConfigurationSetting): any{ + const rawFlag = setting.value; + if (rawFlag === undefined) { + throw new Error("The value of configuration setting cannot be undefined."); + } + const featureFlag = JSON.parse(rawFlag); + + if (featureFlag[TELEMETRY_KEY_NAME]) { + const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME]; + featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = { + ETAG_KEY_NAME: setting.etag, + FEATURE_FLAG_ID_KEY_NAME: "1", + FEATURE_FLAG_REFERENCE_KEY_NAME: this.#createFeatureFlagReference(setting), + ...(metadata || {}) + }; + } + + console.log(featureFlag); + + return featureFlag; + } + + #calculateFeatureFlagId(setting: ConfigurationSetting): string { + return "" + } + + #createFeatureFlagReference(setting: ConfigurationSetting): string { + let featureFlagReference = `${this.#clientEndpoint}kv/${setting.key}`; + if (setting.label && setting.label.trim().length !== 0) { + featureFlagReference += `?label=${setting.label}`; + } + return featureFlagReference; + } } function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { @@ -575,4 +612,4 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } else { return getValidSelectors(selectors); } -} +} \ No newline at end of file diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index f0082f48..7991aa04 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -2,4 +2,9 @@ // Licensed under the MIT license. export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; -export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; +export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; +export const TELEMETRY_KEY_NAME = "telemetry" +export const METADATA_KEY_NAME = "metadata" +export const ETAG_KEY_NAME = "Etag" +export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId" +export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference" \ No newline at end of file diff --git a/src/load.ts b/src/load.ts index 10dda11e..961bc08e 100644 --- a/src/load.ts +++ b/src/load.ts @@ -32,6 +32,7 @@ export async function load( ): Promise { const startTimestamp = Date.now(); let client: AppConfigurationClient; + let clientEndpoint: string | undefined; let options: AzureAppConfigurationOptions | undefined; // input validation @@ -40,12 +41,13 @@ export async function load( options = credentialOrOptions as AzureAppConfigurationOptions; const clientOptions = getClientOptions(options); client = new AppConfigurationClient(connectionString, clientOptions); + clientEndpoint = parseEndpoint(connectionStringOrEndpoint); } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { - let endpoint = connectionStringOrEndpoint; // ensure string is a valid URL. - if (typeof endpoint === "string") { + if (typeof connectionStringOrEndpoint === "string") { try { - endpoint = new URL(endpoint); + let endpoint = new URL(connectionStringOrEndpoint); + clientEndpoint = endpoint.toString(); } catch (error) { if (error.code === "ERR_INVALID_URL") { throw new Error("Invalid endpoint URL.", { cause: error }); @@ -53,17 +55,19 @@ export async function load( throw error; } } + } else { + clientEndpoint = connectionStringOrEndpoint.toString(); } const credential = credentialOrOptions as TokenCredential; options = appConfigOptions; const clientOptions = getClientOptions(options); - client = new AppConfigurationClient(endpoint.toString(), credential, clientOptions); + client = new AppConfigurationClient(clientEndpoint, credential, clientOptions); } else { throw new Error("A connection string or an endpoint with credential must be specified to create a client."); } try { - const appConfiguration = new AzureAppConfigurationImpl(client, options); + const appConfiguration = new AzureAppConfigurationImpl(client, clientEndpoint, options); await appConfiguration.load(); return appConfiguration; } catch (error) { @@ -104,3 +108,18 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat } }); } + +function parseEndpoint(connectionString: string): string | undefined { + const parts = connectionString.split(";"); + const endpointPart = parts.find(part => part.startsWith("Endpoint=")); + + if (endpointPart) { + let endpoint = endpointPart.split("=")[1]; + if (!endpoint.endsWith("/")) { + endpoint += "/"; + } + return endpoint; + } + + return undefined; +} \ No newline at end of file From d139146fab6e889c9daca04e6918a4bfe7a7ed48 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 20 Sep 2024 14:25:56 +0800 Subject: [PATCH 02/10] populate feature flag id --- rollup.config.mjs | 2 +- src/AzureAppConfigurationImpl.ts | 63 +++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index b2e87c64..1cd15dfc 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"], + external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto"], input: "src/index.ts", output: [ { diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 4e4ecbb0..1bb1a41d 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -284,7 +284,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } // parse feature flags - const featureFlags = Array.from(featureFlagsMap.values()).map(setting => this.#parseFeatureflag(setting)); + const featureFlags = await Promise.all( + Array.from(featureFlagsMap.values()).map(setting => this.#parseFeatureFlag(setting)) + ); // feature_management is a reserved key, and feature_flags is an array of feature flags this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags }); @@ -536,7 +538,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return response; } - #parseFeatureflag(setting: ConfigurationSetting): any{ + async #parseFeatureFlag(setting: ConfigurationSetting): Promise{ const rawFlag = setting.value; if (rawFlag === undefined) { throw new Error("The value of configuration setting cannot be undefined."); @@ -546,20 +548,63 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (featureFlag[TELEMETRY_KEY_NAME]) { const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME]; featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = { - ETAG_KEY_NAME: setting.etag, - FEATURE_FLAG_ID_KEY_NAME: "1", - FEATURE_FLAG_REFERENCE_KEY_NAME: this.#createFeatureFlagReference(setting), + [ETAG_KEY_NAME]: setting.etag, + [FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting), + [FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting), ...(metadata || {}) }; } - - console.log(featureFlag); return featureFlag; } - #calculateFeatureFlagId(setting: ConfigurationSetting): string { - return "" + async #calculateFeatureFlagId(setting: ConfigurationSetting): Promise { + let crypto; + + // Check for browser environment + if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) { + crypto = window.crypto; + } + // Check for Node.js environment + else if (typeof global !== "undefined" && global.crypto) { + crypto = global.crypto; + } + // Fallback to native Node.js crypto module + else { + try { + if (typeof module !== "undefined" && module.exports) { + crypto = require("crypto"); + } + else { + crypto = await import("crypto"); + } + } catch (error) { + console.error("Failed to load the crypto module:", error.message); + throw error; + } + } + + let baseString = `${setting.key}\n`; + if (setting.label && setting.label.trim().length !== 0) { + baseString += `${setting.label}`; + } + + // Convert to UTF-8 encoded bytes + const data = new TextEncoder().encode(baseString); + + // In the browser, use crypto.subtle.digest + if (crypto.subtle) { + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = new Uint8Array(hashBuffer); + const base64String = btoa(String.fromCharCode(...hashArray)); + const base64urlString = base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + return base64urlString + } + // In Node.js, use the crypto module's hash function + else { + const hash = crypto.createHash("sha256").update(data).digest(); + return hash.toString("base64url") + } } #createFeatureFlagReference(setting: ConfigurationSetting): string { From e9efb51956f7cad703c0c8805a6f70fd3606be0e Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 20 Sep 2024 14:32:07 +0800 Subject: [PATCH 03/10] fix lint --- src/AzureAppConfigurationImpl.ts | 10 +++++----- src/featureManagement/constants.ts | 10 +++++----- src/load.ts | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 1bb1a41d..7d068a0f 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -538,7 +538,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return response; } - async #parseFeatureFlag(setting: ConfigurationSetting): Promise{ + async #parseFeatureFlag(setting: ConfigurationSetting): Promise { const rawFlag = setting.value; if (rawFlag === undefined) { throw new Error("The value of configuration setting cannot be undefined."); @@ -597,13 +597,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = new Uint8Array(hashBuffer); const base64String = btoa(String.fromCharCode(...hashArray)); - const base64urlString = base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); - return base64urlString + const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + return base64urlString; } // In Node.js, use the crypto module's hash function else { const hash = crypto.createHash("sha256").update(data).digest(); - return hash.toString("base64url") + return hash.toString("base64url"); } } @@ -657,4 +657,4 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } else { return getValidSelectors(selectors); } -} \ No newline at end of file +} diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index 7991aa04..0fe5b4c3 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -3,8 +3,8 @@ export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; -export const TELEMETRY_KEY_NAME = "telemetry" -export const METADATA_KEY_NAME = "metadata" -export const ETAG_KEY_NAME = "Etag" -export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId" -export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference" \ No newline at end of file +export const TELEMETRY_KEY_NAME = "telemetry"; +export const METADATA_KEY_NAME = "metadata"; +export const ETAG_KEY_NAME = "Etag"; +export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId"; +export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; diff --git a/src/load.ts b/src/load.ts index 961bc08e..4a924176 100644 --- a/src/load.ts +++ b/src/load.ts @@ -46,8 +46,8 @@ export async function load( // ensure string is a valid URL. if (typeof connectionStringOrEndpoint === "string") { try { - let endpoint = new URL(connectionStringOrEndpoint); - clientEndpoint = endpoint.toString(); + const endpointUrl = new URL(connectionStringOrEndpoint); + clientEndpoint = endpointUrl.toString(); } catch (error) { if (error.code === "ERR_INVALID_URL") { throw new Error("Invalid endpoint URL.", { cause: error }); @@ -112,7 +112,7 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat function parseEndpoint(connectionString: string): string | undefined { const parts = connectionString.split(";"); const endpointPart = parts.find(part => part.startsWith("Endpoint=")); - + if (endpointPart) { let endpoint = endpointPart.split("=")[1]; if (!endpoint.endsWith("/")) { @@ -122,4 +122,4 @@ function parseEndpoint(connectionString: string): string | undefined { } return undefined; -} \ No newline at end of file +} From d6c2e41caeac0dd0b5ea090c03f8e7339cf44b60 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 20 Sep 2024 15:13:29 +0800 Subject: [PATCH 04/10] populate only when telemetry is enabled --- src/AzureAppConfigurationImpl.ts | 6 +++--- src/featureManagement/constants.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 7d068a0f..3de45041 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,7 +9,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions"; import { Disposable } from "./common/disposable"; -import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants"; +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, ENABLED_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; import { RefreshTimer } from "./refresh/RefreshTimer"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; @@ -545,7 +545,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } const featureFlag = JSON.parse(rawFlag); - if (featureFlag[TELEMETRY_KEY_NAME]) { + if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) { const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME]; featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = { [ETAG_KEY_NAME]: setting.etag, @@ -657,4 +657,4 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel } else { return getValidSelectors(selectors); } -} +} diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index 0fe5b4c3..fe4d8cdf 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -4,7 +4,8 @@ export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; export const TELEMETRY_KEY_NAME = "telemetry"; +export const ENABLED_KEY_NAME = "enabled"; export const METADATA_KEY_NAME = "metadata"; export const ETAG_KEY_NAME = "Etag"; export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId"; -export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; +export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference"; From 5ce3b1e4f2ef913ce6cdb6d3df52c7db448b1538 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Fri, 20 Sep 2024 15:40:49 +0800 Subject: [PATCH 05/10] add testcase --- test/featureFlag.test.ts | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 5022c0fe..bdad42f3 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -4,7 +4,7 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { load } from "./exportedApi"; -import { createMockedConnectionString, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper"; +import { createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, restoreMocks } from "./utils/testHelper"; chai.use(chaiAsPromised); const expect = chai.expect; @@ -54,6 +54,8 @@ const mockedKVs = [{ createMockedFeatureFlag("Beta", { enabled: true }), createMockedFeatureFlag("Alpha_1", { enabled: true }), createMockedFeatureFlag("Alpha_2", { enabled: false }), + createMockedFeatureFlag("Telemetry_1", { enabled: true, telemetry: { enabled: true } }, { etag: "Etag"}), + createMockedFeatureFlag("Telemetry_2", { enabled: true, telemetry: { enabled: true } }, { etag: "Etag", label: "Test"}) ]); describe("feature flags", function () { @@ -158,4 +160,44 @@ describe("feature flags", function () { expect(variant.telemetry).not.undefined; }); + it("should populate telemetry metadata", async () => { + const connectionString = createMockedConnectionString(); + let settings = await load(connectionString, { + featureFlagOptions: { + enabled: true, + selectors: [ + { + keyFilter: "Telemetry_1" + }, + { + keyFilter: "Telemetry_2", + labelFilter: "Test" + } + ] + } + }); + expect(settings).not.undefined; + expect(settings.get("feature_management")).not.undefined; + const featureFlags = settings.get("feature_management").feature_flags; + expect(featureFlags).not.undefined; + expect((featureFlags as []).length).equals(2); + + let featureFlag = featureFlags[0]; + expect(featureFlag).not.undefined; + expect(featureFlag.id).equals("Telemetry_1"); + expect(featureFlag.telemetry).not.undefined; + expect(featureFlag.telemetry.enabled).equals(true); + expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); + expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("krkOsu9dVV9huwbQDPR6gkV_2T0buWxOCS-nNsj5-6g"); + expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_1`); + + featureFlag = featureFlags[1]; + expect(featureFlag).not.undefined; + expect(featureFlag.id).equals("Telemetry_2"); + expect(featureFlag.telemetry).not.undefined; + expect(featureFlag.telemetry.enabled).equals(true); + expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); + expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("Rc8Am7HIGDT7HC5Ovs3wKN_aGaaK_Uz1mH2e11gaK0o"); + expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_2?label=Test`); + }); }); From a5556f03bd6dd728792c3e59777091d96953efc9 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Tue, 24 Sep 2024 18:32:43 +0800 Subject: [PATCH 06/10] fix lint --- test/featureFlag.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index bdad42f3..05537fcc 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -162,7 +162,7 @@ describe("feature flags", function () { it("should populate telemetry metadata", async () => { const connectionString = createMockedConnectionString(); - let settings = await load(connectionString, { + const settings = await load(connectionString, { featureFlagOptions: { enabled: true, selectors: [ @@ -190,7 +190,7 @@ describe("feature flags", function () { expect(featureFlag.telemetry.metadata.Etag).equals("Etag"); expect(featureFlag.telemetry.metadata.FeatureFlagId).equals("krkOsu9dVV9huwbQDPR6gkV_2T0buWxOCS-nNsj5-6g"); expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/kv/.appconfig.featureflag/Telemetry_1`); - + featureFlag = featureFlags[1]; expect(featureFlag).not.undefined; expect(featureFlag.id).equals("Telemetry_2"); From f524c354717f5b51680c6b2bcb5611b3dae834a1 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 12 Oct 2024 00:30:42 +0800 Subject: [PATCH 07/10] update --- src/AzureAppConfigurationImpl.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 3de45041..7f5600fc 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -258,8 +258,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #loadFeatureFlags() { - // Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting - const featureFlagsMap = new Map(); + const featureFlagSettings: ConfigurationSetting[] = []; for (const selector of this.#featureFlagSelectors) { const listOptions: ListConfigurationSettingsOptions = { keyFilter: `${featureFlagPrefix}${selector.keyFilter}`, @@ -276,7 +275,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { pageEtags.push(page.etag ?? ""); for (const setting of page.items) { if (isFeatureFlag(setting)) { - featureFlagsMap.set(setting.key, setting); + featureFlagSettings.push(setting); } } } @@ -285,7 +284,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // parse feature flags const featureFlags = await Promise.all( - Array.from(featureFlagsMap.values()).map(setting => this.#parseFeatureFlag(setting)) + featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) ); // feature_management is a reserved key, and feature_flags is an array of feature flags From 81d006c509ae6824ca5b0a8912909de4bd2b0066 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 12 Oct 2024 00:41:55 +0800 Subject: [PATCH 08/10] use window.btoa --- src/AzureAppConfigurationImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 7f5600fc..df97e24e 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -595,7 +595,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (crypto.subtle) { const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = new Uint8Array(hashBuffer); - const base64String = btoa(String.fromCharCode(...hashArray)); + const base64String = window.btoa(String.fromCharCode(...hashArray)); const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return base64urlString; } From 75c1d5535c3c22ed4d9a0400d54ec53c2afaee28 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Sat, 12 Oct 2024 00:48:16 +0800 Subject: [PATCH 09/10] rename method --- src/load.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/load.ts b/src/load.ts index 4a924176..fffd09b8 100644 --- a/src/load.ts +++ b/src/load.ts @@ -41,7 +41,7 @@ export async function load( options = credentialOrOptions as AzureAppConfigurationOptions; const clientOptions = getClientOptions(options); client = new AppConfigurationClient(connectionString, clientOptions); - clientEndpoint = parseEndpoint(connectionStringOrEndpoint); + clientEndpoint = getEndpoint(connectionStringOrEndpoint); } else if ((connectionStringOrEndpoint instanceof URL || typeof connectionStringOrEndpoint === "string") && instanceOfTokenCredential(credentialOrOptions)) { // ensure string is a valid URL. if (typeof connectionStringOrEndpoint === "string") { @@ -109,7 +109,7 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat }); } -function parseEndpoint(connectionString: string): string | undefined { +function getEndpoint(connectionString: string): string | undefined { const parts = connectionString.split(";"); const endpointPart = parts.find(part => part.startsWith("Endpoint=")); From f92e81bb9ec11d3ea035ac6b8a8b1d7d8d1d15b8 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Tue, 15 Oct 2024 12:30:03 +0800 Subject: [PATCH 10/10] revert add window. --- src/AzureAppConfigurationImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index df97e24e..7f5600fc 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -595,7 +595,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (crypto.subtle) { const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = new Uint8Array(hashBuffer); - const base64String = window.btoa(String.fromCharCode(...hashArray)); + const base64String = btoa(String.fromCharCode(...hashArray)); const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return base64urlString; }