Skip to content

feat(remote-feature-flag-controller): segment threshold flags by idType with canonical ID default#9325

Open
asalsys wants to merge 4 commits into
mainfrom
feat/remote-feature-flag-threshold-groups-clean
Open

feat(remote-feature-flag-controller): segment threshold flags by idType with canonical ID default#9325
asalsys wants to merge 4 commits into
mainfrom
feat/remote-feature-flag-threshold-groups-clean

Conversation

@asalsys

@asalsys asalsys commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add FeatureFlagIdType (metametrics | canonical) and optional idType on threshold entries so each flag can choose which client identifier drives deterministic bucket assignment.
  • Add optional getCanonicalId constructor callback. Threshold processing uses getCanonicalId() by default when idType is omitted, and getMetaMetricsId() when idType is metametrics.
  • 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.

idType behavior

idType on threshold entry Identifier used Constructor callback
omitted (default) Canonical ID getCanonicalId()
"canonical" Canonical ID getCanonicalId()
"metametrics" MetaMetrics ID getMetaMetricsId()

If the configured identifier is empty, threshold arrays are preserved as-is and not processed.

Example threshold config using canonical segmentation (default):

[
  {
    "name": "groupA",
    "scope": { "type": "threshold", "value": 0.5 },
    "value": "valueA"
  },
  {
    "idType": "metametrics",
    "name": "legacyGroup",
    "scope": { "type": "threshold", "value": 1.0 },
    "value": "legacyValue"
  }
]

Migration

Threshold segmentation

Consumers must pass getCanonicalId when using threshold flags that rely on canonical segmentation (the default). Pass getMetaMetricsId as before for flags explicitly configured with idType: "metametrics".

Threshold value shape

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.

Before:

const flag = remoteFeatureFlags.myThresholdFlag;
// { name: 'groupB', value: { enabled: true } }
const groupName = flag.name;
const value = flag.value;

After:

const value = remoteFeatureFlags.myThresholdFlag;
// { enabled: true }

const groupName = featureFlagThresholdGroups.myThresholdFlag;
// 'groupB'

Test plan

  • yarn workspace @metamask/remote-feature-flag-controller run test — all tests pass with 100% coverage
  • Threshold flags default to getCanonicalId when idType is omitted
  • Threshold flags with idType: "metametrics" use getMetaMetricsId for segmentation
  • Threshold flags with idType: "canonical" use getCanonicalId for segmentation
  • Threshold flags return selected values directly and populate featureFlagThresholdGroups when the selected entry includes name
  • Stale featureFlagThresholdGroups and threshold cache entries are cleaned up when flags are removed from the server response

Related


Note

High Risk
Breaking changes to threshold flag shapes and default segmentation (canonical vs MetaMetrics) affect all consumers of remote feature flags and experiment assignment across extension/mobile.

Overview
Threshold feature flags can now choose which client ID drives deterministic bucketing via optional idType on threshold entries (FeatureFlagIdType: canonical by default, or metametrics). The controller accepts an optional getCanonicalId callback (defaults to empty string) and uses #getSegmentationId plus getThresholdIdType so cache keys and hashing use MetaMetrics or canonical ID as configured; if the chosen ID is missing, threshold arrays are left unprocessed.

BREAKING: Processed threshold flags expose the selected value directly in remoteFeatureFlags instead of a { name, value } object. The chosen group name is stored in new optional state featureFlagThresholdGroups, which is updated and pruned alongside thresholdCache when flags change. @metamask/wallet wires getCanonicalId through remote feature flag initialization.

Reviewed by Cursor Bugbot for commit 18f1728. Bugbot is set up for automated code reviews on this repo. Configure here.

…y and support canonical id segmentation

Threshold flags now expose selected values directly, track group names in
featureFlagThresholdGroups, and can use getCanonicalId when idType is canonical.

Co-authored-by: Cursor <cursoragent@cursor.com>
@asalsys asalsys requested review from a team as code owners June 30, 2026 23:04
@asalsys asalsys temporarily deployed to default-branch June 30, 2026 23:04 — with GitHub Actions Inactive
@asalsys asalsys changed the title feat(remote-feature-flag-controller): return threshold values directly and track threshold groups feat(remote-feature-flag-controller): segment threshold flags by idType with canonical ID default Jul 1, 2026
…nd changelog links

Default threshold segmentation to canonical ID, link changelog entries to
PR #9325, add wallet changelog entry, and fix ESLint violations.

Co-authored-by: Cursor <cursoragent@cursor.com>
@asalsys asalsys enabled auto-merge July 1, 2026 03:11

@NicolasMassart NicolasMassart left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two comments, one blocking and a nit.

const firstThresholdEntry = flagValue.find(isFeatureFlagWithScopeValue);

return firstThresholdEntry?.idType ?? FeatureFlagIdType.Canonical;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (blocking): idType is modeled as a per-entry property, but getThresholdIdType determines the segmentation ID by looking only at the first valid threshold entry. That means a mixed configuration silently uses the first entry's idType for the entire flag, making the behavior dependent on entry ordering.

If mixed idType values are not intended, it would be safer to validate that all threshold entries agree and fail fast (or move idType to a flag-level configuration). Otherwise, please add a test documenting the intended behavior for mixed configurations.

idType?: FeatureFlagIdType;
/**
* Selects the threshold entry output shape. Unrecognized versions fall back
* to the legacy `{ name, value }` wrapper for backwards compatibility.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): This comment still says unrecognized threshold versions fall back to the legacy { name, value } wrapper, but this PR removes that behavior and now always returns the selected value directly. The documentation should be updated to match the implementation.

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