load feature flags from new endpoint - #341
Conversation
There was a problem hiding this comment.
FeatureFlagConditions, FeatureFilter, .... all of these types/interfaces/enum are exported by the Azure SDK. We should use them instead of abusing any type.
| ...actualOptions.requestOptions, | ||
| customHeaders: { | ||
| ...actualOptions.requestOptions?.customHeaders, | ||
| headers: { |
There was a problem hiding this comment.
Is this change intentional? Or it is accidently introduced by ai?
It should be customHeaders
There was a problem hiding this comment.
New version SDK introduced "@azure-rest/core-client": "^2.5.0" and we use https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client-rest/src/common.ts#L161 now.
There was a problem hiding this comment.
Fixed in #348
| let correlationContext: string = ""; | ||
| const listKvCallback = (listOptions) => { | ||
| correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; | ||
| correlationContext = listOptions?.requestOptions?.headers?.[CORRELATION_CONTEXT_HEADER_NAME] ?? ""; |
There was a problem hiding this comment.
I am thinking maybe we should verify the request header instead of checking requestOptions in the request tracing test.
| * and @see FeatureFlagClient SDK clients. Request tracing is applied per call before delegating to the | ||
| * corresponding SDK client. | ||
| */ | ||
| export class AppConfigClient implements IAppConfigurationClient { |
There was a problem hiding this comment.
I think we can name this class (and file) as AppConfigurationClient and then
import {
AppConfigurationClient as ConfigurationClient,
} from "@azure/app-configuration";| // Licensed under the MIT license. | ||
|
|
||
| import { AppConfigurationClient, AppConfigurationClientOptions } from "@azure/app-configuration"; | ||
| import { AppConfigurationClient, AppConfigurationClientOptions, FeatureFlagClient } from "@azure/app-configuration"; |
There was a problem hiding this comment.
export interface FeatureFlagClientOptions extends AppConfigurationClientOptions {}It is technically feasible to use AppConfigurationClientOptions for FeatureFlagClient.
Do we intentionally use the same options for both clients?
If not, we should have separated FeatureFlagClientOptions
| * A single client abstraction for an Azure App Configuration endpoint that exposes the subset of | ||
| * operations the provider needs from both the @see AppConfigurationClient and the @see FeatureFlagClient. | ||
| */ | ||
| export interface IAppConfigurationClient { |
There was a problem hiding this comment.
We don't need the IAppConfigurationClient abstraction in javascript. appConfigurationImpl should use the AppConfigClient directly. In JS, when you define an interface, in most of cases, the motivation is to export it for public use. But the AppConfigurationClient is completely for internal usage.
| }); | ||
|
|
||
| // The dedicated feature flag endpoint is queried in addition to feature flags; default to an empty result set. | ||
| sinon.stub(FeatureFlagClient.prototype, "listFeatureFlags").callsFake((listOptions) => { |
There was a problem hiding this comment.
Do we have similar method like checkConfigurationSettings to send HEAD request for feature flags?
| }); | ||
|
|
||
| // The dedicated feature flag endpoint is queried in addition to feature flags; default to an empty result set. | ||
| sinon.stub(FeatureFlagClient.prototype, "listFeatureFlags").callsFake((listOptions) => { |
There was a problem hiding this comment.
I'd like to have it in a different function or update the function name. The current function name is related to configuration settings.
| return getMockedHeadIterator(pages, listOptions); | ||
| }); | ||
| sinon.stub(clientWrapper.client, "listFeatureFlags").callsFake((listOptions) => { | ||
| countObject.count += 1; |
There was a problem hiding this comment.
I think we should update the countObject to record call numbers for both feature flag and configuration settings.
e.g. kvCallCount and ffCallCount (let copilot come up with a better name)
| }); | ||
|
|
||
| // Creates an enhanced feature flag as returned by the dedicated feature flag endpoint (FeatureFlagClient.listFeatureFlags). | ||
| const createMockedEnhancedFeatureFlag = (name: string, props?: any) => Object.assign({ |
There was a problem hiding this comment.
How about enhancing the existing createMockedFeatureFlag by passing a flag isConfigurationSetting
If false, it will create a FeatureFlag shape object
| } | ||
|
|
||
| expect(userAgent).satisfy((ua: string) => ua.startsWith("javascript-appconfiguration-provider")); | ||
| expect(userAgent).satisfy((ua: string) => ua.includes("javascript-appconfiguration-provider")); |
There was a problem hiding this comment.
What's the reason of this change? Is there any behavior change on how we set user agent?
There was a problem hiding this comment.
behavior change is from SDK side, I think we have discussed this offline before. Now the useragent is like this "User-Agent": "azsdk-js-app-configuration/1.13.0-beta.1 javascript-appconfiguration-provider/2.5.0-preview azsdk-js-app-configuration/1.13.0-beta.1 core-rest-pipeline/1.25.0 Node/22.19.0 (Windows_NT 10.0.26200; x64)",
|
|
||
| /** | ||
| * Converts @see FeatureFlag returned by the feature flag endpoint into the | ||
| * Microsoft Feature Management schema object (snake_case) used within the `feature_management.feature_flags` |
There was a problem hiding this comment.
| * Microsoft Feature Management schema object (snake_case) used within the `feature_management.feature_flags` | |
| * Microsoft Feature Flag schema object used within the `feature_management.feature_flags` |
| import { FeatureFlag } from "@azure/app-configuration"; | ||
|
|
||
| /** | ||
| * Converts @see FeatureFlag returned by the feature flag endpoint into the |
There was a problem hiding this comment.
| * Converts @see FeatureFlag returned by the feature flag endpoint into the | |
| * Converts @see FeatureFlag into the |
| * array. This mirrors the shape produced by parsing a classic feature flag key-value, so that downstream | ||
| * feature management parsing and the provider's telemetry/tracing logic are unchanged. | ||
| */ | ||
| export function convertToMicrosoftSchema(featureFlag: FeatureFlag): any { |
There was a problem hiding this comment.
| export function convertToMicrosoftSchema(featureFlag: FeatureFlag): any { | |
| export function convert(featureFlag: FeatureFlag): any { |
| * @param selectors - The @see PagedSettingsWatcher of the enhanced feature flag collection. | ||
| * @returns true if the enhanced feature flag collection has changed, false otherwise. | ||
| */ | ||
| async #checkEnhancedFeatureFlagsChange(selectors: PagedSettingsWatcher[]): Promise<boolean> { |
There was a problem hiding this comment.
Is HEAD request supported for new feature flag?
| async #loadFeatureFlags() { | ||
| const loadFeatureFlag = true; | ||
| const featureFlagSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings(loadFeatureFlag); | ||
| async #loadFeatureFlags(): Promise<ConfigurationSetting[]> { |
There was a problem hiding this comment.
loadFeatureFlags still returns void to make it symmetric to loadSelectedKeyValues
This will make the #initializeWithRetryPolicy code path very clear (reader don't need to know about the context about old/new feature flag business logic)
All the business logic of loading and merging old and new feature flags happen in loadFeatureFlags
We can two new methods:
- loadFeatureFlagConfigurationSettings returns ConfigurationSettings[]
This method should reuse the previous loadConfigurationSettings(boolean)
Currently, #loadSelectedKeyValues and #loadFeatureFlags share a lot of similar code.
But if we keep using the previous loadConfigurationSettings(isLoadFeatureFlag), we can save duplicated code.
- loadEnhancedFeatureFlags returns FeatureFlag[]
loadFeatureFlags should be
ffs = loadFeatureFlagConfigurationSettings();
enhancedFfs = loadEnhancedFeatureFlags();
// remaining logic of merging ffs and enhanced ffs
| // Deep clone so the caller's option objects are never mutated. | ||
| const clonedSelectors = structuredClone(selectors); | ||
| clonedSelectors.forEach(selector => { | ||
| if (selector.keyFilter) { | ||
| selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; | ||
| } | ||
| }); | ||
| return getValidSettingSelectors(selectors); | ||
| return getValidSettingSelectors(clonedSelectors); |
There was a problem hiding this comment.
| // Deep clone so the caller's option objects are never mutated. | |
| const clonedSelectors = structuredClone(selectors); | |
| clonedSelectors.forEach(selector => { | |
| if (selector.keyFilter) { | |
| selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`; | |
| } | |
| }); | |
| return getValidSettingSelectors(selectors); | |
| return getValidSettingSelectors(clonedSelectors); | |
| // Create prefixed copies because the original selectors are also used as unprefixed selectors for enhanced feature flags. | |
| const prefixedSelectors = selectors.map(selector => ({ | |
| ...selector, | |
| keyFilter: selector.keyFilter | |
| ? `${featureFlagPrefix}${selector.keyFilter}` | |
| : selector.keyFilter | |
| })); | |
| return getValidSettingSelectors(prefixedSelectors); |
| this.#useEnhancedFeatureFlag = enhancedFeatureFlags.length > 0; | ||
|
|
||
| // Exclude any feature flags that are superseded by an enhanced feature flag with the same name. | ||
| const ineligibleFeatureFlagKeys = new Set(enhancedFeatureFlags.map(ff => featureFlagPrefix + ff.name)); |
There was a problem hiding this comment.
| const ineligibleFeatureFlagKeys = new Set(enhancedFeatureFlags.map(ff => featureFlagPrefix + ff.name)); | |
| const supersededFeatureFlagKeys = new Set(enhancedFeatureFlags.map(ff => featureFlagPrefix + ff.name)); |
| @@ -989,31 +1106,48 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { | |||
| } | |||
| const featureFlag = JSON.parse(rawFlag); | |||
There was a problem hiding this comment.
| const featureFlag = JSON.parse(rawFlag); | |
| const parsedFeatureFlag = JSON.parse(rawFlag); |
There was a problem hiding this comment.
Make it symmetric to another parseFf function
Summary
Adds support for loading feature flags from new endpoint (via
FeatureFlagClient.listFeatureFlags), in addition to the classic key-value path. When feature flags are enabled, the provider now loads flags from both sources and merges them, with flags from the new endpoint taking precedence over classic ones of the same name. Reference Azure/AppConfiguration-DotnetProvider#738Key changes
Added
appConfigClient.ts: anIAppConfigurationClientinterface plusAppConfigClientimplementation that wraps bothAppConfigurationClient(classic KV) andFeatureFlagClient(new FF endpoint). Each method applies request tracing before delegating to the underlying SDK client.Added
featureFlagConverter.tswithconvertToMicrosoftSchema(), which maps the SDK's typedFeatureFlag(camelCase) into the Microsoft Feature Management schema consumed byfeature_management.feature_flags.Load and merge on init and on refresh:
#loadClassicFeatureFlags()+#loadFeatureFlags() →#setFeatureFlags()dedups by name (new endpoint supersedes classic).Change detection covers both paths