Skip to content

Enhanced feature flags - #73

Open
linglingye001 wants to merge 5 commits into
release/v1.7.0-previewfrom
linglingye/enhanced-ff
Open

Enhanced feature flags#73
linglingye001 wants to merge 5 commits into
release/v1.7.0-previewfrom
linglingye/enhanced-ff

Conversation

@linglingye001

Copy link
Copy Markdown
Member

No description provided.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces enhanced feature flags support by loading feature flags from both classic key-value settings and the dedicated feature-flag endpoint, then merging them into the Microsoft Feature Management schema. It also updates client abstractions to support both settings and feature-flag operations and extends refresh/tracing behavior accordingly.

Changes:

  • Add enhanced feature flag loading + merging logic (classic KV feature flags + enhanced endpoint flags).
  • Introduce an appConfigClient abstraction (appConfigurationClient) to unify settings/snapshot/feature-flag operations.
  • Extend refresh + tracing to detect/enumerate enhanced feature flag usage and changes (via page ETags), and update tests.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
azureappconfiguration/snapshot_test.go Updates snapshot tests to pass an enhanced feature-flag client stub.
azureappconfiguration/settings_client.go Adds enhanced feature-flag loader + ETag-change monitor; switches clients to appConfigClient.
azureappconfiguration/refresh_test.go Extends refresh tests to include enhanced feature-flag loader/monitor fields.
azureappconfiguration/internal/tracing/tracing.go Adds correlation-context tags/options for enhanced feature flags.
azureappconfiguration/feature_flag.go New: feature-flag merge + schema conversion + telemetry metadata helpers.
azureappconfiguration/feature_flag_test.go New: unit tests for conversion/merge/dedup/ETag helpers and merged loading.
azureappconfiguration/failover_test.go Updates failover tests to use appConfigClient wrappers/types.
azureappconfiguration/constants.go Adds schema key constants needed by enhanced feature-flag conversion.
azureappconfiguration/client_manager.go Refactors client manager/wrapper types to appConfigClient abstraction and new constructors.
azureappconfiguration/azureappconfiguration.go Wires enhanced feature-flag loading/refresh into provider lifecycle and replaces old single-source FF loading.
azureappconfiguration/azureappconfiguration_test.go Adds newEmptyEnhancedFFClient() test helper and updates FF load tests for new signature/types.
azureappconfiguration/app_configuration_client.go New: concrete appConfigClient implementation backed by azappconfig clients.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread azureappconfiguration/settings_client.go
Comment thread azureappconfiguration/feature_flag.go Outdated
Comment thread azureappconfiguration/azureappconfiguration.go Outdated
Comment thread azureappconfiguration/app_configuration_client.go Outdated
Comment thread azureappconfiguration/azureappconfiguration.go Outdated
}

