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(27254): implement new remote-feature-flag-controller #4931

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

Conversation

DDDDDanica
Copy link

@DDDDDanica DDDDDanica commented Nov 14, 2024

Explanation

Following the ADR here

Adds a new controller, remote-feature-flag-controller that fetches the remote feature flags and provide cache solution for consumers.

References

Related to #27254

Changelog

@metamask/remote-feature-flag-controller

ADDED: Initial release

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 highlighted breaking changes using the "BREAKING" category above as appropriate
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch 6 times, most recently from 78af7f4 to cd93f7a Compare November 15, 2024 03:13
Copy link

socket-security bot commented Nov 15, 2024

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

👍 No dependency changes detected in pull request

@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch from cd93f7a to 82f7997 Compare November 16, 2024 03:42
@DDDDDanica DDDDDanica self-assigned this Nov 16, 2024
@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch 2 times, most recently from 78ca22b to a7a3cf1 Compare November 19, 2024 11:53
@DDDDDanica DDDDDanica marked this pull request as ready for review November 19, 2024 23:04
@DDDDDanica DDDDDanica requested a review from a team as a code owner November 19, 2024 23:04
@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch 9 times, most recently from 0519340 to fcc71a0 Compare November 20, 2024 12:19
@DDDDDanica
Copy link
Author

@metamaskbot publish-preview

Copy link
Contributor

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/accounts-controller": "19.0.0-preview-fcc71a0",
  "@metamask-previews/address-book-controller": "6.0.1-preview-fcc71a0",
  "@metamask-previews/announcement-controller": "7.0.1-preview-fcc71a0",
  "@metamask-previews/approval-controller": "7.1.1-preview-fcc71a0",
  "@metamask-previews/assets-controllers": "44.0.1-preview-fcc71a0",
  "@metamask-previews/base-controller": "7.0.2-preview-fcc71a0",
  "@metamask-previews/build-utils": "3.0.1-preview-fcc71a0",
  "@metamask-previews/chain-controller": "0.1.3-preview-fcc71a0",
  "@metamask-previews/composable-controller": "9.0.1-preview-fcc71a0",
  "@metamask-previews/controller-utils": "11.4.3-preview-fcc71a0",
  "@metamask-previews/ens-controller": "15.0.0-preview-fcc71a0",
  "@metamask-previews/eth-json-rpc-provider": "4.1.6-preview-fcc71a0",
  "@metamask-previews/gas-fee-controller": "22.0.1-preview-fcc71a0",
  "@metamask-previews/json-rpc-engine": "10.0.1-preview-fcc71a0",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.5-preview-fcc71a0",
  "@metamask-previews/keyring-controller": "18.0.0-preview-fcc71a0",
  "@metamask-previews/logging-controller": "6.0.2-preview-fcc71a0",
  "@metamask-previews/message-manager": "11.0.1-preview-fcc71a0",
  "@metamask-previews/multichain": "0.0.0-preview-fcc71a0",
  "@metamask-previews/name-controller": "8.0.1-preview-fcc71a0",
  "@metamask-previews/network-controller": "22.0.2-preview-fcc71a0",
  "@metamask-previews/notification-controller": "7.0.0-preview-fcc71a0",
  "@metamask-previews/notification-services-controller": "0.13.0-preview-fcc71a0",
  "@metamask-previews/permission-controller": "11.0.3-preview-fcc71a0",
  "@metamask-previews/permission-log-controller": "3.0.1-preview-fcc71a0",
  "@metamask-previews/phishing-controller": "12.3.0-preview-fcc71a0",
  "@metamask-previews/polling-controller": "12.0.1-preview-fcc71a0",
  "@metamask-previews/preferences-controller": "14.0.0-preview-fcc71a0",
  "@metamask-previews/profile-sync-controller": "1.0.2-preview-fcc71a0",
  "@metamask-previews/queued-request-controller": "7.0.1-preview-fcc71a0",
  "@metamask-previews/rate-limit-controller": "6.0.1-preview-fcc71a0",
  "@metamask-previews/selected-network-controller": "19.0.0-preview-fcc71a0",
  "@metamask-previews/signature-controller": "22.0.0-preview-fcc71a0",
  "@metamask-previews/transaction-controller": "39.0.0-preview-fcc71a0",
  "@metamask-previews/user-operation-controller": "18.0.0-preview-fcc71a0"
}

