Skip to content

load feature flags from new endpoint - #341

Open
linglingye001 wants to merge 5 commits into
previewfrom
linglingye/load-new-flag
Open

load feature flags from new endpoint#341
linglingye001 wants to merge 5 commits into
previewfrom
linglingye/load-new-flag

Conversation

@linglingye001

Copy link
Copy Markdown
Member

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#738

Key changes

  • Added appConfigClient.ts: an IAppConfigurationClient interface plus AppConfigClient implementation that wraps both AppConfigurationClient (classic KV) and FeatureFlagClient (new FF endpoint). Each method applies request tracing before delegating to the underlying SDK client.

  • Added featureFlagConverter.ts with convertToMicrosoftSchema(), which maps the SDK's typed FeatureFlag (camelCase) into the Microsoft Feature Management schema consumed by feature_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

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: {

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change intentional? Or it is accidently introduced by ai?

It should be customHeaders

@linglingye001 linglingye001 Aug 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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] ?? "";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking maybe we should verify the request header instead of checking requestOptions in the request tracing test.

Comment thread src/appConfigClient.ts
* and @see FeatureFlagClient SDK clients. Request tracing is applied per call before delegating to the
* corresponding SDK client.
*/
export class AppConfigClient implements IAppConfigurationClient {

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/appConfigClient.ts
* 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread test/utils/testHelper.ts
});

// 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) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have similar method like checkConfigurationSettings to send HEAD request for feature flags?

Comment thread test/utils/testHelper.ts
});

// 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) => {

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to have it in a different function or update the function name. The current function name is related to configuration settings.

Comment thread test/utils/testHelper.ts
return getMockedHeadIterator(pages, listOptions);
});
sinon.stub(clientWrapper.client, "listFeatureFlags").callsFake((listOptions) => {
countObject.count += 1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment thread test/utils/testHelper.ts
});

// Creates an enhanced feature flag as returned by the dedicated feature flag endpoint (FeatureFlagClient.listFeatureFlags).
const createMockedEnhancedFeatureFlag = (name: string, props?: any) => Object.assign({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about enhancing the existing createMockedFeatureFlag by passing a flag isConfigurationSetting

If false, it will create a FeatureFlag shape object

Comment thread test/afd.test.ts
}

expect(userAgent).satisfy((ua: string) => ua.startsWith("javascript-appconfiguration-provider"));
expect(userAgent).satisfy((ua: string) => ua.includes("javascript-appconfiguration-provider"));

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason of this change? Is there any behavior change on how we set user agent?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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 {

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is HEAD request supported for new feature flag?

async #loadFeatureFlags() {
const loadFeatureFlag = true;
const featureFlagSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings(loadFeatureFlag);
async #loadFeatureFlags(): Promise<ConfigurationSetting[]> {

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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.

  1. loadEnhancedFeatureFlags returns FeatureFlag[]

loadFeatureFlags should be

ffs =  loadFeatureFlagConfigurationSettings();
enhancedFfs = loadEnhancedFeatureFlags();
// remaining logic of merging ffs and enhanced ffs

Comment on lines +1373 to +1380
// 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const featureFlag = JSON.parse(rawFlag);
const parsedFeatureFlag = JSON.parse(rawFlag);

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it symmetric to another parseFf function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants