Skip to content

fix: Normalize remote feature flag value wrappers#8908

Open
pedronfigueiredo wants to merge 5 commits into
mainfrom
pnf/remote-feature-flag-renormalization
Open

fix: Normalize remote feature flag value wrappers#8908
pedronfigueiredo wants to merge 5 commits into
mainfrom
pnf/remote-feature-flag-renormalization

Conversation

@pedronfigueiredo
Copy link
Copy Markdown
Contributor

@pedronfigueiredo pedronfigueiredo commented May 27, 2026

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.

getIsNotificationEnabledByDefaultFeatureFlag inside ui/selectors/metamask-notifications/metamask-notifications.ts expects a .value property for example. But confirmations_pay feature flag, we don't expect such .value property to house the configuration JSON, and instead it's available directly at the root level.

Summary

  • Normalize processed remote feature flag wrapper objects that include a top-level value field.
  • Copy object-valued value contents onto the root flag object while preserving the original .value field.
  • Preserve direct feature flag config objects that do not use wrapper metadata.

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 .value access. Existing consumers that still read .value remain 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: 2 expose 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 the name / value wrapper. Optional thresholdName and thresholdVersion were added to FeatureFlagScopeValue, 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.

@pedronfigueiredo pedronfigueiredo changed the title [codex] Normalize remote feature flag value wrappers fix: Normalize remote feature flag value wrappers May 27, 2026
@pedronfigueiredo pedronfigueiredo marked this pull request as ready for review May 27, 2026 16:37
@pedronfigueiredo pedronfigueiredo requested review from a team as code owners May 27, 2026 16:37
};
}

function normalizeFeatureFlagValue(featureFlagValue: Json): Json {
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.

For clarity, should we call this normalizeThresholdValue?


processedFlags[remoteFeatureFlagName] = processedValue;
processedFlags[remoteFeatureFlagName] =
normalizeFeatureFlagValue(processedValue);
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.

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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:

  1. If a threshold hasn't been defined, then just add the whole flag

  2. 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 our confirmations_pay with thresholds use case)

  3. 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?

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 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?

@pedronfigueiredo pedronfigueiredo force-pushed the pnf/remote-feature-flag-renormalization branch from 37347e8 to 8576910 Compare May 28, 2026 09:59
@pedronfigueiredo pedronfigueiredo force-pushed the pnf/remote-feature-flag-renormalization branch from 66f42ad to 0f896ee Compare May 28, 2026 11:31
Comment thread packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts Outdated
@pedronfigueiredo pedronfigueiredo force-pushed the pnf/remote-feature-flag-renormalization branch from a525b59 to 761c717 Compare May 28, 2026 13:03
return null;
}

return isFeatureFlagValueWrapper(versionData)
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.

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) {
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.

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 : {}),
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.

If this is the fallback for the existing support, can we just return the same?

{
    name: featureFlag.name,
    value: featureFlag.value
}

Copy link
Copy Markdown
Contributor Author

@pedronfigueiredo pedronfigueiredo May 28, 2026

Choose a reason for hiding this comment

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

good point, I forgot to revert this part

);
}

function spreadFeatureFlagValueWrapper(
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.

Not needed?

return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function isFeatureFlagValueWrapper(
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.

Not needed?

};
}

function isJsonObject(value: Json): value is JsonObject {
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.

Not needed?

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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

Comment thread packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts Outdated
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