Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions docs/remote-feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,28 +83,31 @@ Choose the appropriate feature flag type based on your needs:
]
```

Threshold entries without `thresholdVersion` use the legacy output shape. After the controller selects the matching threshold entry, the processed flag is:
After the controller selects the matching threshold entry, `remoteFeatureFlags[flagName]` contains the entry's `value` directly. For example, if the first entry above is selected:

```json
{
"name": "feature is ON",
"value": true
}
true
```

When the selected threshold entry includes `name`, the controller stores the group label separately on controller state in `featureFlagThresholdGroups`:

```typescript
remoteFeatureFlags.myThresholdFlag; // true
featureFlagThresholdGroups.myThresholdFlag; // "feature is ON"
```

Use this shape when selectors read the selected flag data from `.value`.
Read flag payloads from `remoteFeatureFlags`. Read A/B group assignment from `featureFlagThresholdGroups` when the selected entry includes `name`.

#### Direct-value threshold entries
#### Object-valued threshold entries

Use `thresholdVersion: 2` when a threshold-based flag should expose the selected `value` directly, matching the shape of a non-threshold object flag. This is useful when converting an existing object flag to threshold rollout and existing selectors read root-level fields such as `flag.enabled`.
Threshold entries can return object values directly, matching the shape of a non-threshold object flag. This is useful when converting an existing object flag to threshold rollout and existing selectors read root-level fields such as `flag.enabled`.

When using `thresholdVersion: 2`, use `thresholdName` instead of `name` to label the threshold entry. `thresholdName` is metadata for configuration readability and is not included in the processed feature flag value. Because the selected entry resolves directly to its `value`, this avoids confusion or collisions with any `name` field inside the value object itself. `thresholdName` is recommended for clarity, but the controller selects entries using `scope` and returns `value`.
Use `name` to label each threshold entry. The selected entry's `value` is returned directly from `remoteFeatureFlags`, and the selected entry's `name` is stored separately in `featureFlagThresholdGroups`.

```json
[
{
"thresholdName": "feature is ON",
"thresholdVersion": 2,
"name": "feature is ON",
"scope": {
"type": "threshold",
"value": 0.3
Expand All @@ -115,8 +118,7 @@ When using `thresholdVersion: 2`, use `thresholdName` instead of `name` to label
}
},
{
"thresholdName": "feature is OFF",
"thresholdVersion": 2,
"name": "feature is OFF",
"scope": {
"type": "threshold",
"value": 1
Expand All @@ -137,7 +139,12 @@ If the first entry is selected, the processed flag is:
}
```

Existing non-threshold object flags do not need `thresholdVersion`. This property is only used on entries inside threshold arrays.
The selected group label is available separately:

```typescript
remoteFeatureFlags.myObjectThresholdFlag; // { enabled: true, minimumVersion: "13.10.0" }
featureFlagThresholdGroups.myObjectThresholdFlag; // "feature is ON"
```

The distribution is deterministic based on the user's `metametricsId`, ensuring consistent group assignment across sessions.

Expand Down Expand Up @@ -168,7 +175,7 @@ In this example: users get different UI configurations based on their app versio

The version matching is deterministic and uses semantic version comparison to find the highest version that is less than or equal to the current app version.

`thresholdVersion` is not used for this flag type. The selected version value is returned as-is.
Threshold entries are not used for this flag type. The selected version value is returned as-is.

**4. Composing version-based scope with threshold scope**: Use when you need to control both version targeting and user percentage rollout simultaneously.

Expand All @@ -180,17 +187,15 @@ In this example: the feature is rolled out to 30% of users on v13.0.0+, and 100%
"versions": {
"13.0.0": [
{
"thresholdName": "gradual rollout",
"thresholdVersion": 2,
"name": "gradual rollout",
"scope": {
"type": "threshold",
"value": 0.3
},
"value": { "enabled": true }
},
{
"thresholdName": "disabled",
"thresholdVersion": 2,
"name": "disabled",
"scope": {
"type": "threshold",
"value": 1
Expand All @@ -200,8 +205,7 @@ In this example: the feature is rolled out to 30% of users on v13.0.0+, and 100%
],
"13.2.0": [
{
"thresholdName": "full rollout",
"thresholdVersion": 2,
"name": "full rollout",
"scope": {
"type": "threshold",
"value": 1
Expand All @@ -219,7 +223,7 @@ In this example: the feature is rolled out to 30% of users on v13.0.0+, and 100%
- v13.2.1 user (any bucket) → `{ "enabled": true }` (100% rollout)
- v12.9.0 user → Feature excluded (no matching versions)

When composing version-based and threshold scopes, place `thresholdVersion: 2` inside each threshold entry under the relevant version. Without `thresholdVersion: 2`, the selected threshold entry uses the legacy `{ "name": "...", "value": ... }` wrapper shape.
When composing version-based and threshold scopes, place threshold entries under the relevant version. Processed threshold values are returned directly from `remoteFeatureFlags`; use `featureFlagThresholdGroups` for group names when entries include `name`.

## Implementation Guide

Expand Down Expand Up @@ -307,10 +311,7 @@ In production, MetaMask Extension fetches remote flags from the [client-config A
{
"_flags": {
"remoteFeatureFlags": {
"testFlagForThreshold": {
"name": "test-flag",
"value": "121212"
}
"testFlagForThreshold": true
}
}
}
Expand Down
Loading