if !eTagChanged {
if !ffChanged {

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 wondering if the refresh logic here is an intentional decision: it currently reloads both flags when either is changed, which means more network calls if only one flag is changed. If the flag merge logic is correct, we should not need to reload both flags every time?

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.

It's intentional, I remember it was discussed before that we always make call for both 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.

Yuan Qu (@yuanqu72)

I am wondering if the refresh logic here is an intentional decision: it currently reloads both flags when either is changed, which means more network calls if only one flag is changed.

If we detect that some classic feature flags are missing or have been deleted, it could mean that the customer has adopted or migrated to the new enhanced feature flags. Therefore, we should reload both types of feature flags.

}

func rotateClientsToNextEndpoint(clients []*configurationClientWrapper, lastSuccessfulEndpoint string) {
func rotateClientsToNextEndpoint(clients []*appConfigClientWrapper, lastSuccessfulEndpoint string) {

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.

A note for future consideration: this file is very long and contains all the core logic for setup/load/refresh + helper methods. Maybe some helper methods can be separated out and only leave the high level orchestration logic in this file.

}

azappcfg.updateFeatureFlagTracing(ff)
populateTelemetryMetadata(ff, setting.ETag, generateFeatureFlagReference(clientEndpoint, "kv", *setting.Key, setting.Label))

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
populateTelemetryMetadata(ff, setting.ETag, generateFeatureFlagReference(clientEndpoint, "kv", *setting.Key, setting.Label))
populateTelemetryMetadata(ff, setting.ETag, generateFeatureFlagReference(clientEndpoint, "ff", *setting.Key, setting.Label))

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.

we use kv path for flags loaded from key-value endpoint, and ff for enhanced 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.

I find this confusing -> if the official name is ff for kv based ff and enhanced ff for the new one, we should stick to it everywhere. In this specific case, I think kv is OK, but using ff to indicate enhanced ff is very confusing.


ff := convertToMicrosoftSchema(flag)
azappcfg.updateFeatureFlagTracing(ff)
populateTelemetryMetadata(ff, flag.ETag, generateFeatureFlagReference(clientEndpoint, "ff", *flag.Name, flag.Label))

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
populateTelemetryMetadata(ff, flag.ETag, generateFeatureFlagReference(clientEndpoint, "ff", *flag.Name, flag.Label))
populateTelemetryMetadata(ff, flag.ETag, generateFeatureFlagReference(clientEndpoint, "enhancedFF", *flag.Name, flag.Label))

Comment thread azureappconfiguration/feature_flag.go
@yuanqu72

Copy link
Copy Markdown
Member
  • Did we run any integration tests/live tests that does the real network call and interact with the enhanced feature flag endpoint?
  • Please check the copilot suggestions, some of them seem reasonable.

@linglingye001

Copy link
Copy Markdown
Member Author
  • Did we run any integration tests/live tests that does the real network call and interact with the enhanced feature flag endpoint?

Yes, I did end to end test in my local env.

  • Please check the copilot suggestions, some of them seem reasonable.

Updated.

Comment on lines 255 to 267
eg.Go(func() error {
if keyValueRefreshed, err = azappcfg.refreshKeyValues(egCtx, azappcfg.newKeyValueRefreshClient(client)); err != nil {
return fmt.Errorf("failed to refresh key values: %w", err)
}
return nil
})

eg.Go(func() error {
if featureFlagRefreshed, err = azappcfg.refreshFeatureFlags(egCtx, azappcfg.newFeatureFlagRefreshClient(client)); err != nil {
return fmt.Errorf("failed to refresh feature flags: %w", err)
}
return nil
})

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.

These two routines write on the same err variable. Will there be any race issue?
For example, the kv routine got an error, but ff routine set the error to nil.

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.

Good catch. FIxed in #74

populateTelemetryMetadata(enabled, &etag, "https://fake.azconfig.io/ff/Beta")
metadata := enabled[telemetryKey].(map[string]any)[metadataKey].(map[string]any)
assert.Equal(t, "etag-1", metadata[eTagKey])
assert.Equal(t, "https://fake.azconfig.io/ff/Beta", metadata[featureFlagReferenceKey])

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.

Did we miss the feature flag prefix?

Image

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.

In dotnet, we have
image

@linglingye001 linglingye001 mentioned this pull request Aug 18, 2026
return reference
}

func populateTelemetryMetadata(featureFlag map[string]any, eTag *azcore.ETag, reference string) {

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 18, 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.

It seems like we don't populate AllocationId in go provider

}

var parsed any
if err := json.Unmarshal([]byte(*raw), &parsed); err == nil {

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 should align with the .NET PR where we only parse json when the string value looks like json (e.g. starts with "{")

json.Unmarshal will introduce a subtle difference here. For example , string "42" or "true" will be converted to number and boolean instead of keeping as string. This is different from other language.

variantMap[nameKey] = *variant.Name
}
if variant.Value != nil {
variantMap[configurationValueKey] = parseFeatureFlagValue(variant.Value)

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) Aug 18, 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.

We should respect variant content type

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.

4 participants