Enhanced feature flags - #73
Conversation
There was a problem hiding this comment.
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
appConfigClientabstraction (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.
| } | ||
|
|
||
| if !eTagChanged { | ||
| if !ffChanged { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
It's intentional, I remember it was discussed before that we always make call for both flags.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
| populateTelemetryMetadata(ff, setting.ETag, generateFeatureFlagReference(clientEndpoint, "kv", *setting.Key, setting.Label)) | |
| populateTelemetryMetadata(ff, setting.ETag, generateFeatureFlagReference(clientEndpoint, "ff", *setting.Key, setting.Label)) |
There was a problem hiding this comment.
we use kv path for flags loaded from key-value endpoint, and ff for enhanced flags
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
| populateTelemetryMetadata(ff, flag.ETag, generateFeatureFlagReference(clientEndpoint, "ff", *flag.Name, flag.Label)) | |
| populateTelemetryMetadata(ff, flag.ETag, generateFeatureFlagReference(clientEndpoint, "enhancedFF", *flag.Name, flag.Label)) |
|
Yes, I did end to end test in my local env.
Updated. |
| 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 | ||
| }) |
There was a problem hiding this comment.
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.
| 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]) |
…Configuration-GoProvider into linglingye/enhanced-ff
| return reference | ||
| } | ||
|
|
||
| func populateTelemetryMetadata(featureFlag map[string]any, eTag *azcore.ETag, reference string) { |
There was a problem hiding this comment.
It seems like we don't populate AllocationId in go provider
| } | ||
|
|
||
| var parsed any | ||
| if err := json.Unmarshal([]byte(*raw), &parsed); err == nil { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
We should respect variant content type


No description provided.