-
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
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 |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"remoteFeatureFlags": { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,6 +56,17 @@ 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. | ||
- Duplicate `manifest-flags.json.dist` within the root and rename it to `manifest-flags.json` by running `cp .manifest-flags.json{.dist,}`. This file is used to add flags to `.manifest.json` build files for the extension. You can add flags to the file to be used in the build process, for example: | ||
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. No action required yet, but when we figure out what to call the file, we have to be consistent everywhere. Sometimes in the code and documents it has a dot in front, and sometimes it doesn't. This is an optional step (not everyone who develops locally has to do this immediately) and that should be specified. |
||
```json | ||
{ | ||
"remoteFeatureFlags": { | ||
"testFlagForThreshold": { | ||
"name": "test-flag", | ||
"value": "test-value" | ||
} | ||
} | ||
} | ||
``` | ||
- 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 |
---|---|---|
|
@@ -18,6 +18,29 @@ const { getEnvironment, getBuildName } = require('./utils'); | |
|
||
module.exports = createManifestTasks; | ||
|
||
async function loadManifestFlags() { | ||
try { | ||
return JSON.parse( | ||
await fs.readFile( | ||
path.join(__dirname, '../../.manifest-flags.json'), | ||
'utf8', | ||
), | ||
); | ||
} catch (error) { | ||
return { remoteFeatureFlags: {} }; | ||
} | ||
} | ||
|
||
// Initialize with default value | ||
let manifestFlags = { remoteFeatureFlags: {} }; | ||
|
||
// Load flags asynchronously | ||
loadManifestFlags().then((flags) => { | ||
manifestFlags = flags; | ||
}); | ||
|
||
module.exports = createManifestTasks; | ||
|
||
Comment on lines
+21
to
+43
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. While I don't think the race condition this introduces will ever show its face because the rest of the build process is slow, it's still adding a race condition, which generally makes me uncomfortable. It also hides parsing errors the dev may have made in their |
||
function createManifestTasks({ | ||
browserPlatforms, | ||
browserVersionMap, | ||
|
@@ -47,8 +70,10 @@ function createManifestTasks({ | |
browserVersionMap[platform], | ||
await getBuildModifications(buildType, platform), | ||
customArrayMerge, | ||
{ | ||
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.
|
||
_flags: manifestFlags, | ||
}, | ||
); | ||
|
||
modifyNameAndDescForNonProd(result); | ||
|
||
const dir = path.join('.', 'dist', platform); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,11 +9,15 @@ | |
* @param args | ||
* @param args.lockdown | ||
* @param args.test | ||
* @param isDevelopment | ||
* @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, | ||
) { | ||
const transforms: ((manifest: chrome.runtime.Manifest) => void)[] = []; | ||
|
||
function removeLockdown(browserManifest: chrome.runtime.Manifest) { | ||
|
@@ -29,6 +33,37 @@ 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 = { remoteFeatureFlags: {} }; | ||
|
||
try { | ||
const fs = require('fs'); | ||
const manifestFlagsContent = fs.readFileSync( | ||
'.manifest-flags.json', | ||
'utf8', | ||
); | ||
manifestFlags = JSON.parse(manifestFlagsContent); | ||
} catch (error: unknown) { | ||
// Only ignore the error if the file doesn't exist | ||
if (error instanceof Error && error.message !== 'ENOENT') { | ||
throw error; | ||
} | ||
} | ||
|
||
browserManifest._flags = manifestFlags; | ||
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. Instead of reading in the Also, hiding 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. Add sync reading + error catching here a5ea54f |
||
} | ||
|
||
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:
|
||
}; | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- you can't extend a type, we want this to be an interface | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,14 @@ import { getCleanAppState, withFixtures } from '../../helpers'; | |
import FixtureBuilder from '../../fixture-builder'; | ||
import { TestSuiteArguments } from '../confirmations/transactions/shared'; | ||
import { loginWithBalanceValidation } from '../../page-objects/flows/login.flow'; | ||
import { MOCK_META_METRICS_ID } from '../../constants'; | ||
import { MOCK_REMOTE_FEATURE_FLAGS_RESPONSE } from './mock-data'; | ||
import HeaderNavbar from '../../page-objects/pages/header-navbar'; | ||
import SettingsPage from '../../page-objects/pages/settings/settings-page'; | ||
import DevelopOptions from '../../page-objects/pages/developer-options-page'; | ||
import { | ||
MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, | ||
MOCK_META_METRICS_ID, | ||
MOCK_REMOTE_FEATURE_FLAGS_RESPONSE, | ||
} from '../../constants'; | ||
|
||
describe('Remote feature flag', function (this: Suite) { | ||
it('should be fetched with threshold value when basic functionality toggle is on', async function () { | ||
|
@@ -45,4 +51,33 @@ describe('Remote feature flag', function (this: Suite) { | |
}, | ||
); | ||
}); | ||
|
||
it('offers the option to pass into manifest file for developers along with original response', async function () { | ||
await withFixtures( | ||
{ | ||
fixtures: new FixtureBuilder() | ||
.withMetaMetricsController({ | ||
metaMetricsId: MOCK_META_METRICS_ID, | ||
participateInMetaMetrics: true, | ||
}) | ||
.build(), | ||
manifestFlags: { | ||
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.
|
||
remoteFeatureFlags: MOCK_CUSTOMIZED_REMOTE_FEATURE_FLAGS, | ||
}, | ||
title: this.test?.fullTitle(), | ||
}, | ||
async ({ driver }: TestSuiteArguments) => { | ||
await loginWithBalanceValidation(driver); | ||
const headerNavbar = new HeaderNavbar(driver); | ||
await headerNavbar.openSettingsPage(); | ||
const settingsPage = new SettingsPage(driver); | ||
await settingsPage.check_pageIsLoaded(); | ||
await settingsPage.goToDevelopOptionSettings(); | ||
|
||
const developOptionsPage = new DevelopOptions(driver); | ||
await developOptionsPage.check_pageIsLoaded(); | ||
await developOptionsPage.validateRemoteFeatureFlagState(); | ||
}, | ||
); | ||
}); | ||
}); |
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.