Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(27255): allow local modification for remote feature flags #29696

Open
wants to merge 30 commits into
base: main
Choose a base branch
from

Conversation

DDDDDanica
Copy link
Contributor

@DDDDDanica DDDDDanica commented Jan 14, 2025

Description

Note

You can now follow the steps here to review the PR.

Local Feature Flag Override System

This PR implements a system that allows developers to override remote feature flags locally through manifest.json, providing more flexibility in development and testing environments.

Key Features

  1. Local Feature Flag Override

    • Developers can override remoteFeatureFlag values by defining them in manifest.json
    • These overrides take precedence over values received from the controller
    • Accessible in all development environments:
      • Local builds
      • Webpack builds
      • E2E tests
  2. Testing Integration

    • Leverages the manifest system introduced in PR #26588
    • Allows custom remoteFeatureFlag objects to be passed within test withFixtures
    • Simplifies feature flag testing scenarios
  3. Developer Validation

    • Override values can be verified through the developer options panel
    • Provides immediate feedback on applied feature flag settings

This enhancement streamlines the development workflow by providing local control over feature flags without requiring changes to the controller or deployment configurations.

Usage Example

A. Local build

  1. Define overrides in remoteFeatureFlags in manifest.json:
  "remoteFeatureFlags": {
    "testFlagForThreshold": {
      "name": "test-flag",
      "value": "121212"
    }
  }
  1. Verify in Developer Options:
    • Open extension
    • Click on the account menu (top-right corner)
    • Select "Settings" > "Developer Options"
    • Look for "Remote Feature Flags" section to verify your overrides
Screenshot 2025-01-14 at 00 48 22

B. e2e test

Add the customized value in

fixtures: new FixtureBuilder()
          .withMetaMetricsController({
            metaMetricsId: MOCK_META_METRICS_ID,
            participateInMetaMetrics: true,
          })
          .build(),

Open in GitHub Codespaces

Related issues

Fixes: #27255

Manual testing steps

Please check <### Usage Example> above to check how to test manually.

Screenshots/Recordings

Before

After

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Copy link
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-extension-platform Extension Platform team label Jan 14, 2025
@DDDDDanica
Copy link
Contributor Author

@metamaskbot update-policies

Copy link

socket-security bot commented Jan 14, 2025

No dependency changes detected. Learn more about Socket for GitHub ↗︎

👍 No dependency changes detected in pull request

@metamaskbot
Copy link
Collaborator

Policy update failed. You can review the logs or retry the policy update here

@DDDDDanica DDDDDanica force-pushed the feature/remote-feature-flags-manifest-adaption branch 2 times, most recently from 1e9217f to 1905574 Compare January 14, 2025 14:48
@DDDDDanica DDDDDanica self-assigned this Jan 14, 2025
@DDDDDanica
Copy link
Contributor Author

@metamaskbot update-policies

@metamaskbot
Copy link
Collaborator

Policy update failed. You can review the logs or retry the policy update here

@DDDDDanica DDDDDanica force-pushed the feature/remote-feature-flags-manifest-adaption branch from 1905574 to ced7bf4 Compare January 14, 2025 15:01
@DDDDDanica
Copy link
Contributor Author

@metamaskbot update-policies

@metamaskbot
Copy link
Collaborator

Policies updated.
👀 Please review the diff for suspicious new powers.

🧠 Learn how: https://lavamoat.github.io/guides/policy-diff/#what-to-look-for-when-reviewing-a-policy-diff

@DDDDDanica DDDDDanica force-pushed the feature/remote-feature-flags-manifest-adaption branch 5 times, most recently from 704d4ab to 511c2d5 Compare January 15, 2025 20:53
@DDDDDanica
Copy link
Contributor Author

@metamaskbot update-policies

@metamaskbot
Copy link
Collaborator

Policies updated.
👀 Please review the diff for suspicious new powers.

🧠 Learn how: https://lavamoat.github.io/guides/policy-diff/#what-to-look-for-when-reviewing-a-policy-diff

@DDDDDanica DDDDDanica force-pushed the feature/remote-feature-flags-manifest-adaption branch from 6ccfac6 to 804e3d7 Compare January 15, 2025 21:18
@DDDDDanica
Copy link
Contributor Author

@metamaskbot update-policies

@metamaskbot
Copy link
Collaborator

