Skip to content

Conversation

@asalsys
Copy link

@asalsys asalsys commented Nov 30, 2025

Remote Feature Flag Controller - Override Support & A/B Test Visibility

Explanation

Current State & Problem

The RemoteFeatureFlagController currently only supports remote feature flags with no ability to locally override them. This creates several pain points for developers and QA teams:

  • No local testing capability: Developers cannot test different flag configurations without changing remote settings
  • A/B test visibility gap: Once A/B test arrays are processed into single values, the raw options are lost, making it impossible to see what alternatives were available
  • Limited debugging: No way to understand current flag assignments vs. available options
  • QA testing constraints: Testing teams cannot systematically test different flag scenarios locally

Solution Overview

This PR adds local override functionality with A/B test visibility through direct state access:

Core Features:

  • Local Overrides: New localOverrides state field allows manual flag overrides that take precedence over remote flags
  • A/B Test Visibility: New rawRemoteFeatureFlags field stores raw A/B test arrays (preserves original flag data before processing)
  • Override Management: Complete CRUD operations for managing overrides (setFlagOverride, clearFlagOverride, clearAllOverrides)
  • State-Based Access: Flag values and A/B test data accessible through controller state properties

How It Works:

  1. Processing Enhancement: The #updateCache method now stores raw flag data in rawRemoteFeatureFlags before processing A/B tests into single values
  2. Priority System: Consumers can check state.localOverrides[flagName] ?? state.remoteFeatureFlags[flagName] for override-aware access
  3. Raw Storage: Raw flag data is stored for all flags, allowing visibility into original A/B test arrays
  4. Persistence: All override data persists across sessions and remote flag updates via the #updateCache method

Implementation Details

The implementation in metamask-mobile can be found in this PR

  • State Management: Three state fields work together:

    • remoteFeatureFlags: Processed flags (A/B tests resolved to single values)
    • localOverrides: Manual overrides that take precedence
    • rawRemoteFeatureFlags: Original flag data before processing (preserves A/B test arrays)
  • Override Persistence: Remote flag updates preserve existing local overrides through state preservation in #updateCache

  • Messenger Integration: All override methods are exposed as controller actions for external access

  • Metadata Configuration: All new state fields are properly configured for logging, persistence, and UI exposure

Access Patterns

Getting Flag Values (with override support):
Getting the effective Value can be retrieved from the remote feature flag selector by processing the overrides on it as such

    const localOverrides = state?.localOverrides ?? {};
    const remoteFeatureFlags = state?.remoteFeatureFlags ?? {};
    return {
      ...remoteFeatureFlags,
      ...localOverrides,
    };

Accessing A/B Test Groups:
View available A/B test options can be done through the rawRemoteFeatureFlags selector

Managing Overrides:

// Set override
controller.setFlagOverride('flagName', 'testValue');

// Clear specific override
controller.clearFlagOverride('flagName');

// Clear all overrides
controller.clearAllOverrides();

Dependencies & Imports

  • Added Json type import from @metamask/utils for type safety
  • No new external dependencies required

References

This enhancement addresses the need for local flag testing and A/B test visibility that has been requested by development and QA teams for improved testing workflows.

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note: No breaking changes were introduced - this is purely additive functionality that maintains full backward compatibility.


Note

Adds local override support and raw flag storage to the RemoteFeatureFlagController, updates state/metadata and exports, and updates tests/consumers accordingly.

  • Remote Feature Flag Controller:
    • State/Metadata: Add localOverrides and rawRemoteFeatureFlags; update metadata and default state; include in logs/persistence and expose relevant fields to UI.
    • API: Add setFlagOverride(flagName, value), removeFlagOverride(flagName), clearAllFlagOverrides().
    • Caching: #updateCache now preserves localOverrides and stores rawRemoteFeatureFlags alongside processed flags.
    • Exports/Types: Export controllerName; extend actions with RemoteFeatureFlagControllerSetFlagOverrideAction, RemoveFlagOverrideAction, ClearAllFlagOverridesAction and re-export them in src/index.ts.
  • Tests/Consumers:
    • Update controller tests and add comprehensive override tests; adjust transaction and transaction-pay controller tests/mocks to include new state fields.
  • Docs:
    • Update CHANGELOG.md with new features and metadata changes.

