From 0027254e526a08ee42db970ed28c4cdfcce8349f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:05:55 +0800 Subject: [PATCH 1/7] update version in package-lock.json (#128) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 73c45442..ac1204cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@azure/app-configuration-provider", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", From 3f2ffeb1abd7cad8a19a2a7006f5febf7b19f60a Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:52:41 +0800 Subject: [PATCH 2/7] Avoid multiple refresh (#136) * add lock for refresh operation * bind context * fix lint * update * simplify * add more testcases * add more comments --- package.json | 2 +- src/AzureAppConfigurationImpl.ts | 14 +++++ test/featureFlag.test.ts | 2 +- test/json.test.ts | 8 +-- test/keyvault.test.ts | 2 +- test/load.test.ts | 2 +- test/refresh.test.ts | 98 ++++++++++++++++++++++++++++---- test/requestTracing.test.ts | 4 +- test/utils/testHelper.ts | 15 +++-- 9 files changed, 123 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 0b493ea3..b194a724 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev": "rollup --config --watch", "lint": "eslint src/ test/", "fix-lint": "eslint src/ test/ --fix", - "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel" + "test": "mocha out/test/refresh.test.{js,cjs,mjs} --parallel" }, "repository": { "type": "git", diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 1bbf0779..1541c40b 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -40,6 +40,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #isInitialLoadCompleted: boolean = false; // Refresh + #refreshInProgress: boolean = false; + #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; #onRefreshListeners: Array<() => any> = []; /** @@ -350,6 +352,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { throw new Error("Refresh is not enabled for key-values or feature flags."); } + if (this.#refreshInProgress) { + return; + } + this.#refreshInProgress = true; + try { + await this.#refreshTasks(); + } finally { + this.#refreshInProgress = false; + } + } + + async #refreshTasks(): Promise { const refreshTasks: Promise[] = []; if (this.#refreshEnabled) { refreshTasks.push(this.#refreshKeyValues()); diff --git a/test/featureFlag.test.ts b/test/featureFlag.test.ts index 1bf8eae2..897efe97 100644 --- a/test/featureFlag.test.ts +++ b/test/featureFlag.test.ts @@ -60,7 +60,7 @@ describe("feature flags", function () { this.timeout(10000); before(() => { - mockAppConfigurationClientListConfigurationSettings(mockedKVs); + mockAppConfigurationClientListConfigurationSettings([mockedKVs]); }); after(() => { diff --git a/test/json.test.ts b/test/json.test.ts index c139d53b..47a3f670 100644 --- a/test/json.test.ts +++ b/test/json.test.ts @@ -20,7 +20,7 @@ describe("json", function () { }); it("should load and parse if content type is application/json", async () => { - mockAppConfigurationClientListConfigurationSettings([jsonKeyValue]); + mockAppConfigurationClientListConfigurationSettings([[jsonKeyValue]]); const connectionString = createMockedConnectionString(); const settings = await load(connectionString); @@ -34,7 +34,7 @@ describe("json", function () { }); it("should not parse key-vault reference", async () => { - mockAppConfigurationClientListConfigurationSettings([jsonKeyValue, keyVaultKeyValue]); + mockAppConfigurationClientListConfigurationSettings([[jsonKeyValue, keyVaultKeyValue]]); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -50,7 +50,7 @@ describe("json", function () { }); it("should parse different kinds of legal values", async () => { - mockAppConfigurationClientListConfigurationSettings([ + mockAppConfigurationClientListConfigurationSettings([[ /** * A JSON value MUST be an object, array, number, or string, false, null, true * See https://www.ietf.org/rfc/rfc4627.txt @@ -69,7 +69,7 @@ describe("json", function () { createMockedJsonKeyValue("json.settings.emptyString", ""), // should fail JSON.parse and use string value as fallback createMockedJsonKeyValue("json.settings.illegalString", "[unclosed"), // should fail JSON.parse - ]); + ]]); const connectionString = createMockedConnectionString(); const settings = await load(connectionString); expect(settings).not.undefined; diff --git a/test/keyvault.test.ts b/test/keyvault.test.ts index cf01235c..2877243b 100644 --- a/test/keyvault.test.ts +++ b/test/keyvault.test.ts @@ -19,7 +19,7 @@ const mockedData = [ function mockAppConfigurationClient() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const kvs = mockedData.map(([key, vaultUri, _value]) => createMockedKeyVaultReference(key, vaultUri)); - mockAppConfigurationClientListConfigurationSettings(kvs); + mockAppConfigurationClientListConfigurationSettings([kvs]); } function mockNewlyCreatedKeyVaultSecretClients() { diff --git a/test/load.test.ts b/test/load.test.ts index ce22b1a8..6d2c94b8 100644 --- a/test/load.test.ts +++ b/test/load.test.ts @@ -80,7 +80,7 @@ describe("load", function () { this.timeout(10000); before(() => { - mockAppConfigurationClientListConfigurationSettings(mockedKVs); + mockAppConfigurationClientListConfigurationSettings([mockedKVs]); }); after(() => { diff --git a/test/refresh.test.ts b/test/refresh.test.ts index fcffaee5..5fbeb973 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -23,6 +23,15 @@ function addSetting(key: string, value: any) { mockedKVs.push(createMockedKeyValue({ key, value })); } +let listKvRequestCount = 0; +const listKvCallback = () => { + listKvRequestCount++; +}; +let getKvRequestCount = 0; +const getKvCallback = () => { + getKvRequestCount++; +}; + describe("dynamic refresh", function () { this.timeout(10000); @@ -32,12 +41,14 @@ describe("dynamic refresh", function () { { value: "40", key: "app.settings.fontSize" }, { value: "30", key: "app.settings.fontSize", label: "prod" } ].map(createMockedKeyValue); - mockAppConfigurationClientListConfigurationSettings(mockedKVs); - mockAppConfigurationClientGetConfigurationSetting(mockedKVs); + mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback); + mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvCallback); }); afterEach(() => { restoreMocks(); + listKvRequestCount = 0; + getKvRequestCount = 0; }); it("should throw error when refresh is not enabled but refresh is called", async () => { @@ -139,6 +150,8 @@ describe("dynamic refresh", function () { ] } }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); expect(settings).not.undefined; expect(settings.get("app.settings.fontColor")).eq("red"); expect(settings.get("app.settings.fontSize")).eq("40"); @@ -149,10 +162,14 @@ describe("dynamic refresh", function () { // within refreshInterval, should not really refresh await settings.refresh(); expect(settings.get("app.settings.fontColor")).eq("red"); + expect(listKvRequestCount).eq(1); // no more request should be sent during the refresh interval + expect(getKvRequestCount).eq(0); // no more request should be sent during the refresh interval // after refreshInterval, should really refresh await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(2); + expect(getKvRequestCount).eq(1); expect(settings.get("app.settings.fontColor")).eq("blue"); }); @@ -167,6 +184,8 @@ describe("dynamic refresh", function () { ] } }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); expect(settings).not.undefined; expect(settings.get("app.settings.fontColor")).eq("red"); expect(settings.get("app.settings.fontSize")).eq("40"); @@ -174,11 +193,13 @@ describe("dynamic refresh", function () { // delete setting 'app.settings.fontColor' const newMockedKVs = mockedKVs.filter(elem => elem.key !== "app.settings.fontColor"); restoreMocks(); - mockAppConfigurationClientListConfigurationSettings(newMockedKVs); - mockAppConfigurationClientGetConfigurationSetting(newMockedKVs); + mockAppConfigurationClientListConfigurationSettings([newMockedKVs], listKvCallback); + mockAppConfigurationClientGetConfigurationSetting(newMockedKVs, getKvCallback); await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(2); + expect(getKvRequestCount).eq(2); // one conditional request to detect change and one request as part of loading all kvs (because app.settings.fontColor doesn't exist in the response of listKv request) expect(settings.get("app.settings.fontColor")).eq(undefined); }); @@ -193,6 +214,8 @@ describe("dynamic refresh", function () { ] } }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); expect(settings).not.undefined; expect(settings.get("app.settings.fontColor")).eq("red"); expect(settings.get("app.settings.fontSize")).eq("40"); @@ -200,6 +223,8 @@ describe("dynamic refresh", function () { updateSetting("app.settings.fontSize", "50"); // unwatched setting await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(1); expect(settings.get("app.settings.fontSize")).eq("40"); }); @@ -215,6 +240,8 @@ describe("dynamic refresh", function () { ] } }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); expect(settings).not.undefined; expect(settings.get("app.settings.fontColor")).eq("red"); expect(settings.get("app.settings.fontSize")).eq("40"); @@ -224,6 +251,8 @@ describe("dynamic refresh", function () { updateSetting("app.settings.fontSize", "50"); await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(2); + expect(getKvRequestCount).eq(2); // two getKv request for two watched settings expect(settings.get("app.settings.fontSize")).eq("50"); expect(settings.get("app.settings.bgColor")).eq("white"); }); @@ -309,6 +338,8 @@ describe("dynamic refresh", function () { ] } }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(1); // app.settings.bgColor doesn't exist in the response of listKv request, so an additional getKv request is made to get it. expect(settings).not.undefined; expect(settings.get("app.settings.fontColor")).eq("red"); expect(settings.get("app.settings.fontSize")).eq("40"); @@ -317,10 +348,45 @@ describe("dynamic refresh", function () { updateSetting("app.settings.fontColor", "blue"); await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(2); // should not refresh expect(settings.get("app.settings.fontColor")).eq("red"); }); + it("should not refresh any more when there is refresh in progress", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + refreshOptions: { + enabled: true, + refreshIntervalInMs: 2000, + watchedSettings: [ + { key: "app.settings.fontColor" } + ] + } + }); + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(0); + expect(settings).not.undefined; + expect(settings.get("app.settings.fontColor")).eq("red"); + + // change setting + updateSetting("app.settings.fontColor", "blue"); + + // after refreshInterval, should really refresh + await sleepInMs(2 * 1000 + 1); + for (let i = 0; i < 5; i++) { // in practice, refresh should not be used in this way + settings.refresh(); // refresh "concurrently" + } + expect(listKvRequestCount).to.be.at.most(2); + expect(getKvRequestCount).to.be.at.most(1); + + await sleepInMs(1000); // wait for all 5 refresh attempts to finish + + expect(listKvRequestCount).eq(2); + expect(getKvRequestCount).eq(1); + expect(settings.get("app.settings.fontColor")).eq("blue"); + }); }); describe("dynamic refresh feature flags", function () { @@ -331,14 +397,16 @@ describe("dynamic refresh feature flags", function () { afterEach(() => { restoreMocks(); + listKvRequestCount = 0; + getKvRequestCount = 0; }); it("should refresh feature flags when enabled", async () => { mockedKVs = [ createMockedFeatureFlag("Beta", { enabled: true }) ]; - mockAppConfigurationClientListConfigurationSettings(mockedKVs); - mockAppConfigurationClientGetConfigurationSetting(mockedKVs); + mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback); + mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvCallback); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -353,6 +421,8 @@ describe("dynamic refresh feature flags", function () { } } }); + expect(listKvRequestCount).eq(2); // one listKv request for kvs and one listKv request for feature flags + expect(getKvRequestCount).eq(0); expect(settings).not.undefined; expect(settings.get("feature_management")).not.undefined; expect(settings.get("feature_management").feature_flags).not.undefined; @@ -371,6 +441,8 @@ describe("dynamic refresh feature flags", function () { await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(4); // 2 + 2 more requests: one conditional request to detect change and one request to reload all feature flags + expect(getKvRequestCount).eq(0); expect(settings.get("feature_management").feature_flags[0].id).eq("Beta"); expect(settings.get("feature_management").feature_flags[0].enabled).eq(false); @@ -387,8 +459,8 @@ describe("dynamic refresh feature flags", function () { createMockedFeatureFlag("Beta_1", { enabled: true }), createMockedFeatureFlag("Beta_2", { enabled: true }), ]; - mockAppConfigurationClientListConfigurationSettings(page1, page2); - mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2]); + mockAppConfigurationClientListConfigurationSettings([page1, page2], listKvCallback); + mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2], getKvCallback); const connectionString = createMockedConnectionString(); const settings = await load(connectionString, { @@ -403,6 +475,8 @@ describe("dynamic refresh feature flags", function () { } } }); + expect(listKvRequestCount).eq(2); + expect(getKvRequestCount).eq(0); let refreshSuccessfulCount = 0; settings.onRefresh(() => { @@ -411,16 +485,20 @@ describe("dynamic refresh feature flags", function () { await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(3); // one conditional request to detect change + expect(getKvRequestCount).eq(0); expect(refreshSuccessfulCount).eq(0); // no change in feature flags, because page etags are the same. // change feature flag Beta_1 to false page2[0] = createMockedFeatureFlag("Beta_1", { enabled: false }); restoreMocks(); - mockAppConfigurationClientListConfigurationSettings(page1, page2); - mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2]); + mockAppConfigurationClientListConfigurationSettings([page1, page2], listKvCallback); + mockAppConfigurationClientGetConfigurationSetting([...page1, ...page2], getKvCallback); await sleepInMs(2 * 1000 + 1); await settings.refresh(); + expect(listKvRequestCount).eq(5); // 3 + 2 more requests: one conditional request to detect change and one request to reload all feature flags + expect(getKvRequestCount).eq(0); expect(refreshSuccessfulCount).eq(1); // change in feature flags, because page etags are different. }); }); diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index d4e7edcf..a64d2c4a 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -123,10 +123,10 @@ describe("request tracing", function () { }); it("should have request type in correlation-context header when refresh is enabled", async () => { - mockAppConfigurationClientListConfigurationSettings([{ + mockAppConfigurationClientListConfigurationSettings([[{ key: "app.settings.fontColor", value: "red" - }].map(createMockedKeyValue)); + }].map(createMockedKeyValue)]); const settings = await load(createMockedConnectionString(fakeEndpoint), { clientOptions, diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 6e787dd7..bf05882d 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -42,13 +42,16 @@ function _filterKVs(unfilteredKvs: ConfigurationSetting[], listOptions: any) { * Mocks the listConfigurationSettings method of AppConfigurationClient to return the provided pages of ConfigurationSetting. * E.g. * - mockAppConfigurationClientListConfigurationSettings([item1, item2, item3]) // single page - * - mockAppConfigurationClientListConfigurationSettings([item1, item2], [item3], [item4]) // multiple pages * * @param pages List of pages, each page is a list of ConfigurationSetting */ -function mockAppConfigurationClientListConfigurationSettings(...pages: ConfigurationSetting[][]) { +function mockAppConfigurationClientListConfigurationSettings(pages: ConfigurationSetting[][], customCallback?: (listOptions) => any) { sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings").callsFake((listOptions) => { + if (customCallback) { + customCallback(listOptions); + } + let kvs = _filterKVs(pages.flat(), listOptions); const mockIterator: AsyncIterableIterator & { byPage(): AsyncIterableIterator } = { [Symbol.asyncIterator](): AsyncIterableIterator { @@ -94,8 +97,12 @@ function mockAppConfigurationClientListConfigurationSettings(...pages: Configura }); } -function mockAppConfigurationClientGetConfigurationSetting(kvList) { +function mockAppConfigurationClientGetConfigurationSetting(kvList, customCallback?: (options) => any) { sinon.stub(AppConfigurationClient.prototype, "getConfigurationSetting").callsFake((settingId, options) => { + if (customCallback) { + customCallback(options); + } + const found = kvList.find(elem => elem.key === settingId.key && elem.label === settingId.label); if (found) { if (options?.onlyIfChanged && settingId.etag === found.etag) { @@ -210,4 +217,4 @@ export { createMockedFeatureFlag, sleepInMs -}; +}; From ad0290b98ead61165d376486053054de1a7103bb Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:10:04 +0800 Subject: [PATCH 3/7] small fix (#138) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b194a724..0b493ea3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dev": "rollup --config --watch", "lint": "eslint src/ test/", "fix-lint": "eslint src/ test/ --fix", - "test": "mocha out/test/refresh.test.{js,cjs,mjs} --parallel" + "test": "mocha out/test/*.test.{js,cjs,mjs} --parallel" }, "repository": { "type": "git", From 48585042a65f9174f4d435834928086842d77205 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:28:11 +0800 Subject: [PATCH 4/7] Add feature filter usage tracing (#108) * wip * WIP * add integration test * fix lint * update port * update port * update * update * avoid create new featureFlagTracing object when request tracing is disabled * update * update * add variant and telemetry tracing --- src/AzureAppConfigurationImpl.ts | 50 +++++- src/featureManagement/constants.ts | 13 +- .../FeatureFlagTracingOptions.ts | 95 ++++++++++ src/requestTracing/constants.ts | 15 +- src/requestTracing/utils.ts | 24 ++- test/requestTracing.test.ts | 165 ++++++++++++++++-- test/utils/testHelper.ts | 16 ++ 7 files changed, 351 insertions(+), 27 deletions(-) create mode 100644 src/requestTracing/FeatureFlagTracingOptions.ts diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 1541c40b..4523642d 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,10 +9,11 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js"; import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js"; import { Disposable } from "./common/disposable.js"; -import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME } from "./featureManagement/constants.js"; +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME, TELEMETRY_KEY_NAME, VARIANTS_KEY_NAME, ALLOCATION_KEY_NAME, SEED_KEY_NAME, NAME_KEY_NAME, ENABLED_KEY_NAME } from "./featureManagement/constants.js"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js"; import { RefreshTimer } from "./refresh/RefreshTimer.js"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js"; +import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; type PagedSettingSelector = SettingSelector & { @@ -38,6 +39,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #client: AppConfigurationClient; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; + #featureFlagTracing: FeatureFlagTracingOptions | undefined; // Refresh #refreshInProgress: boolean = false; @@ -66,6 +68,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // Enable request tracing if not opt-out this.#requestTracingEnabled = requestTracingEnabled(); + if (this.#requestTracingEnabled) { + this.#featureFlagTracing = new FeatureFlagTracingOptions(); + } if (options?.trimKeyPrefixes) { this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a)); @@ -175,7 +180,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return { requestTracingEnabled: this.#requestTracingEnabled, initialLoadCompleted: this.#isInitialLoadCompleted, - appConfigOptions: this.#options + appConfigOptions: this.#options, + featureFlagTracingOptions: this.#featureFlagTracing }; } @@ -257,8 +263,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}`, @@ -275,15 +280,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { pageEtags.push(page.etag ?? ""); for (const setting of page.items) { if (isFeatureFlag(setting)) { - featureFlagsMap.set(setting.key, setting.value); + featureFlagSettings.push(setting); } } } selector.pageEtags = pageEtags; } + if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { + this.#featureFlagTracing.resetFeatureFlagTracing(); + } + // parse feature flags - const featureFlags = Array.from(featureFlagsMap.values()).map(rawFlag => JSON.parse(rawFlag)); + const featureFlags = await Promise.all( + featureFlagSettings.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 }); @@ -546,6 +557,33 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } return response; } + + async #parseFeatureFlag(setting: ConfigurationSetting): Promise { + const rawFlag = setting.value; + if (rawFlag === undefined) { + throw new Error("The value of configuration setting cannot be undefined."); + } + const featureFlag = JSON.parse(rawFlag); + if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { + if (featureFlag[CONDITIONS_KEY_NAME] && + featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] && + Array.isArray(featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME])) { + for (const filter of featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME]) { + this.#featureFlagTracing.updateFeatureFilterTracing(filter[NAME_KEY_NAME]); + } + } + if (featureFlag[VARIANTS_KEY_NAME] && Array.isArray(featureFlag[VARIANTS_KEY_NAME])) { + this.#featureFlagTracing.notifyMaxVariants(featureFlag[VARIANTS_KEY_NAME].length); + } + if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME]) { + this.#featureFlagTracing.usesTelemetry = true; + } + if (featureFlag[ALLOCATION_KEY_NAME] && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME]) { + this.#featureFlagTracing.usesSeed = true; + } + } + return featureFlag; + } } function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index f0082f48..67afa554 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -2,4 +2,15 @@ // 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 CONDITIONS_KEY_NAME = "conditions"; +export const CLIENT_FILTERS_KEY_NAME = "client_filters"; +export const TELEMETRY_KEY_NAME = "telemetry"; +export const VARIANTS_KEY_NAME = "variants"; +export const ALLOCATION_KEY_NAME = "allocation"; +export const SEED_KEY_NAME = "seed"; +export const NAME_KEY_NAME = "name"; +export const ENABLED_KEY_NAME = "enabled"; + +export const TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter"]; +export const TARGETING_FILTER_NAMES = ["Targeting", "Microsoft.Targeting", "TargetingFilter", "Microsoft.TargetingFilter"]; diff --git a/src/requestTracing/FeatureFlagTracingOptions.ts b/src/requestTracing/FeatureFlagTracingOptions.ts new file mode 100644 index 00000000..006d969e --- /dev/null +++ b/src/requestTracing/FeatureFlagTracingOptions.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES } from "../featureManagement/constants.js"; +import { CUSTOM_FILTER_KEY, TIME_WINDOW_FILTER_KEY, TARGETING_FILTER_KEY, FF_SEED_USED_TAG, FF_TELEMETRY_USED_TAG, DELIMITER } from "./constants.js"; + +/** + * Tracing for tracking feature flag usage. + */ +export class FeatureFlagTracingOptions { + /** + * Built-in feature filter usage. + */ + usesCustomFilter: boolean = false; + usesTimeWindowFilter: boolean = false; + usesTargetingFilter: boolean = false; + usesTelemetry: boolean = false; + usesSeed: boolean = false; + maxVariants: number = 0; + + resetFeatureFlagTracing(): void { + this.usesCustomFilter = false; + this.usesTimeWindowFilter = false; + this.usesTargetingFilter = false; + this.usesTelemetry = false; + this.usesSeed = false; + this.maxVariants = 0; + } + + updateFeatureFilterTracing(filterName: string): void { + if (TIME_WINDOW_FILTER_NAMES.some(name => name === filterName)) { + this.usesTimeWindowFilter = true; + } else if (TARGETING_FILTER_NAMES.some(name => name === filterName)) { + this.usesTargetingFilter = true; + } else { + this.usesCustomFilter = true; + } + } + + notifyMaxVariants(currentFFTotalVariants: number): void { + if (currentFFTotalVariants > this.maxVariants) { + this.maxVariants = currentFFTotalVariants; + } + } + + usesAnyFeatureFilter(): boolean { + return this.usesCustomFilter || this.usesTimeWindowFilter || this.usesTargetingFilter; + } + + usesAnyTracingFeature() { + return this.usesSeed || this.usesTelemetry; + } + + createFeatureFiltersString(): string { + if (!this.usesAnyFeatureFilter()) { + return ""; + } + + let result: string = ""; + if (this.usesCustomFilter) { + result += CUSTOM_FILTER_KEY; + } + if (this.usesTimeWindowFilter) { + if (result !== "") { + result += DELIMITER; + } + result += TIME_WINDOW_FILTER_KEY; + } + if (this.usesTargetingFilter) { + if (result !== "") { + result += DELIMITER; + } + result += TARGETING_FILTER_KEY; + } + return result; + } + + createFeaturesString(): string { + if (!this.usesAnyTracingFeature()) { + return ""; + } + + let result: string = ""; + if (this.usesSeed) { + result += FF_SEED_USED_TAG; + } + if (this.usesTelemetry) { + if (result !== "") { + result += DELIMITER; + } + result += FF_TELEMETRY_USED_TAG; + } + return result; + } +} diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index d46cdfda..f32996b9 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -37,7 +37,7 @@ export const CONTAINER_APP_ENV_VAR = "CONTAINER_APP_NAME"; export const KUBERNETES_ENV_VAR = "KUBERNETES_PORT"; export const SERVICE_FABRIC_ENV_VAR = "Fabric_NodeName"; // See: https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-environment-variables-reference -// Request Type +// Request type export const REQUEST_TYPE_KEY = "RequestType"; export enum RequestType { STARTUP = "Startup", @@ -46,3 +46,16 @@ export enum RequestType { // Tag names export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; + +// Feature flag usage tracing +export const FEATURE_FILTER_TYPE_KEY = "Filter"; +export const CUSTOM_FILTER_KEY = "CSTM"; +export const TIME_WINDOW_FILTER_KEY = "TIME"; +export const TARGETING_FILTER_KEY = "TRGT"; + +export const FF_TELEMETRY_USED_TAG = "Telemetry"; +export const FF_MAX_VARIANTS_KEY = "MaxVariants"; +export const FF_SEED_USED_TAG = "Seed"; +export const FF_FEATURES_KEY = "FFFeatures"; + +export const DELIMITER = "+"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index de335737..e9d0b0df 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -3,6 +3,7 @@ import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration"; import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js"; +import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js"; import { AZURE_FUNCTION_ENV_VAR, AZURE_WEB_APP_ENV_VAR, @@ -10,6 +11,9 @@ import { DEV_ENV_VAL, ENV_AZURE_APP_CONFIGURATION_TRACING_DISABLED, ENV_KEY, + FEATURE_FILTER_TYPE_KEY, + FF_MAX_VARIANTS_KEY, + FF_FEATURES_KEY, HOST_TYPE_KEY, HostType, KEY_VAULT_CONFIGURED_TAG, @@ -28,17 +32,18 @@ export function listConfigurationSettingsWithTrace( requestTracingEnabled: boolean; initialLoadCompleted: boolean; appConfigOptions: AzureAppConfigurationOptions | undefined; + featureFlagTracingOptions: FeatureFlagTracingOptions | undefined; }, client: AppConfigurationClient, listOptions: ListConfigurationSettingsOptions ) { - const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions; + const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, featureFlagTracingOptions } = requestTracingOptions; const actualListOptions = { ...listOptions }; if (requestTracingEnabled) { actualListOptions.requestOptions = { customHeaders: { - [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted) + [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, featureFlagTracingOptions, initialLoadCompleted) } }; } @@ -51,18 +56,19 @@ export function getConfigurationSettingWithTrace( requestTracingEnabled: boolean; initialLoadCompleted: boolean; appConfigOptions: AzureAppConfigurationOptions | undefined; + featureFlagTracingOptions: FeatureFlagTracingOptions | undefined; }, client: AppConfigurationClient, configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions, ) { - const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions; + const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, featureFlagTracingOptions } = requestTracingOptions; const actualGetOptions = { ...getOptions }; if (requestTracingEnabled) { actualGetOptions.requestOptions = { customHeaders: { - [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted) + [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, featureFlagTracingOptions, initialLoadCompleted) } }; } @@ -70,7 +76,7 @@ export function getConfigurationSettingWithTrace( return client.getConfigurationSetting(configurationSettingId, actualGetOptions); } -export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, isInitialLoadCompleted: boolean): string { +export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, featureFlagTracing: FeatureFlagTracingOptions | undefined, isInitialLoadCompleted: boolean): string { /* RequestType: 'Startup' during application starting up, 'Watch' after startup completed. Host: identify with defined envs @@ -82,6 +88,14 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt keyValues.set(HOST_TYPE_KEY, getHostType()); keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined); + if (featureFlagTracing) { + keyValues.set(FEATURE_FILTER_TYPE_KEY, featureFlagTracing.usesAnyFeatureFilter() ? featureFlagTracing.createFeatureFiltersString() : undefined); + keyValues.set(FF_FEATURES_KEY, featureFlagTracing.usesAnyTracingFeature() ? featureFlagTracing.createFeaturesString() : undefined); + if (featureFlagTracing.maxVariants > 0) { + keyValues.set(FF_MAX_VARIANTS_KEY, featureFlagTracing.maxVariants.toString()); + } + } + const tags: string[] = []; if (options?.keyVaultOptions) { const { credential, secretClients, secretResolver } = options.keyVaultOptions; diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index a64d2c4a..7bd73ce0 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -5,22 +5,10 @@ import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; chai.use(chaiAsPromised); const expect = chai.expect; -import { createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sleepInMs } from "./utils/testHelper.js"; +import { createMockedConnectionString, createMockedKeyValue, createMockedFeatureFlag, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, HttpRequestHeadersPolicy, sleepInMs } from "./utils/testHelper.js"; import { load } from "./exportedApi.js"; -class HttpRequestHeadersPolicy { - headers: any; - name: string; - - constructor() { - this.headers = {}; - this.name = "HttpRequestHeadersPolicy"; - } - sendRequest(req, next) { - this.headers = req.headers; - return next(req).then(resp => resp); - } -} +const CORRELATION_CONTEXT_HEADER_NAME = "Correlation-Context"; describe("request tracing", function () { this.timeout(15000); @@ -150,6 +138,155 @@ describe("request tracing", function () { restoreMocks(); }); + it("should have filter type in correlation-context header if feature flags use feature filters", async () => { + let correlationContext: string = ""; + const listKvCallback = (listOptions) => { + correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; + }; + + mockAppConfigurationClientListConfigurationSettings([[ + createMockedFeatureFlag("Alpha_1", { conditions: { client_filters: [ { name: "Microsoft.TimeWindow" } ] } }), + createMockedFeatureFlag("Alpha_2", { conditions: { client_filters: [ { name: "Microsoft.Targeting" } ] } }), + createMockedFeatureFlag("Alpha_3", { conditions: { client_filters: [ { name: "CustomFilter" } ] } }) + ]], listKvCallback); + + const settings = await load(createMockedConnectionString(fakeEndpoint), { + featureFlagOptions: { + enabled: true, + selectors: [ {keyFilter: "*"} ], + refresh: { + enabled: true, + refreshIntervalInMs: 1000 + } + } + }); + + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Startup")).eq(true); + + await sleepInMs(1000 + 1); + try { + await settings.refresh(); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Watch")).eq(true); + expect(correlationContext?.includes("Filter=CSTM+TIME+TRGT")).eq(true); + + restoreMocks(); + }); + + it("should have max variants in correlation-context header if feature flags use variants", async () => { + let correlationContext: string = ""; + const listKvCallback = (listOptions) => { + correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; + }; + + mockAppConfigurationClientListConfigurationSettings([[ + createMockedFeatureFlag("Alpha_1", { variants: [ {name: "a"}, {name: "b"}] }), + createMockedFeatureFlag("Alpha_2", { variants: [ {name: "a"}, {name: "b"}, {name: "c"}] }), + createMockedFeatureFlag("Alpha_3", { variants: [] }) + ]], listKvCallback); + + const settings = await load(createMockedConnectionString(fakeEndpoint), { + featureFlagOptions: { + enabled: true, + selectors: [ {keyFilter: "*"} ], + refresh: { + enabled: true, + refreshIntervalInMs: 1000 + } + } + }); + + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Startup")).eq(true); + + await sleepInMs(1000 + 1); + try { + await settings.refresh(); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Watch")).eq(true); + expect(correlationContext?.includes("MaxVariants=3")).eq(true); + + restoreMocks(); + }); + + it("should have telemety tag in correlation-context header if feature flags enable telemetry", async () => { + let correlationContext: string = ""; + const listKvCallback = (listOptions) => { + correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; + }; + + mockAppConfigurationClientListConfigurationSettings([[ + createMockedFeatureFlag("Alpha_1", { telemetry: {enabled: true} }) + ]], listKvCallback); + + const settings = await load(createMockedConnectionString(fakeEndpoint), { + featureFlagOptions: { + enabled: true, + selectors: [ {keyFilter: "*"} ], + refresh: { + enabled: true, + refreshIntervalInMs: 1000 + } + } + }); + + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Startup")).eq(true); + + await sleepInMs(1000 + 1); + try { + await settings.refresh(); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Watch")).eq(true); + expect(correlationContext?.includes("FFFeatures=Telemetry")).eq(true); + + restoreMocks(); + }); + + it("should have seed tag in correlation-context header if feature flags use allocation seed", async () => { + let correlationContext: string = ""; + const listKvCallback = (listOptions) => { + correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; + }; + + mockAppConfigurationClientListConfigurationSettings([[ + createMockedFeatureFlag("Alpha_1", { telemetry: {enabled: true} }), + createMockedFeatureFlag("Alpha_2", { allocation: {seed: "123"} }) + ]], listKvCallback); + + const settings = await load(createMockedConnectionString(fakeEndpoint), { + featureFlagOptions: { + enabled: true, + selectors: [ {keyFilter: "*"} ], + refresh: { + enabled: true, + refreshIntervalInMs: 1000 + } + } + }); + + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Startup")).eq(true); + + await sleepInMs(1000 + 1); + try { + await settings.refresh(); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + expect(correlationContext).not.undefined; + expect(correlationContext?.includes("RequestType=Watch")).eq(true); + expect(correlationContext?.includes("FFFeatures=Seed+Telemetry")).eq(true); + + restoreMocks(); + }); + describe("request tracing in Web Worker environment", () => { let originalNavigator; let originalWorkerNavigator; diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index bf05882d..c00c99b6 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -201,6 +201,20 @@ const createMockedFeatureFlag = (name: string, flagProps?: any, props?: any) => isReadOnly: false }, props)); +class HttpRequestHeadersPolicy { + headers: any; + name: string; + + constructor() { + this.headers = {}; + this.name = "HttpRequestHeadersPolicy"; + } + sendRequest(req, next) { + this.headers = req.headers; + return next(req).then(resp => resp); + } +} + export { sinon, mockAppConfigurationClientListConfigurationSettings, @@ -216,5 +230,7 @@ export { createMockedKeyValue, createMockedFeatureFlag, + HttpRequestHeadersPolicy, + sleepInMs }; From 300cf06b3658313ceb6ae55b8e46ea02e2dd4637 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:59:15 +0800 Subject: [PATCH 5/7] version bump 1.1.3 (#145) --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac1204cd..b265cb5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure/app-configuration-provider", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure/app-configuration-provider", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", diff --git a/package.json b/package.json index 0b493ea3..7ec6f6b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@azure/app-configuration-provider", - "version": "1.1.2", + "version": "1.1.3", "description": "The JavaScript configuration provider for Azure App Configuration", "main": "dist/index.js", "module": "./dist-esm/index.js", diff --git a/src/version.ts b/src/version.ts index 1c39d36f..91c099f6 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 = "1.1.2"; +export const VERSION = "1.1.3"; From 6206bc0fd2122075722120871fb22c5e858ffc9a Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:19:38 +0800 Subject: [PATCH 6/7] fix vulnerable package (#149) --- package-lock.json | 158 +++++++++++++++++++++------------------------- 1 file changed, 73 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index b265cb5d..e2c6af23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1138,9 +1138,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" @@ -1425,9 +1425,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", @@ -1439,11 +1439,11 @@ } }, "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==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1501,9 +1501,9 @@ } }, "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" @@ -2573,32 +2573,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", @@ -2606,10 +2605,6 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha/node_modules/brace-expansion": { @@ -2621,10 +2616,30 @@ "balanced-match": "^1.0.0" } }, + "node_modules/mocha/node_modules/glob": { + "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": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "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" @@ -2633,12 +2648,6 @@ "node": ">=10" } }, - "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", @@ -2655,21 +2664,9 @@ } }, "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==" - }, - "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" - } + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -3175,9 +3172,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" @@ -3235,15 +3232,6 @@ "url": "https://opencollective.com/sinon" } }, - "node_modules/sinon/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3477,9 +3465,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": { @@ -3556,9 +3544,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" From d649dd3fb6951d1c731e26a39da64a91bd9387c9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:19:54 +0800 Subject: [PATCH 7/7] add lint rule (#151) --- .eslintrc | 7 +++++++ 1 file changed, 7 insertions(+) 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",