Builds ready [804e3d7]
Page Load Metrics (1723 ± 60 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint45620351587392188
domContentLoaded15032022169712661
load15242036172312560
domInteractive24187493617
backgroundConnect85926168
firstReactRender1698362612
getState575252411
initialActions01000
loadScripts11321539127810952
setupStore511711
uiStartup17742603202619794
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 260 Bytes (0.00%)
  • ui: 869 Bytes (0.01%)
  • common: 233 Bytes (0.00%)

@metamaskbot
Copy link
Collaborator

Policies updated.
👀 Please review the diff for suspicious new powers.

🧠 Learn how: https://lavamoat.github.io/guides/policy-diff/#what-to-look-for-when-reviewing-a-policy-diff

@metamaskbot
Copy link
Collaborator

Builds ready [16c667e]
Page Load Metrics (1860 ± 60 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint43020891774334160
domContentLoaded16822062183212661
load17052089186012660
domInteractive28663795
backgroundConnect77124178
firstReactRender1783472412
getState773182010
initialActions01000
loadScripts12441585138110651
setupStore662252311
uiStartup19702466213614067
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 260 Bytes (0.00%)
  • ui: 869 Bytes (0.01%)
  • common: 233 Bytes (0.00%)

@DDDDDanica
Copy link
Contributor Author

Walk through the PR changes from here -

/**
* Feature flags to control business logic behavior
*/
remoteFeatureFlags?: {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Include the type of remoteFeatureFlags in manifest as optional value. We include testFlagForThreshold that's created for testing purpose in remote feature flag API .

@@ -11,6 +11,7 @@ const baradDurManifest = isManifestV3
? require('../../app/manifest/v3/_barad_dur.json')
: require('../../app/manifest/v2/_barad_dur.json');
const { loadBuildTypesConfig } = require('../lib/build-type');
const manifestFlags = require('../../manifest-flags.json');
Copy link
Contributor Author

@DDDDDanica DDDDDanica Jan 16, 2025

Choose a reason for hiding this comment

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

  1. Read customized manifestFlags from manifest-flags.json so we can override the one from controller (API) - old build system

@@ -1,3 +1,5 @@
import manifestFlags from '../../../../../manifest-flags.json';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Read customized manifestFlags from manifest-flags.json so we can override the one from controller (API) - webpack build

@@ -47,8 +48,10 @@ function createManifestTasks({
browserVersionMap[platform],
await getBuildModifications(buildType, platform),
customArrayMerge,
{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Add customized local manifestFlags to manifest file (old build system)

@metamaskbot
Copy link
Collaborator

Builds ready [77ad1e5]
Page Load Metrics (2081 ± 87 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint36725081912545262
domContentLoaded17642498206018287
load17952505208118187
domInteractive29112572512
backgroundConnect66123189
firstReactRender2081512512
getState781222110
initialActions01000
loadScripts13331948156515574
setupStore65718178
uiStartup20912805238117785
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -6.7 KiB (-0.11%)
  • ui: 1.51 KiB (0.02%)
  • common: 19.73 KiB (0.23%)

@metamaskbot
Copy link
Collaborator

Builds ready [b09f6a5]
Page Load Metrics (1884 ± 76 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint29321691786372179
domContentLoaded16012144185015273
load16092169188415976
domInteractive278148178
backgroundConnect763272010
firstReactRender16110503215
getState577212311
initialActions01000
loadScripts11861592137212258
setupStore65718189
uiStartup182630722227295142
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -6.7 KiB (-0.11%)
  • ui: 1.51 KiB (0.02%)
  • common: 19.73 KiB (0.23%)

Comment on lines +296 to +299
// Mock fs.readFileSync
const fs = require('fs');
const originalReadFileSync = fs.readFileSync;
fs.readFileSync = () => JSON.stringify(mockFlags);
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT:

Node's testing library has mocking built it. I think the right way to do this is to create a before block within this new 'manifest flags in development mode' describe block:

    // (note: this is untested)
    const originalReadFileSync = fs.readFileSync;
    before("mock `.manifest-flags.json`", () => {
      mock.method(fs, 'readFileSync', (path: string, options: object) => {
        // only override reads for `.manifest-flags.json`:
        if (path === resolve(__dirname, <the path to the file here>)) {
          // mock `.manifest-flags.json`
          return JSON.stringify(mockFlags);
        }
        return originalReadFileSync(path, options);
      });
    });
    
    after(() => mock.restoreAll());

Copy link
Contributor

Choose a reason for hiding this comment

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

bump

}
});

it('handles missing manifest flags file', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

love that you are testing the error conditions! can you also add a test for the case where the fs.readFileSync error is NOT ENOENT?

Copy link
Contributor

Choose a reason for hiding this comment

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

bump

@metamaskbot
Copy link
Collaborator

Builds ready [51b6de6]
Page Load Metrics (1598 ± 72 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint41319151534295142
domContentLoaded14091881156714268
load14171922159815072
domInteractive2194382110
backgroundConnect9172333718
firstReactRender1586442613
getState44612105
initialActions00000
loadScripts10011416115011857
setupStore765182010
uiStartup15742374182520096
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -6.64 KiB (-0.11%)
  • ui: 854 Bytes (0.01%)
  • common: 19.67 KiB (0.22%)

README.md Outdated Show resolved Hide resolved
.gitignore Outdated Show resolved Hide resolved
@metamaskbot
Copy link
Collaborator

Builds ready [3a53d83]
Page Load Metrics (1628 ± 115 ms)
PlatformPageMetricMin (ms)Max (ms)Average (ms)StandardDeviation (ms)MarginOfError (ms)
ChromeHomefirstPaint143323621626224108
domContentLoaded142522711598210101
load143324441628240115
domInteractive2396382210
backgroundConnect1098342512
firstReactRender1598342613
getState597212512
initialActions01000
loadScripts10041757116017986
setupStore692182211
uiStartup161831901868348167
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: -6.64 KiB (-0.11%)
  • ui: 854 Bytes (0.01%)
  • common: 19.7 KiB (0.21%)

Comment on lines +296 to +299
// Mock fs.readFileSync
const fs = require('fs');
const originalReadFileSync = fs.readFileSync;
fs.readFileSync = () => JSON.stringify(mockFlags);
Copy link
Contributor

Choose a reason for hiding this comment

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

bump

}
});

it('handles missing manifest flags file', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

bump

const manifestFlags = getManifestFlags().remoteFeatureFlags;
const stateFlags = state.metamask.remoteFeatureFlags;

return merge({}, stateFlags, manifestFlags);
Copy link
Contributor

Choose a reason for hiding this comment

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

For some reason merge makes the properties types of manifestFlags required, even though they are not.

FeatureFlags & {
    testFlagForThreshold: {
        name: string;
        value: string;
    };
}

So I think the return type of this function is incorrect, as I think testFlagForThreshold should be optional? Does that sound right?

@@ -160,7 +191,7 @@ function createManifestTasks({

// helper for reading and deserializing json from fs
async function readJson(file) {
return JSON.parse(await fs.readFile(file, 'utf8'));
return JSON.parse(await readFileSync(file, 'utf8'));
Copy link
Contributor

Choose a reason for hiding this comment

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

this function doesn't need to be async if we are reading the file synchronously. ORRRRR we don't need to read it synchronously if the function can just be async.

Anyway you want to do it is fine. I just need to be consistent (I don't know why our lint rules didn't catch it)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think though your suggestion of await readFileSync is incorrect though 🙂

Comment on lines +24 to +26
const { definitions } = await fromIniFile(
path.resolve(__dirname, '..', '..', '.metamaskrc'),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Since our build reads from both .metamaskrc and a .metamaskprodrc should we do that here, too?

Comment on lines +70 to +73
testFlagForThreshold: {
name: string;
value: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

bump

Comment on lines +225 to +233
<div className="settings-page__content-description">
The remote feature flags here by <b>getRemoteFeatureFlags()</b> is
retrieved from one of the following sources:
<br />
1) manifest-flags.json file 2) RemoteFeatureFlagsController
<br />
Modify the manifest-flags.json file will change the state locally.
</div>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Its nice to be able to see this in the UI like this, but I think the section is in need of some UI polish, as well as some improvement on the copy itself. Maybe we could get someone from a design team in here to make some improvments?

It also doesn't seem to be something that should be at the top of this settings page :-/

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we have to block on this. I think we can merge and then improve in a later PR.

Copy link
Contributor

@HowardBraham HowardBraham left a comment

Choose a reason for hiding this comment

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

There's a major problem. If you put in .manifest-overrides.json the following:

{
  "_flags": {
    "remoteFeatureFlags": { "testBooleanFlag": false }
  }
}

Then the built manifest.json has:

  "_flags": {
    "_flags": {
      "remoteFeatureFlags": {
        "testBooleanFlag": false
      }
    }
}

But you don't want to just remove the _flags level in .manifest-overrides.json, because you want any manifest property to be changeable, with a config like this:

{
  "_flags": {
    "remoteFeatureFlags": { "testBooleanFlag": false }
  },
  "version": "1.1.1",
  "version_name": "1.1.1"
}

So you need to make these two changes, one for the webpack build and one for the browserify build. I have tested these changes in many different configurations myself.

@@ -47,8 +77,9 @@ function createManifestTasks({
browserVersionMap[platform],
await getBuildModifications(buildType, platform),
customArrayMerge,
// Only include _flags if manifestFlags has content
manifestFlags && { _flags: manifestFlags },
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
manifestFlags && { _flags: manifestFlags },
manifestFlags,

}

if (manifestFlags) {
browserManifest._flags = manifestFlags;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
browserManifest._flags = manifestFlags;
browserManifest = merge(browserManifest, manifestFlags);

Also needs at the top of this file:
import { merge } from 'lodash';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team-extension-platform Extension Platform team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Modify each remote feature flags locally for testing purpose
7 participants