Written by Cursor Bugbot for commit fb818a8. This will update automatically on new commits. Configure here.

@asalsys asalsys requested review from a team as code owners November 30, 2025 21:53
@asalsys asalsys requested a review from a team as a code owner December 2, 2025 18:46
@asalsys asalsys force-pushed the feat/override-functionality-to-remote-feature-flags branch from f131cc8 to 11c0f59 Compare December 5, 2025 00:38
@asalsys asalsys force-pushed the feat/override-functionality-to-remote-feature-flags branch from 11c0f59 to 7ae47a3 Compare December 8, 2025 21:48
@github-project-automation github-project-automation bot moved this to Needs dev review in PR review queue Dec 10, 2025
Copy link
Contributor

@Cal-L Cal-L left a comment

Choose a reason for hiding this comment

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

Left a comment

* @param value - The override value for the feature flag.
*/
setFlagOverride(flagName: string, value: Json): void {
this.update(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can spread ...this.state for unaffected states

this.update(() => {
return {
remoteFeatureFlags: this.state.remoteFeatureFlags,
localOverrides: newOverrides,
Copy link
Contributor

Choose a reason for hiding this comment

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

spread ..this.state for unaffected states

this.update(() => {
return {
remoteFeatureFlags: this.state.remoteFeatureFlags,
localOverrides: {},
Copy link
Contributor

Choose a reason for hiding this comment

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

Spread state for unaffected states

@asalsys asalsys requested a review from Cal-L December 11, 2025 21:32
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

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

Hello! I had some comments on this PR. Most are minor.

I think the main thing I am trying to understand is what these overrides actually do. I started reviewing thinking that they affected how this controller returns feature flags — that perhaps the local overrides were mixed into the flags returned by the API. But that is not what seems to be happening. If we are expecting the consumer to really say state.localOverrides[flagName] ?? state.remoteFeatureFlags[flagName] when they want to access a feature flag, that seems inconvenient to me. Let me know your thoughts.


// Access flag value with override taking precedence
const flagValue =
controller.state.localOverrides.testFlagForThreshold ??
Copy link
Contributor

@mcmire mcmire Dec 11, 2025

Choose a reason for hiding this comment

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

Why are we using ?? here? Do we not expect controller.state.localOverrides.testFlagForThreshold to be set, and if not, why?

Copy link
Author

Choose a reason for hiding this comment

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

removed test

});

describe('integration with remote flags', () => {
it('preserves overrides when remote flags are updated', async () => {
Copy link
Contributor

@mcmire mcmire Dec 11, 2025

Choose a reason for hiding this comment

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

There are a large number of steps represented here for what I think ought to be a simple test (given my reading of the logic). Similar to other suggestions I've made, is it enough to set up the initial state instead of having to call a few methods?

Copy link
Author

Choose a reason for hiding this comment

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

updated


await controller.updateRemoteFeatureFlags();

// Mock different remote response for second fetch
Copy link
Contributor

Choose a reason for hiding this comment

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

This mentions that we are mocking the request for the second time, but where are we mocking the request for the first time?

Copy link
Author

Choose a reason for hiding this comment

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

The first fetch would have been created the initial remote value. I will update the comment to be more clear

});
});

it('overrides work with threshold-based feature flags', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this test testing? If it's merely testing that overrides do not change when updateRemoteFeatureFlags is called, isn't that what the previous test is for?

Copy link
Author

Choose a reason for hiding this comment

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

I had changed the logic for threshold based feature flag. so this test is not necessary anymore. removed!

Copy link
Contributor

@vinnyhoward vinnyhoward left a comment

Choose a reason for hiding this comment

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

LGTM

@asalsys asalsys requested a review from mcmire December 12, 2025 22:10
Copy link
Contributor

@Cal-L Cal-L left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs dev review

Development

Successfully merging this pull request may close these issues.

5 participants