-
Notifications
You must be signed in to change notification settings - Fork 5k
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
base: main
Are you sure you want to change the base?
Changes from all commits
d4f2f3f
887ed35
4cef487
5e3788b
5c703f2
35fe1f0
473a3bd
caa4b4f
0bff314
d4e79a7
8e88e7f
b1a52ae
b2b5862
a5ea54f
5308bed
2cc1560
248ec01
40f3c07
3a16019
77ad1e5
b09f6a5
9487c17
51b6de6
f3c8e38
2c5ab32
945c42a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -56,6 +56,8 @@ If you are not a MetaMask Internal Developer, or are otherwise developing on a f | |||||
- If debugging MetaMetrics, you'll need to add a value for `SEGMENT_WRITE_KEY` [Segment write key](https://segment.com/docs/connections/find-writekey/), see [Developing on MetaMask - Segment](./development/README.md#segment). | ||||||
- If debugging unhandled exceptions, you'll need to add a value for `SENTRY_DSN` [Sentry Dsn](https://docs.sentry.io/product/sentry-basics/dsn-explainer/), see [Developing on MetaMask - Sentry](./development/README.md#sentry). | ||||||
- Optionally, replace the `PASSWORD` value with your development wallet password to avoid entering it each time you open the app. | ||||||
- If developing with remote feature flags and you want to override the flags in the build process, you can add a `.manifest-overrides.json` file to the root of the project and set `MANIFEST_OVERRIDES=.manifest-overrides.json` in `.metamaskrc` to the path of the file. This file is used to add flags to `manifest.json` build files for the extension. You can also modify the `_flags.remoteFeatureFlags` in `manifest.json` file to tweak the flags after the build process. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying to make the instructions slightly clearer
Suggested change
|
||||||
``` | ||||||
- Run `yarn install` to install the dependencies. | ||||||
- Build the project to the `./dist/` folder with `yarn dist` (for Chromium-based browsers) or `yarn dist:mv2` (for Firefox) | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -160,4 +160,5 @@ async function getConfig(buildType, environment) { | |
|
||
module.exports = { | ||
getConfig, | ||
fromIniFile, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -232,7 +232,7 @@ describe('ManifestPlugin', () => { | |
function runTest(baseManifest: Combination<typeof manifestMatrix>) { | ||
const manifest = baseManifest as unknown as chrome.runtime.Manifest; | ||
const hasTabsPermission = (manifest.permissions || []).includes('tabs'); | ||
const transform = transformManifest(args); | ||
const transform = transformManifest(args, false); | ||
|
||
if (args.test && hasTabsPermission) { | ||
it("throws in test mode when manifest already contains 'tabs' permission", () => { | ||
|
@@ -281,4 +281,67 @@ describe('ManifestPlugin', () => { | |
} | ||
} | ||
}); | ||
|
||
describe('manifest flags in development mode', () => { | ||
const testManifest = {} as chrome.runtime.Manifest; | ||
const mockFlags = { remoteFeatureFlags: { testFlag: true } }; | ||
const manifestOverridesPath = 'testManifestOverridesPath.json'; | ||
|
||
it('adds manifest flags in development mode with path provided', () => { | ||
const transform = transformManifest( | ||
{ lockdown: true, test: false }, | ||
true, | ||
manifestOverridesPath, | ||
); | ||
assert(transform, 'transform should be truthy'); | ||
|
||
// Mock fs.readFileSync | ||
const fs = require('fs'); | ||
const originalReadFileSync = fs.readFileSync; | ||
fs.readFileSync = () => JSON.stringify(mockFlags); | ||
Comment on lines
+298
to
+301
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 // (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()); |
||
|
||
try { | ||
const transformed = transform(testManifest, 'chrome'); | ||
assert.deepStrictEqual( | ||
transformed._flags, | ||
mockFlags, | ||
'manifest should have flags in development mode', | ||
); | ||
} finally { | ||
// Restore original readFileSync | ||
fs.readFileSync = originalReadFileSync; | ||
} | ||
}); | ||
|
||
it('handles missing manifest flags file with path provided', () => { | ||
const transform = transformManifest( | ||
{ lockdown: true, test: false }, | ||
true, | ||
manifestOverridesPath, | ||
); | ||
assert(transform, 'transform should be truthy'); | ||
|
||
// Mock fs.readFileSync to throw ENOENT | ||
const fs = require('node:fs'); | ||
const originalReadFileSync = fs.readFileSync; | ||
fs.readFileSync = () => { | ||
const error = new Error('File not found') as NodeJS.ErrnoException; | ||
error.code = 'ENOENT'; | ||
throw error; | ||
}; | ||
|
||
try { | ||
assert.throws( | ||
() => transform(testManifest, 'chrome'), | ||
{ | ||
message: `Manifest override file not found: ${manifestOverridesPath}`, | ||
}, | ||
'should throw when manifest override file is not found', | ||
); | ||
} finally { | ||
// Restore original readFileSync | ||
fs.readFileSync = originalReadFileSync; | ||
} | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,11 +9,17 @@ | |
* @param args | ||
* @param args.lockdown | ||
* @param args.test | ||
* @param isDevelopment | ||
* @param manifestOverridesPath | ||
* @returns a function that will transform the manifest JSON object | ||
* @throws an error if the manifest already contains the "tabs" permission and | ||
* `test` is `true` | ||
*/ | ||
export function transformManifest(args: { lockdown: boolean; test: boolean }) { | ||
export function transformManifest( | ||
args: { lockdown: boolean; test: boolean }, | ||
isDevelopment: boolean, | ||
manifestOverridesPath?: string, | ||
) { | ||
const transforms: ((manifest: chrome.runtime.Manifest) => void)[] = []; | ||
|
||
function removeLockdown(browserManifest: chrome.runtime.Manifest) { | ||
|
@@ -29,6 +35,48 @@ export function transformManifest(args: { lockdown: boolean; test: boolean }) { | |
transforms.push(removeLockdown); | ||
} | ||
|
||
/** | ||
davidmurdoch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* This function sets predefined flags in the manifest's _flags property | ||
* that are stored in the .manifest-flags.json file. | ||
* | ||
* @param browserManifest - The Chrome extension manifest object to modify | ||
*/ | ||
function addManifestFlags(browserManifest: chrome.runtime.Manifest) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
let manifestFlags; | ||
|
||
if (manifestOverridesPath) { | ||
try { | ||
const fs = require('node:fs'); | ||
const path = require('node:path'); | ||
const manifestFlagsContent = fs.readFileSync( | ||
path.resolve(process.cwd(), manifestOverridesPath), | ||
'utf8', | ||
); | ||
manifestFlags = JSON.parse(manifestFlagsContent); | ||
} catch (error: unknown) { | ||
if ( | ||
error instanceof Error && | ||
'code' in error && | ||
error.code === 'ENOENT' | ||
) { | ||
// Only throw if ENOENT and manifestOverridesPath was provided | ||
throw new Error( | ||
`Manifest override file not found: ${manifestOverridesPath}`, | ||
); | ||
} | ||
} | ||
} | ||
|
||
if (manifestFlags) { | ||
browserManifest._flags = manifestFlags; | ||
} | ||
} | ||
|
||
if (isDevelopment) { | ||
// Add manifest flags only for development builds | ||
transforms.push(addManifestFlags); | ||
} | ||
|
||
function addTabsPermission(browserManifest: chrome.runtime.Manifest) { | ||
if (browserManifest.permissions) { | ||
if (browserManifest.permissions.includes('tabs')) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,6 +60,18 @@ export type ManifestFlags = { | |
*/ | ||
forceEnable?: boolean; | ||
}; | ||
/** | ||
* Feature flags to control business logic behavior | ||
*/ | ||
remoteFeatureFlags?: { | ||
/** | ||
* A test remote featureflag for threshold | ||
*/ | ||
testFlagForThreshold: { | ||
name: string; | ||
value: string; | ||
}; | ||
Comment on lines
+70
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use better types here? maybe something like Or if you really want to go the extra mile, you can define the type as any valid JSON:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this one, the name and value are the same in request, and controller will choose one of the value to return based on threshold => https://client-config.api.cx.metamask.io/v1/flags?client=extension&distribution=main&environment=dev There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, so it's an array of |
||
}; | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- you can't extend a type, we want this to be an interface | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest ignoring
.manifest-overrides*.json
Because remember we want people to be able to create a few of these and toggle between them, plus be able to put them in folders, and gitignore all of these.