fix: Normalize remote feature flag value wrappers#8908
fix: Normalize remote feature flag value wrappers#8908pedronfigueiredo wants to merge 5 commits into
Conversation
| }; | ||
| } | ||
|
|
||
| function normalizeFeatureFlagValue(featureFlagValue: Json): Json { |
There was a problem hiding this comment.
For clarity, should we call this normalizeThresholdValue?
|
|
||
| processedFlags[remoteFeatureFlagName] = processedValue; | ||
| processedFlags[remoteFeatureFlagName] = | ||
| normalizeFeatureFlagValue(processedValue); |
There was a problem hiding this comment.
As we also normalize for versioning, would it be simpler to normalize on line 412 above, so a function specific for getting threshold data?
|
|
||
| function normalizeFeatureFlagValue(featureFlagValue: Json): Json { | ||
| return isFeatureFlagValueWrapper(featureFlagValue) | ||
| ? spreadFeatureFlagValueWrapper(featureFlagValue) |
There was a problem hiding this comment.
Am I misreading or is this saying if the flag is an object with a value property, then return the value property directly also?
But won't that mean we duplicate the data in every threshold flag?
Could we check (just for the threshold flow) if a thresholdVersion property exists and if set to 2, then we just return value directly? And maybe with a thresholdName property instead of name for clarity?
There was a problem hiding this comment.
Yes we do duplicate because although we know in this function what is the shape of the flag defined on launch darkly, we have no way of knowing at this point what is the shape of the data that the selector needs or expects. In other words, we need this change to be backwards compatible.
We have to support three cases:
-
If a threshold hasn't been defined, then just add the whole flag
-
If a threshold is defined, and the selectors are currently defined without
.value, we want to spread the .value at the root level so no selector change is required (to support ourconfirmations_paywith thresholds use case) -
If a threshold is defined, and the selectors already have
.value, we also need to keep the .value prop that the selector expects.
To support 2 and 3 simultaneously we need to duplicate the data.
I am not sure what you mean by the thresholdVersion prop with value 2, I can't see it in the client config api response. Can you clarify?
There was a problem hiding this comment.
I was suggesting we make the distinction explicit with a new property such as thresholdVersion: 2 that we put in one of the array entries, so the code knows to only spread the value, rather than including it as it does currently.
Both should solve the problem, but didn't know if we wanted to future-proof and make the RemoteFeatureFlagController state more readable?
37347e8 to
8576910
Compare
66f42ad to
0f896ee
Compare
a525b59 to
761c717
Compare
| return null; | ||
| } | ||
|
|
||
| return isFeatureFlagValueWrapper(versionData) |
There was a problem hiding this comment.
Doesn't the chosen version data get assigned to processedValue and then hit the standard threshold flow so we don't need to touch this?
| } | ||
|
|
||
| function normalizeThresholdValue(featureFlag: FeatureFlagScopeValue): Json { | ||
| if (featureFlag.thresholdVersion === THRESHOLD_VALUE_VERSION) { |
There was a problem hiding this comment.
Minor, if we add future versions, this constant might get confusing. Maybe an enum?
|
|
||
| function normalizeThresholdValue(featureFlag: FeatureFlagScopeValue): Json { | ||
| if (featureFlag.thresholdVersion === THRESHOLD_VALUE_VERSION) { | ||
| return featureFlag.value; |
There was a problem hiding this comment.
Minor, could still be useful to also include thresholdName property only if an object, for debug in the client?
Not sure how this will impact the new AB testing hooks.
There was a problem hiding this comment.
good points let's add that if and when it becomes necessary then
| const name = featureFlag.thresholdName ?? featureFlag.name; | ||
|
|
||
| return { | ||
| ...(isJsonObject(featureFlag.value) ? featureFlag.value : {}), |
There was a problem hiding this comment.
If this is the fallback for the existing support, can we just return the same?
{
name: featureFlag.name,
value: featureFlag.value
}
There was a problem hiding this comment.
good point, I forgot to revert this part
| ); | ||
| } | ||
|
|
||
| function spreadFeatureFlagValueWrapper( |
| return typeof value === 'object' && value !== null && !Array.isArray(value); | ||
| } | ||
|
|
||
| function isFeatureFlagValueWrapper( |
| }; | ||
| } | ||
|
|
||
| function isJsonObject(value: Json): value is JsonObject { |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9dc313d. Configure here.

There is an issue with the current remote feature flag implementation.
As you can see when the feature flags are configured for threshold based config, we define an array of objects that contains name, scope, and value. The problem with this is that there's lots of feature flags that are not defined in this format, and the selectors are unfortunately coupled to this format.
getIsNotificationEnabledByDefaultFeatureFlaginsideui/selectors/metamask-notifications/metamask-notifications.tsexpects a.valueproperty for example. Butconfirmations_payfeature flag, we don't expect such .value property to house the configuration JSON, and instead it's available directly at the root level.Summary
valuefield.valuecontents onto the root flag object while preserving the original.valuefield.Root Cause
Some consumers read feature flag config directly from the flag object, while threshold or version wrapper shapes expose config under
.value. This made selectors depend on the rollout format instead of the feature flag config itself.Impact
Feature flags can move between direct, threshold, and versioned configurations without requiring selectors to switch between direct config access and
.valueaccess. Existing consumers that still read.valueremain compatible.Note
Medium Risk
Changes the shape of processed threshold flags for v2 entries, which can affect any consumer expecting the legacy wrapper; legacy behavior is unchanged.
Overview
Threshold-based remote feature flags now normalize the winning segment through
normalizeThresholdValue, instead of always emitting{ name, value }.Entries with
thresholdVersion: 2expose the selected config object directly (e.g.{ enabled, minimumVersion }), so UI code can read flags the same way as non-threshold configs. Legacy threshold rows without that version still get thename/valuewrapper. OptionalthresholdNameandthresholdVersionwere added toFeatureFlagScopeValue, with tests covering direct configs, legacy wrappers, and v2 behavior.Reviewed by Cursor Bugbot for commit 363c9fd. Bugbot is set up for automated code reviews on this repo. Configure here.