@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch 6 times, most recently from 153ebc9 to 0fab160 Compare November 21, 2024 21:20
@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch from f13e7b4 to 6ea8e51 Compare November 27, 2024 17:14
@danjm
Copy link
Contributor

danjm commented Nov 27, 2024

I've coded a proposed alternative to service error handling, can be found here #4995

@DDDDDanica DDDDDanica force-pushed the feature/27254-feature-flag-controller branch from 6ea8e51 to e5e0b5c Compare November 27, 2024 22:41
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.

A few more things, but this is looking pretty good otherwise!

packages/remote-feature-flag-controller/package.json Outdated Show resolved Hide resolved
packages/remote-feature-flag-controller/src/logger.ts Outdated Show resolved Hide resolved
Comment on lines 7 to 10
export const incomingTransactionsLogger = createModuleLogger(
projectLogger,
'remote-feature-flag',
);
Copy link
Contributor

@mcmire mcmire Nov 27, 2024

Choose a reason for hiding this comment

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

Hmm, I see that you copied this pattern from transaction-controller. It seems that they are putting all of their logger objects in this file. I guess that is okay — there isn't clear guidance on this. But should we at least name this variable and the scope appropriately?

Suggested change
export const incomingTransactionsLogger = createModuleLogger(
projectLogger,
'remote-feature-flag',
);
export const remoteFeatureFlagControllerLogger = createModuleLogger(
projectLogger,
'remote-feature-flag-controller',
);

As an alternative to this, you could put this variable at the top of remote-feature-flag-controller.ts itself and name it log, with the scope named after the class/function/file that's being logged. Here is another example to demonstrate this approach:

const log = createModuleLogger(projectLogger, 'etherscan');

And so you could have a similar one for remote-feature-flag-controller like so:

export const log = createModuleLogger(projectLogger, 'RemoteFeatureFlagController');

Copy link
Contributor

Choose a reason for hiding this comment

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

Your second suggestion has been applied here 440d170

// === STATE ===

export type RemoteFeatureFlagControllerState = {
remoteFeatureFlag: FeatureFlags;
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that this was changed recently. Just want to double-check, should this be remoteFeatureFlags instead of remoteFeatureFlag, since there could be multiple?

Suggested change
remoteFeatureFlag: FeatureFlags;
remoteFeatureFlags: FeatureFlags;

Copy link
Contributor

Choose a reason for hiding this comment

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

Done in 8f3c4d5

} catch (error) {
log('Remote feature flag API request failed: %o', error);
reject(error);
throw error;
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that we are returning the promise in the successful case but not the error case. So I see why you added this line. But since we have a promise that we are rejecting, it seems that we ought to be able to use it somehow for all cases...

What if instead of a try/catch we add this when we set up the promise above:

    const { promise, resolve, reject } = createDeferredPromise<FeatureFlags>({
      suppressUnhandledRejection: true,
    });
    this.#inProgressFlagUpdate = promise;
    promise.finally(() => {
      this.#inProgressFlagUpdate = undefined;
    });

And now we can put return await promise as the very last thing in this method.

Copy link
Contributor

Choose a reason for hiding this comment

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

done in 9454a8f


export const controllerName = 'RemoteFeatureFlagController';
export const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 1 day
const log = createModuleLogger(projectLogger, 'ClientConfigApiService');
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I just noticed this. Perhaps we don't need the additional logger in logger.ts? Maybe we just need projectLogger there, and we can correct the scope of this logger:

Suggested change
const log = createModuleLogger(projectLogger, 'ClientConfigApiService');
const log = createModuleLogger(projectLogger, 'RemoteFeatureFlagController');

Copy link
Contributor

Choose a reason for hiding this comment

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

this was addressed in 440d170

*
* @returns A promise that resolves to the current set of feature flags.
*/
async getRemoteFeatureFlag(): Promise<FeatureFlags> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar to a previous comment, did we want to use getRemoteFeatureFlags since we're potentially grabbing multiple? Or is this fine? Just wanted to double-check the API is the way we want it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Pluralized in 8f3c4d5

Copy link
Contributor

@danjm danjm Nov 28, 2024

Choose a reason for hiding this comment

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

And some more pluralization was done here d0b81fa

@@ -0,0 +1,15 @@
# `@metamask/remote-feature-flag-controller`

Controller with caching, fallback, and privacy for managing feature flags via ClientConfigAPI.
Copy link
Member

Choose a reason for hiding this comment

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

This sentence is a little confusing, maybe we can re-word it to be more clear. I'm not sure what it means.

Copy link
Member

Choose a reason for hiding this comment

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

The description in the JSDoc entry for the class seems better: "manages the retrieval and caching of remote feature flags"

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated in d2711d7

{
"name": "@metamask/remote-feature-flag-controller",
"version": "0.0.0",
"description": "Controller with caching, fallback, and privacy for managing feature flags via ClientConfigAPI",
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the README, I don't find this to be a clear description

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated in d2711d7

"type": "git",
"url": "https://github.com/MetaMask/core.git"
},
"license": "MIT",
Copy link
Member

Choose a reason for hiding this comment

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

The preference for new open source packages is to use a dual MIT and Apache 2.0 license. See here for an example: https://github.com/MetaMask/design-tokens/blob/f8c5e466964a4709708bf35c3663bce6fa55c049/package.json#L17

And note the two license files

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated to use both licenses in 2bb0ade

return await this.#inProgressFlagUpdate;
}

