docs: update remote feature flag threshold API - #176
Closed
asalsys wants to merge 1 commit into
Closed
Conversation
Document featureFlagThresholdGroups, canonical ID as the default threshold
segmentation identifier, and removal of the legacy { name, value } wrapper.
Co-authored-by: Cursor <cursoragent@cursor.com>
pull Bot
pushed a commit
to dmrazzy/core
that referenced
this pull request
Jul 2, 2026
…y and track threshold groups (MetaMask#9289) ## Summary - **BREAKING:** Threshold feature flags now return the selected `value` directly from `remoteFeatureFlags` instead of a `{ name, value }` wrapper object. - Add optional `featureFlagThresholdGroups` state field (`Record<string, string>`) that maps feature flag names to their selected threshold group name when the selected threshold entry includes `name`. - Remove `normalizeThresholdValue` and the legacy `{ name, value }` wrapper fallback. ## References - Fixes [MCWP-655](https://consensyssoftware.atlassian.net/browse/MCWP-655) ## Motivation Separating the processed flag value from threshold group metadata makes it easier for consumers to use threshold flags as plain values (booleans, objects, etc.) without unwrapping. The new `featureFlagThresholdGroups` field provides explicit access to A/B group assignment when the selected threshold entry includes `name`. Entries that only define `thresholdName` (for example in `thresholdVersion: 2` configs) still resolve to the selected `value`, but do not populate `featureFlagThresholdGroups` because that field is driven by `name`. ## Migration Consumers that previously read threshold group names from `remoteFeatureFlags[flagName].name` should instead: 1. Read the flag value directly from `remoteFeatureFlags[flagName]`. 2. Read the threshold group name from `featureFlagThresholdGroups[flagName]` when available (populated when the selected threshold entry includes `name`). **Before:** ```ts const flag = remoteFeatureFlags.myThresholdFlag; // { name: 'groupB', value: { enabled: true } } const groupName = flag.name; const value = flag.value; ``` **After:** ```ts const value = remoteFeatureFlags.myThresholdFlag; // { enabled: true } const groupName = featureFlagThresholdGroups.myThresholdFlag; // 'groupB' ``` ## Related - Documentation: [MetaMask/contributor-docs#176](MetaMask/contributor-docs#176) ## Test plan - [x] `yarn workspace @metamask/remote-feature-flag-controller run test` — all tests pass with 100% coverage - [x] Threshold flags return the selected `value` directly from `remoteFeatureFlags` - [x] Threshold flags populate `featureFlagThresholdGroups` when the selected entry includes `name` - [x] Threshold flags with only `thresholdName` return the value directly and leave `featureFlagThresholdGroups` empty - [x] Stale `featureFlagThresholdGroups` entries are cleaned up when flags are removed from the server response [MCWP-655]: https://consensyssoftware.atlassian.net/browse/MCWP-655?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking change to the public shape of threshold flag values affects all consumers of `remoteFeatureFlags`; persisted state gains a new field but assignment logic is otherwise unchanged. > > **Overview** > **BREAKING:** Threshold/A/B flags in `remoteFeatureFlags` now expose the selected entry’s **`value` only** (string, boolean, object, etc.) instead of a `{ name, value }` wrapper. Group assignment moves to a new optional persisted state field **`featureFlagThresholdGroups`** (`flagName → group name`) when the winning threshold item has a `name`. > > Processing drops **`normalizeThresholdValue`** and **`ThresholdVersion`** branching—legacy wrappers and “direct value vs wrapper” version handling are gone; threshold arrays are resolved the same way regardless of `thresholdVersion`. On each fetch, **`featureFlagThresholdGroups` is rebuilt** from the current server payload so entries for removed flags are cleared (covered by tests). > > **Migration:** read values from `remoteFeatureFlags[flag]`; read group names from `featureFlagThresholdGroups[flag]` when present. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 98b74a2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mark Stacey <markjstacey@gmail.com>
basgys
pushed a commit
to basgys/metamask-core
that referenced
this pull request
Aug 20, 2026
…MetricsFlags with canonical ID default (MetaMask#9325) ## Summary - Add required `getCanonicalProfileId` constructor callback and `metaMetricsFlags` map for threshold flag segmentation. - Threshold flags use canonical profile ID by default; flags whose names are present in `metaMetricsFlags` use MetaMetrics ID instead. - Generalize threshold calculation to hash the selected segmentation ID while preserving the existing deterministic bucketing behavior. - Pass the new segmentation options through `@metamask/wallet` remote feature flag controller initialization. ## Segmentation Behavior | Flag configuration | Identifier used | Constructor callback | | --- | --- | --- | | Flag name absent from `metaMetricsFlags` | Canonical profile ID | `getCanonicalProfileId()` | | Flag name present in `metaMetricsFlags` | MetaMetrics ID | `getMetaMetricsId()` | If the selected identifier is empty, threshold arrays are preserved as-is and not processed. Example threshold config using canonical segmentation by default: ```json { "myCanonicalFlag": [ { "name": "groupA", "scope": { "type": "threshold", "value": 0.5 }, "value": "valueA" }, { "name": "groupB", "scope": { "type": "threshold", "value": 1.0 }, "value": "valueB" } ] } ``` Example controller setup for a flag that should keep MetaMetrics segmentation: ```ts new RemoteFeatureFlagController({ // ... getMetaMetricsId, getCanonicalProfileId, metaMetricsFlags: { legacyMetaMetricsFlag: true, }, }); ``` ## Migration Consumers must pass `getCanonicalProfileId` for threshold flags that should use canonical profile segmentation, which is now the default for flags absent from `metaMetricsFlags`. Existing threshold flags that must keep MetaMetrics-based bucketing should be listed in `metaMetricsFlags` and continue to provide `getMetaMetricsId`. ## Test plan - [x] `yarn workspace @metamask/remote-feature-flag-controller run jest --no-coverage src/utils/user-segmentation-utils.test.ts src/remote-feature-flag-controller.test.ts` - [x] Threshold flags absent from `metaMetricsFlags` use `getCanonicalProfileId` for segmentation - [x] Threshold flags present in `metaMetricsFlags` use `getMetaMetricsId` for segmentation - [x] Threshold arrays are preserved when the selected identifier is empty - [x] Threshold cache entries use the selected segmentation ID and stale entries are cleaned up when flags are removed from the server response ## Related - Documentation: [MetaMask/contributor-docs#176](MetaMask/contributor-docs#176) - Ticket - https://consensyssoftware.atlassian.net/browse/MCWP-662 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Breaking constructor and experiment-assignment change: existing threshold flags will re-bucket unless listed in `metaMetricsFlags`. Not auth or payment logic, but it can change live A/B group membership. > > **Overview** > **Breaking:** threshold flags now bucket by **canonical profile ID** by default instead of MetaMetrics ID. `RemoteFeatureFlagController` requires `getCanonicalProfileId`; flags listed in optional `metaMetricsFlags` still use `getMetaMetricsId`. > > Hash-based assignment hashes the chosen segmentation ID. If that ID is empty, the threshold array is left unprocessed. Explicit MetaMetrics ID targeting is unchanged. Threshold cache keys and stale-entry cleanup cover both identifiers. > > `@metamask/wallet` forwards the new options (optional; empty-string default for canonical ID). Consumers that need the old MetaMetrics bucketing must list those flag names in `metaMetricsFlags`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 09b141f. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Cal-L <cal.leung@consensys.net> Co-authored-by: Cal Leung <cleun007@gmail.com> Co-authored-by: Elliot Winkler <elliot.winkler@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Updates remote feature flags documentation to reflect the
RemoteFeatureFlagControllerAPI changes in MetaMask/core#9325, MetaMask/core#9289:valuedirectly fromremoteFeatureFlags(no{ name, value }wrapper).featureFlagThresholdGroupswhen the winning threshold entry includesname.idTypeon threshold entries (canonicalby default,metametricsfor legacy MetaMetrics segmentation).Test plan
RemoteFeatureFlagControllerbehavior in core PR #9325Made with Cursor