diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index b6984cfce3e..6333ccffef8 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Sort threshold-scoped variations by ascending scope value before selecting a group, so unordered threshold arrays are evaluated correctly. ([#8128](https://github.com/MetaMask/core/pull/8128)) + ## [4.1.0] ### Added diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts index 2ea779a4ae9..1a47c5b58b9 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts @@ -422,6 +422,44 @@ describe('RemoteFeatureFlagController', () => { }); }); + it('selects the lowest matching threshold when threshold scopes are unordered', async () => { + const mockFlags = { + unorderedThresholdFlag: [ + { + name: 'control', + scope: { type: 'threshold', value: 1.0 }, + value: 'control', + }, + { + name: 'treatment', + scope: { type: 'threshold', value: 0.1 }, + value: 'treatment', + }, + ], + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: mockFlags, + }); + const controller = createController({ + clientConfigApiService, + getMetaMetricsId: () => MOCK_METRICS_ID, + state: { + thresholdCache: { + [`${MOCK_METRICS_ID}:unorderedThresholdFlag`]: 0.05, + }, + }, + }); + + await controller.updateRemoteFeatureFlags(); + + expect( + controller.state.remoteFeatureFlags.unorderedThresholdFlag, + ).toStrictEqual({ + name: 'treatment', + value: 'treatment', + }); + }); + it('preserves non-threshold feature flags unchanged', async () => { const clientConfigApiService = buildClientConfigApiService({ remoteFeatureFlags: MOCK_FLAGS_WITH_THRESHOLD, diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts index f05d3147ab1..936b250f96e 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts @@ -11,7 +11,6 @@ import type { AbstractClientConfigApiService } from './client-config-api-service import type { FeatureFlags, ServiceResponse, - FeatureFlagScopeValue, } from './remote-feature-flag-controller-types'; import { calculateThresholdForFlag, @@ -372,15 +371,13 @@ export class RemoteFeatureFlagController extends BaseController< thresholdCacheUpdates[cacheKey] = thresholdValue; } + const thresholdScopedFlags = processedValue + .filter(isFeatureFlagWithScopeValue) + .sort((a, b) => a.scope.value - b.scope.value); + const threshold = thresholdValue; - const selectedGroup = processedValue.find( - (featureFlag): featureFlag is FeatureFlagScopeValue => { - if (!isFeatureFlagWithScopeValue(featureFlag)) { - return false; - } - - return threshold <= featureFlag.scope.value; - }, + const selectedGroup = thresholdScopedFlags.find( + (featureFlag) => threshold <= featureFlag.scope.value, ); if (selectedGroup) { processedValue = {