From 7d45d1c2ffd608850bd158e3e8df0f89b55ebccb Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Wed, 16 Oct 2024 17:44:17 +0800 Subject: [PATCH 01/12] wip --- src/AzureAppConfigurationImpl.ts | 28 +++++++-- src/featureManagement/constants.ts | 4 +- src/requestTracing/FeatureFlagTracing.ts | 72 ++++++++++++++++++++++++ src/requestTracing/constants.ts | 7 +++ src/requestTracing/utils.ts | 2 + 5 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 src/requestTracing/FeatureFlagTracing.ts diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 30eba149..6a2868a2 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,10 +9,11 @@ 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, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; import { RefreshTimer } from "./refresh/RefreshTimer"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; +import { FeatureFlagTracing } from "./requestTracing/FeatureFlagTracing"; import { KeyFilter, LabelFilter, SettingSelector } from "./types"; type PagedSettingSelector = SettingSelector & { @@ -38,6 +39,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #client: AppConfigurationClient; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; + #featureFlagTracing: FeatureFlagTracing = new FeatureFlagTracing(); // Refresh #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; @@ -173,7 +175,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return { requestTracingEnabled: this.#requestTracingEnabled, initialLoadCompleted: this.#isInitialLoadCompleted, - appConfigOptions: this.#options + appConfigOptions: this.#options, + featureFlagTracingOptions: this.#featureFlagTracing }; } @@ -255,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}`, @@ -273,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.value); + featureFlagSettings.push(setting); } } } @@ -281,7 +283,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } // 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 }); @@ -532,6 +536,18 @@ 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); + + + + return featureFlag; + } } function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] { diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index f0082f48..d5389bbb 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -2,4 +2,6 @@ // 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"; \ No newline at end of file diff --git a/src/requestTracing/FeatureFlagTracing.ts b/src/requestTracing/FeatureFlagTracing.ts new file mode 100644 index 00000000..299ced61 --- /dev/null +++ b/src/requestTracing/FeatureFlagTracing.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + CUSTOM_FILTER_KEY, + TIME_WINDOW_FILTER_KEY, + TARGETING_FILTER_KEY, + DELIMITER +} from "./constants"; + +/** + * Tracing for tracking feature flag usage. + */ +export class FeatureFlagTracing { + #timeWindowFilterNames: string[] = ["TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter"]; + #targetingFilterNames: string[] = ["Targeting", "Microsoft.Targeting", "TargetingFilter", "Microsoft.TargetingFilter"]; + + /** + * Built-in feature filter usage. + */ + usesCustomFilter: boolean = false; + usesTimeWindowFilter: boolean = false; + usesTargetingFilter: boolean = false; + + resetFeatureFlagTracing(): void { + this.usesCustomFilter = false; + this.usesTimeWindowFilter = false; + this.usesTargetingFilter = false; + } + + updateFeatureFilterTracing(filterName: string): void { + if (this.#timeWindowFilterNames.some(name => name.toLowerCase() === filterName.toLowerCase())) { + this.usesTimeWindowFilter = true; + } else if (this.#targetingFilterNames.some(name => name.toLowerCase() === filterName.toLowerCase())) { + this.usesTargetingFilter = true; + } else { + this.usesCustomFilter = true; + } + } + + usesAnyFeatureFilter(): boolean { + return this.usesCustomFilter || this.usesTimeWindowFilter || this.usesTargetingFilter; + } + + 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; + } +} diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index be33aa5d..cfcbc540 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -46,3 +46,10 @@ export enum RequestType { // Tag names export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; + +// Feature Flag Usage Tracing +export const CUSTOM_FILTER_KEY = "CSTM"; +export const TIME_WINDOW_FILTER_KEY = "TIME"; +export const TARGETING_FILTER_KEY = "TRGT"; + +export const DELIMITER = "+"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index 0d851a61..e73de6de 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"; +import { FeatureFlagTracing } from "./FeatureFlagTracing"; import { AZURE_FUNCTION_ENV_VAR, AZURE_WEB_APP_ENV_VAR, @@ -28,6 +29,7 @@ export function listConfigurationSettingsWithTrace( requestTracingEnabled: boolean; initialLoadCompleted: boolean; appConfigOptions: AzureAppConfigurationOptions | undefined; + featureFlagTracingOptions: FeatureFlagTracing | undefined; }, client: AppConfigurationClient, listOptions: ListConfigurationSettingsOptions From 4db73922eef78e1fa4a499cdced70484a7189d60 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Thu, 17 Oct 2024 03:09:56 +0800 Subject: [PATCH 02/12] WIP --- src/AzureAppConfigurationImpl.ts | 13 +++++++++---- src/featureManagement/constants.ts | 6 +++++- ...gTracing.ts => FeatureFlagTracingOptions.ts} | 17 +++++------------ src/requestTracing/constants.ts | 1 + src/requestTracing/utils.ts | 17 ++++++++++------- 5 files changed, 30 insertions(+), 24 deletions(-) rename src/requestTracing/{FeatureFlagTracing.ts => FeatureFlagTracingOptions.ts} (70%) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 6a2868a2..e10a37c8 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,11 +9,11 @@ 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, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME } from "./featureManagement/constants"; +import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME, NAME_KEY_NAME } from "./featureManagement/constants"; import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter"; import { RefreshTimer } from "./refresh/RefreshTimer"; import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils"; -import { FeatureFlagTracing } from "./requestTracing/FeatureFlagTracing"; +import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions"; import { KeyFilter, LabelFilter, SettingSelector } from "./types"; type PagedSettingSelector = SettingSelector & { @@ -39,7 +39,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #client: AppConfigurationClient; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; - #featureFlagTracing: FeatureFlagTracing = new FeatureFlagTracing(); + #featureFlagTracing: FeatureFlagTracingOptions = new FeatureFlagTracingOptions(); // Refresh #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; @@ -258,6 +258,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #loadFeatureFlags() { + this.#featureFlagTracing.resetFeatureFlagTracing(); const featureFlagSettings: ConfigurationSetting[] = []; for (const selector of this.#featureFlagSelectors) { const listOptions: ListConfigurationSettingsOptions = { @@ -544,7 +545,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } const featureFlag = JSON.parse(rawFlag); - + if (featureFlag[CONDITIONS_KEY_NAME] && 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]); + } + } return featureFlag; } diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index d5389bbb..f0a88a25 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -4,4 +4,8 @@ export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; export const FEATURE_FLAGS_KEY_NAME = "feature_flags"; export const CONDITIONS_KEY_NAME = "conditions"; -export const CLIENT_FILTERS_KEY_NAME = "client_filters"; \ No newline at end of file +export const CLIENT_FILTERS_KEY_NAME = "client_filters"; +export const NAME_KEY_NAME = "name"; + +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/FeatureFlagTracing.ts b/src/requestTracing/FeatureFlagTracingOptions.ts similarity index 70% rename from src/requestTracing/FeatureFlagTracing.ts rename to src/requestTracing/FeatureFlagTracingOptions.ts index 299ced61..b5dd7e3e 100644 --- a/src/requestTracing/FeatureFlagTracing.ts +++ b/src/requestTracing/FeatureFlagTracingOptions.ts @@ -1,20 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { - CUSTOM_FILTER_KEY, - TIME_WINDOW_FILTER_KEY, - TARGETING_FILTER_KEY, - DELIMITER -} from "./constants"; +import { TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES } from "../featureManagement/constants"; +import { CUSTOM_FILTER_KEY, TIME_WINDOW_FILTER_KEY, TARGETING_FILTER_KEY, DELIMITER } from "./constants"; /** * Tracing for tracking feature flag usage. */ -export class FeatureFlagTracing { - #timeWindowFilterNames: string[] = ["TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter"]; - #targetingFilterNames: string[] = ["Targeting", "Microsoft.Targeting", "TargetingFilter", "Microsoft.TargetingFilter"]; - +export class FeatureFlagTracingOptions { /** * Built-in feature filter usage. */ @@ -29,9 +22,9 @@ export class FeatureFlagTracing { } updateFeatureFilterTracing(filterName: string): void { - if (this.#timeWindowFilterNames.some(name => name.toLowerCase() === filterName.toLowerCase())) { + if (TIME_WINDOW_FILTER_NAMES.some(name => name === filterName)) { this.usesTimeWindowFilter = true; - } else if (this.#targetingFilterNames.some(name => name.toLowerCase() === filterName.toLowerCase())) { + } else if (TARGETING_FILTER_NAMES.some(name => name === filterName)) { this.usesTargetingFilter = true; } else { this.usesCustomFilter = true; diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index cfcbc540..068c91d9 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -48,6 +48,7 @@ export enum RequestType { 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"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index e73de6de..ffdcda61 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -3,7 +3,7 @@ import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration"; import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions"; -import { FeatureFlagTracing } from "./FeatureFlagTracing"; +import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions"; import { AZURE_FUNCTION_ENV_VAR, AZURE_WEB_APP_ENV_VAR, @@ -11,6 +11,7 @@ import { DEV_ENV_VAL, ENV_AZURE_APP_CONFIGURATION_TRACING_DISABLED, ENV_KEY, + FEATURE_FILTER_TYPE_KEY, HOST_TYPE_KEY, HostType, KEY_VAULT_CONFIGURED_TAG, @@ -29,18 +30,18 @@ export function listConfigurationSettingsWithTrace( requestTracingEnabled: boolean; initialLoadCompleted: boolean; appConfigOptions: AzureAppConfigurationOptions | undefined; - featureFlagTracingOptions: FeatureFlagTracing | 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) } }; } @@ -53,18 +54,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) } }; } @@ -72,7 +74,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 @@ -83,6 +85,7 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP); keyValues.set(HOST_TYPE_KEY, getHostType()); keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined); + keyValues.set(FEATURE_FILTER_TYPE_KEY, featureFlagTracing?.usesAnyFeatureFilter() ? featureFlagTracing.createFeatureFiltersString() : undefined); const tags: string[] = []; if (options?.keyVaultOptions) { From 067a8492de62a525617515e3b33fdedf44b1e54d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Oct 2024 14:34:16 +0800 Subject: [PATCH 03/12] add integration test --- .gitignore | 4 ++ package-lock.json | 57 +++++++++++++++++++++++++++ package.json | 3 +- src/AzureAppConfigurationImpl.ts | 3 +- test/integration.test.ts | 61 +++++++++++++++++++++++++++++ test/requestTracing.test.ts | 16 +------- test/utils/integrationTestHelper.ts | 46 ++++++++++++++++++++++ test/utils/testHelper.ts | 18 ++++++++- 8 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 test/integration.test.ts create mode 100644 test/utils/integrationTestHelper.ts diff --git a/.gitignore b/.gitignore index 1ccd7b97..34934b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -411,3 +411,7 @@ types/ # examples examples/package-lock.json + +# cert +*.cert +*.key \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b6171131..a56c16c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "rimraf": "^5.0.1", "rollup": "^3.26.3", "rollup-plugin-dts": "^5.3.0", + "selfsigned": "^2.4.1", "sinon": "^15.2.0", "tslib": "^2.6.0", "typescript": "^5.1.6", @@ -873,6 +874,15 @@ "undici-types": "~5.25.1" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -2713,6 +2723,15 @@ "node": ">= 10.13" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3160,6 +3179,19 @@ } ] }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4239,6 +4271,15 @@ "undici-types": "~5.25.1" } }, + "@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -5599,6 +5640,12 @@ "propagate": "^2.0.0" } }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5880,6 +5927,16 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "requires": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + } + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", diff --git a/package.json b/package.json index 74aca6ec..a2b9589e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "sinon": "^15.2.0", "tslib": "^2.6.0", "typescript": "^5.1.6", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "selfsigned": "^2.4.1" }, "dependencies": { "@azure/app-configuration": "^1.6.1", diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index e10a37c8..7ace5ed8 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -258,7 +258,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } async #loadFeatureFlags() { - this.#featureFlagTracing.resetFeatureFlagTracing(); const featureFlagSettings: ConfigurationSetting[] = []; for (const selector of this.#featureFlagSelectors) { const listOptions: ListConfigurationSettingsOptions = { @@ -283,6 +282,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { selector.pageEtags = pageEtags; } + this.#featureFlagTracing.resetFeatureFlagTracing(); + // parse feature flags const featureFlags = await Promise.all( featureFlagSettings.map(setting => this.#parseFeatureFlag(setting)) diff --git a/test/integration.test.ts b/test/integration.test.ts new file mode 100644 index 00000000..f9fb49ec --- /dev/null +++ b/test/integration.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const expect = chai.expect; +import { createMockedConnectionString, createMockedFeatureFlag, HttpRequestHeadersPolicy, sleepInMs } from "./utils/testHelper"; +import { mockServerEndpoint, startMockServer, closeMockServer } from "./utils/integrationTestHelper"; +import { load } from "./exportedApi"; + +describe("integration test", function () { + this.timeout(15000); + + const headerPolicy = new HttpRequestHeadersPolicy(); + const position: "perCall" | "perRetry" = "perCall"; + const clientOptions = { + retryOptions: { + maxRetries: 0 // save time + }, + allowInsecureConnection: true, + additionalPolicies: [{ + policy: headerPolicy, + position + }] + }; + + it("should have request type in correlation-context header if feature flags use feature filters", async () => { + // We are using self-signed certificate + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + + startMockServer([ + 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" } ] } }) + ]); + + const settings = await load(createMockedConnectionString(mockServerEndpoint), { + clientOptions, + featureFlagOptions: { + enabled: true, + selectors: [ {keyFilter: "*"} ], + refresh: { + enabled: true, + refreshIntervalInMs: 1000 + } + } + }); + await sleepInMs(1000 + 1); + try { + await settings.refresh(); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + const correlationContext = headerPolicy.headers.get("Correlation-Context"); + expect(correlationContext).not.undefined; + expect(correlationContext.includes("RequestType=Watch")).eq(true); + expect(correlationContext.includes("Filter=CSTM+TIME+TRGT")).eq(true); + + closeMockServer(); + }); +}); diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index 1496b097..45e5984a 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -5,23 +5,9 @@ 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"; +import { createMockedConnectionString, createMockedKeyValue, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, HttpRequestHeadersPolicy, sleepInMs } from "./utils/testHelper"; import { load } from "./exportedApi"; -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); - } -} - describe("request tracing", function () { this.timeout(15000); diff --git a/test/utils/integrationTestHelper.ts b/test/utils/integrationTestHelper.ts new file mode 100644 index 00000000..17c56792 --- /dev/null +++ b/test/utils/integrationTestHelper.ts @@ -0,0 +1,46 @@ +import { ConfigurationSetting } from "@azure/app-configuration"; +import * as https from "https"; +import * as selfsigned from "selfsigned"; +import * as fs from "fs"; + +let server; + +const domain = "localhost"; +const port = 443 + +function startMockServer(settings: ConfigurationSetting[]) { + const attrs = [{ name: "commonName", value: domain }]; + const certOptions = { keySize: 2048, selfSigned: true }; + const pems = selfsigned.generate(attrs, certOptions); + + fs.writeFileSync("server.key", pems.private); + fs.writeFileSync("server.cert", pems.cert); + + const options = { + key: fs.readFileSync("server.key"), + cert: fs.readFileSync("server.cert") + }; + + const responseBody = { + items: [...settings] + }; + + server = https.createServer(options, (req, res) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(responseBody)); + }); + + server.listen(port); +} + +function closeMockServer() { + server.close(); +} + +const mockServerEndpoint = "https://localhost:443"; + +export { + startMockServer, + closeMockServer, + mockServerEndpoint +} \ No newline at end of file diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index 6e787dd7..0652fb08 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -194,6 +194,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, @@ -209,5 +223,7 @@ export { createMockedKeyValue, createMockedFeatureFlag, + HttpRequestHeadersPolicy, + sleepInMs -}; +}; From 8d7cefd3ee49be827d4a727656a5d12d0bfe7d7e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Oct 2024 14:40:25 +0800 Subject: [PATCH 04/12] fix lint --- src/requestTracing/FeatureFlagTracingOptions.ts | 14 +++++++------- test/utils/integrationTestHelper.ts | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/requestTracing/FeatureFlagTracingOptions.ts b/src/requestTracing/FeatureFlagTracingOptions.ts index b5dd7e3e..09540701 100644 --- a/src/requestTracing/FeatureFlagTracingOptions.ts +++ b/src/requestTracing/FeatureFlagTracingOptions.ts @@ -30,7 +30,7 @@ export class FeatureFlagTracingOptions { this.usesCustomFilter = true; } } - + usesAnyFeatureFilter(): boolean { return this.usesCustomFilter || this.usesTimeWindowFilter || this.usesTargetingFilter; } @@ -39,27 +39,27 @@ export class FeatureFlagTracingOptions { if (!this.usesAnyFeatureFilter()) { return ""; } - + let result: string = ""; - + if (this.usesCustomFilter) { - result += CUSTOM_FILTER_KEY + 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; } } diff --git a/test/utils/integrationTestHelper.ts b/test/utils/integrationTestHelper.ts index 17c56792..c025195e 100644 --- a/test/utils/integrationTestHelper.ts +++ b/test/utils/integrationTestHelper.ts @@ -6,7 +6,7 @@ import * as fs from "fs"; let server; const domain = "localhost"; -const port = 443 +const port = 443; function startMockServer(settings: ConfigurationSetting[]) { const attrs = [{ name: "commonName", value: domain }]; @@ -20,16 +20,16 @@ function startMockServer(settings: ConfigurationSetting[]) { key: fs.readFileSync("server.key"), cert: fs.readFileSync("server.cert") }; - + const responseBody = { items: [...settings] }; server = https.createServer(options, (req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify(responseBody)); + res.end(JSON.stringify(responseBody)); }); - + server.listen(port); } @@ -43,4 +43,4 @@ export { startMockServer, closeMockServer, mockServerEndpoint -} \ No newline at end of file +}; From 0d3b5608317f71b1e1c7d0e56352354da7b0b44f Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Oct 2024 14:46:48 +0800 Subject: [PATCH 05/12] update port --- test/integration.test.ts | 2 +- test/utils/integrationTestHelper.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index f9fb49ec..7598f8fd 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -25,7 +25,7 @@ describe("integration test", function () { }] }; - it("should have request type in correlation-context header if feature flags use feature filters", async () => { + it("should have filter type in correlation-context header if feature flags use feature filters", async () => { // We are using self-signed certificate process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; diff --git a/test/utils/integrationTestHelper.ts b/test/utils/integrationTestHelper.ts index c025195e..c4a74c19 100644 --- a/test/utils/integrationTestHelper.ts +++ b/test/utils/integrationTestHelper.ts @@ -6,7 +6,7 @@ import * as fs from "fs"; let server; const domain = "localhost"; -const port = 443; +const port = 8443; function startMockServer(settings: ConfigurationSetting[]) { const attrs = [{ name: "commonName", value: domain }]; @@ -37,7 +37,7 @@ function closeMockServer() { server.close(); } -const mockServerEndpoint = "https://localhost:443"; +const mockServerEndpoint = `https://localhost:${port}`; export { startMockServer, From 4b64726ef930acbd1b330b2b32e32343afde30cb Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Oct 2024 14:54:41 +0800 Subject: [PATCH 06/12] update port --- test/utils/integrationTestHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/integrationTestHelper.ts b/test/utils/integrationTestHelper.ts index c4a74c19..69ad6f29 100644 --- a/test/utils/integrationTestHelper.ts +++ b/test/utils/integrationTestHelper.ts @@ -6,7 +6,7 @@ import * as fs from "fs"; let server; const domain = "localhost"; -const port = 8443; +const port = 54321; function startMockServer(settings: ConfigurationSetting[]) { const attrs = [{ name: "commonName", value: domain }]; From b5d0a6069c26315046db36fcbddd8c8c1b541803 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Oct 2024 15:00:39 +0800 Subject: [PATCH 07/12] update --- test/integration.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/integration.test.ts b/test/integration.test.ts index 7598f8fd..84192b2e 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -46,12 +46,17 @@ describe("integration test", function () { } } }); + expect(headerPolicy.headers).not.undefined; + let correlationContext = headerPolicy.headers.get("Correlation-Context"); + 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; - const correlationContext = headerPolicy.headers.get("Correlation-Context"); + correlationContext = headerPolicy.headers.get("Correlation-Context"); expect(correlationContext).not.undefined; expect(correlationContext.includes("RequestType=Watch")).eq(true); expect(correlationContext.includes("Filter=CSTM+TIME+TRGT")).eq(true); From 089d677e9acde91f4efbb1bd84585f3ceac33bea Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Thu, 17 Oct 2024 15:45:57 +0800 Subject: [PATCH 08/12] update --- test/utils/integrationTestHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/integrationTestHelper.ts b/test/utils/integrationTestHelper.ts index 69ad6f29..aeba4ff5 100644 --- a/test/utils/integrationTestHelper.ts +++ b/test/utils/integrationTestHelper.ts @@ -34,7 +34,7 @@ function startMockServer(settings: ConfigurationSetting[]) { } function closeMockServer() { - server.close(); + server?.close(); } const mockServerEndpoint = `https://localhost:${port}`; From 68aee3305237190fab49cf1e735b5556d5bd4b28 Mon Sep 17 00:00:00 2001 From: zhiyuanliang Date: Wed, 6 Nov 2024 00:30:13 +0800 Subject: [PATCH 09/12] avoid create new featureFlagTracing object when request tracing is disabled --- src/AzureAppConfigurationImpl.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 09aea335..b53d6077 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -39,7 +39,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { #client: AppConfigurationClient; #options: AzureAppConfigurationOptions | undefined; #isInitialLoadCompleted: boolean = false; - #featureFlagTracing: FeatureFlagTracingOptions = new FeatureFlagTracingOptions(); + #featureFlagTracing: FeatureFlagTracingOptions | undefined; // Refresh #refreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS; @@ -66,6 +66,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)); @@ -282,7 +285,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { selector.pageEtags = pageEtags; } - this.#featureFlagTracing.resetFeatureFlagTracing(); + if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { + this.#featureFlagTracing.resetFeatureFlagTracing(); + } // parse feature flags const featureFlags = await Promise.all( @@ -545,13 +550,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { throw new Error("The value of configuration setting cannot be undefined."); } const featureFlag = JSON.parse(rawFlag); - - if (featureFlag[CONDITIONS_KEY_NAME] && 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 (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { + if (featureFlag[CONDITIONS_KEY_NAME] && 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]); + } } } - return featureFlag; } } From f0dabb5cddd3e369ef3067c7740ba0017a9f4c3e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 2 Dec 2024 18:23:48 +0800 Subject: [PATCH 10/12] update --- test/integration.test.ts | 66 ----------------------------- test/requestTracing.test.ts | 42 +++++++++++++++++- test/utils/integrationTestHelper.ts | 46 -------------------- 3 files changed, 41 insertions(+), 113 deletions(-) delete mode 100644 test/integration.test.ts delete mode 100644 test/utils/integrationTestHelper.ts diff --git a/test/integration.test.ts b/test/integration.test.ts deleted file mode 100644 index e3817002..00000000 --- a/test/integration.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import * as chai from "chai"; -import * as chaiAsPromised from "chai-as-promised"; -chai.use(chaiAsPromised); -const expect = chai.expect; -import { createMockedConnectionString, createMockedFeatureFlag, HttpRequestHeadersPolicy, sleepInMs } from "./utils/testHelper.js"; -import { mockServerEndpoint, startMockServer, closeMockServer } from "./utils/integrationTestHelper.js"; -import { load } from "./exportedApi.js"; - -describe("integration test", function () { - this.timeout(15000); - - const headerPolicy = new HttpRequestHeadersPolicy(); - const position: "perCall" | "perRetry" = "perCall"; - const clientOptions = { - retryOptions: { - maxRetries: 0 // save time - }, - allowInsecureConnection: true, - additionalPolicies: [{ - policy: headerPolicy, - position - }] - }; - - it("should have filter type in correlation-context header if feature flags use feature filters", async () => { - // We are using self-signed certificate - process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - - startMockServer([ - 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" } ] } }) - ]); - - const settings = await load(createMockedConnectionString(mockServerEndpoint), { - clientOptions, - featureFlagOptions: { - enabled: true, - selectors: [ {keyFilter: "*"} ], - refresh: { - enabled: true, - refreshIntervalInMs: 1000 - } - } - }); - expect(headerPolicy.headers).not.undefined; - let correlationContext = headerPolicy.headers.get("Correlation-Context"); - 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; - correlationContext = headerPolicy.headers.get("Correlation-Context"); - expect(correlationContext).not.undefined; - expect(correlationContext.includes("RequestType=Watch")).eq(true); - expect(correlationContext.includes("Filter=CSTM+TIME+TRGT")).eq(true); - - closeMockServer(); - }); -}); diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index b841696a..4d364127 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -5,9 +5,11 @@ 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, HttpRequestHeadersPolicy, sleepInMs } from "./utils/testHelper.js"; +import { createMockedConnectionString, createMockedKeyValue, createMockedFeatureFlag, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, HttpRequestHeadersPolicy, sleepInMs } from "./utils/testHelper.js"; import { load } from "./exportedApi.js"; +const CORRELATION_CONTEXT_HEADER_NAME = "Correlation-Context"; + describe("request tracing", function () { this.timeout(15000); @@ -136,6 +138,44 @@ 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(); + }); + describe("request tracing in Web Worker environment", () => { let originalNavigator; let originalWorkerNavigator; diff --git a/test/utils/integrationTestHelper.ts b/test/utils/integrationTestHelper.ts deleted file mode 100644 index aeba4ff5..00000000 --- a/test/utils/integrationTestHelper.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ConfigurationSetting } from "@azure/app-configuration"; -import * as https from "https"; -import * as selfsigned from "selfsigned"; -import * as fs from "fs"; - -let server; - -const domain = "localhost"; -const port = 54321; - -function startMockServer(settings: ConfigurationSetting[]) { - const attrs = [{ name: "commonName", value: domain }]; - const certOptions = { keySize: 2048, selfSigned: true }; - const pems = selfsigned.generate(attrs, certOptions); - - fs.writeFileSync("server.key", pems.private); - fs.writeFileSync("server.cert", pems.cert); - - const options = { - key: fs.readFileSync("server.key"), - cert: fs.readFileSync("server.cert") - }; - - const responseBody = { - items: [...settings] - }; - - server = https.createServer(options, (req, res) => { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify(responseBody)); - }); - - server.listen(port); -} - -function closeMockServer() { - server?.close(); -} - -const mockServerEndpoint = `https://localhost:${port}`; - -export { - startMockServer, - closeMockServer, - mockServerEndpoint -}; From fcfff03803caece8a2aeb82a8fb051f2628d3c6d Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 2 Dec 2024 18:26:54 +0800 Subject: [PATCH 11/12] update --- .gitignore | 4 ---- package-lock.json | 32 -------------------------------- 2 files changed, 36 deletions(-) diff --git a/.gitignore b/.gitignore index 34934b3f..1ccd7b97 100644 --- a/.gitignore +++ b/.gitignore @@ -411,7 +411,3 @@ types/ # examples examples/package-lock.json - -# cert -*.cert -*.key \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 10d52ba2..ac1204cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,6 @@ "rimraf": "^5.0.1", "rollup": "^3.29.5", "rollup-plugin-dts": "^5.3.0", - "selfsigned": "^2.4.1", "sinon": "^15.2.0", "tslib": "^2.6.0", "typescript": "^5.6.3", @@ -874,15 +873,6 @@ "undici-types": "~6.19.2" } }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", - "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", @@ -2723,15 +2713,6 @@ "node": ">= 10.13" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "dev": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3179,19 +3160,6 @@ } ] }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", From b2766735df55dbd1dcda5602fd405c5bbb34f0ab Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang Date: Mon, 16 Dec 2024 16:48:09 +0800 Subject: [PATCH 12/12] add variant and telemetry tracing --- src/AzureAppConfigurationImpl.ts | 15 ++- src/featureManagement/constants.ts | 5 + .../FeatureFlagTracingOptions.ts | 40 ++++++- src/requestTracing/constants.ts | 9 +- src/requestTracing/utils.ts | 11 +- test/requestTracing.test.ts | 111 ++++++++++++++++++ 6 files changed, 181 insertions(+), 10 deletions(-) diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 4ee6dabb..4523642d 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -9,7 +9,7 @@ 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, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME, NAME_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"; @@ -565,11 +565,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } const featureFlag = JSON.parse(rawFlag); if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) { - if (featureFlag[CONDITIONS_KEY_NAME] && featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME]) { + 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; } diff --git a/src/featureManagement/constants.ts b/src/featureManagement/constants.ts index f0a88a25..67afa554 100644 --- a/src/featureManagement/constants.ts +++ b/src/featureManagement/constants.ts @@ -5,7 +5,12 @@ export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management"; 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 index 09540701..006d969e 100644 --- a/src/requestTracing/FeatureFlagTracingOptions.ts +++ b/src/requestTracing/FeatureFlagTracingOptions.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES } from "../featureManagement/constants"; -import { CUSTOM_FILTER_KEY, TIME_WINDOW_FILTER_KEY, TARGETING_FILTER_KEY, DELIMITER } from "./constants"; +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. @@ -14,11 +14,17 @@ export class FeatureFlagTracingOptions { 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 { @@ -31,35 +37,59 @@ export class FeatureFlagTracingOptions { } } + 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 f2e27ed6..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", @@ -47,10 +47,15 @@ export enum RequestType { // Tag names export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault"; -// Feature Flag Usage Tracing +// 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 97e91e3a..e9d0b0df 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -12,6 +12,8 @@ import { 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, @@ -85,7 +87,14 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP); keyValues.set(HOST_TYPE_KEY, getHostType()); keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined); - keyValues.set(FEATURE_FILTER_TYPE_KEY, featureFlagTracing?.usesAnyFeatureFilter() ? featureFlagTracing.createFeatureFiltersString() : 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) { diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index 4d364127..7bd73ce0 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -176,6 +176,117 @@ describe("request tracing", function () { 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;