const { promise, resolve, reject } = createDeferredPromise<FeatureFlags>({
Copy link
Member

@Gudahtt Gudahtt Nov 28, 2024

Choose a reason for hiding this comment

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

What is the purpose of using a deferred Promise here? We appear to want a promise representing the "fetch feature flags" call, but we already have that (it's returned by clientConfigApiService.fetchRemoteFeatureFlag).

We could do something like this instead:

this.#inProgressFlagUpdate = this.#clientConfigApiService.fetchRemoteFeatureFlags();
const serverData = await this.#inProgressFlagUpdate;
this.#inProgressFlagUpdate = undefined;

No deferred promise needed.

Copy link
Member

@Gudahtt Gudahtt Nov 28, 2024

Choose a reason for hiding this comment

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

Oh sorry, to handle the failure case:

this.#inProgressFlagUpdate = this.#clientConfigApiService.fetchRemoteFeatureFlags();
let serverData;
try {
 serverData = await this.#inProgressFlagUpdate;
} finally {
  this.#inProgressFlagUpdate = undefined;
}

*/
async getRemoteFeatureFlag(): Promise<FeatureFlags> {
if (this.#disabled) {
return [];
Copy link
Member

@Gudahtt Gudahtt Nov 28, 2024

Choose a reason for hiding this comment

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

Should we return the cached flags here instead? That wouldn't make a network request. The enabled/disabled state is meant to prevent network requests.

it seems odd to "hide" the cached values, since they can easily be retrieved via state instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with returning cached data when disabled is true. It was changed to worked that way in d0b81fa

throw new Error('Failed to fetch remote feature flags');
}

const data = await response.json();
Copy link
Member

Choose a reason for hiding this comment

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

We should validate that it's an array. We seem to assume that without verification.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done in a7a703f


/** Type representing the feature flags collection */
export type FeatureFlag = {
[key: string]: Json;
Copy link
Member

Choose a reason for hiding this comment

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

How do you envision this would be used? i.e. if I wanted to look for a specific feature flag, how would I find it? Is there a name property I'd search for, and if so, why not specify it here so it can be more easily discovered?

Copy link
Contributor

Choose a reason for hiding this comment

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

It would probably be best if feature flags was an object keyed by feature flag names

Copy link
Contributor

Choose a reason for hiding this comment

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

but I see that it is an array everywhere, including the ADR, so perhaps best to leave that as is for now

Copy link
Contributor

Choose a reason for hiding this comment

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

Proposed approach to add a name property to each feature flag in state is here 8a8be11

@danjm danjm force-pushed the feature/27254-feature-flag-controller branch from 889d359 to 6c4af68 Compare November 28, 2024 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Needs more work from the author
Development

Successfully merging this pull request may close these issues.

5 participants