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(3742): Feature Flag Values with Scope Based on threshold #5051

Merged
merged 5 commits into from
Dec 12, 2024

Conversation

DDDDDanica
Copy link
Contributor

@DDDDDanica DDDDDanica commented Dec 10, 2024

Explanation

This PR added ability for @metamask/remote-feature-flag-controller to take in a new param metametricsId and applied to the featureFlag processing. Furthermore, a threshold will be generated using a simple function, which generates a deterministic random group number between 0 and 1 from metametricsId. If encounters a flag from api with scope as type: "threshold", controller will drill down to return the value that's matching the threshold generated based on metametricsId.
Note: metametricsId will always be provided from client, a random SHA-256 hash will be used as fallback

References

Fixes https://github.com/MetaMask/MetaMask-planning/issues/3742

Changelog

@metamask/remote-feature-flag-controller

  • ADDED: Added support for threshold-based feature flag scoping

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 self-assigned this Dec 10, 2024
@DDDDDanica DDDDDanica force-pushed the feature/3742-threshold branch 5 times, most recently from f9326fa to 19d6bb3 Compare December 10, 2024 23:29
@DDDDDanica DDDDDanica marked this pull request as ready for review December 10, 2024 23:29
@DDDDDanica DDDDDanica force-pushed the feature/3742-threshold branch 6 times, most recently from e025bda to 96b3edc Compare December 11, 2024 00:58
Copy link
Contributor

@NicolasMassart NicolasMassart left a comment

Choose a reason for hiding this comment

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

The only thing to change is Metametrics is actually a UUIDv4 and hot a hex hash

@DDDDDanica DDDDDanica force-pushed the feature/3742-threshold branch from 1803083 to 209a912 Compare December 11, 2024 19:47
@DDDDDanica DDDDDanica force-pushed the feature/3742-threshold branch from 209a912 to f2bc3bf Compare December 11, 2024 21:08
Copy link
Contributor

@NicolasMassart NicolasMassart left a comment

Choose a reason for hiding this comment

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

The format we use must be UUIDv4.
Easy to spot, the UUIDv4 must have it 13th digit to be a 4:
6D796265-7374-(4)953-6D65-74616D61736B

Comment on lines 10 to 13
'123e4567-e89b-12d3-a456-426614174000',
'987fcdeb-51a2-3c4b-9876-543210fedcba',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'f9e8d7c6-b5a4-3210-9876-543210fedcba',
Copy link
Contributor

Choose a reason for hiding this comment

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

@@ -41,7 +41,7 @@ const MOCK_FLAGS_WITH_THRESHOLD = {
],
};

const MOCK_METRICS_ID = '0x1234567890abcdef';
const MOCK_METRICS_ID = 'f9e8d7c6-b5a4-3210-9876-543210fedcba';
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a valid UUIDv4, this is UUIDv3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch ! addressed 93d8351

Copy link

@joaoloureirop joaoloureirop left a comment

Choose a reason for hiding this comment

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

Hi Danica.

Can we add a Promise value that will return the metametrics id?

Mobile controller initialization architecture makes it difficult to await for such value, it would help a lot having the option to pass a promise inside the controller.

I've tested with these changes.

diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts
index 37c8d64c..7ded1f5a 100644
--- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts
+++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts
@@ -105,6 +105,8 @@ export class RemoteFeatureFlagController extends BaseController<
 
   #metaMetricsId?: string | undefined;
 
+  #getMetaMetricsId?: Promise<string | undefined>;
+
   /**
    * Constructs a new RemoteFeatureFlagController instance.
    *
@@ -115,6 +117,7 @@ export class RemoteFeatureFlagController extends BaseController<
    * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day.
    * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false.
    * @param options.metaMetricsId - Determines the threshold value for the feature flag to return
+   * @param options.getMetaMetricsId - Promise that will return MetaMetricsId
    */
   constructor({
     messenger,
@@ -123,11 +126,13 @@ export class RemoteFeatureFlagController extends BaseController<
     fetchInterval = DEFAULT_CACHE_DURATION,
     disabled = false,
     metaMetricsId,
+    getMetaMetricsId,
   }: {
     messenger: RemoteFeatureFlagControllerMessenger;
     state?: Partial<RemoteFeatureFlagControllerState>;
     clientConfigApiService: AbstractClientConfigApiService;
     metaMetricsId?: string | undefined;
+    getMetaMetricsId?: Promise<string | undefined>,
     fetchInterval?: number;
     disabled?: boolean;
   }) {
@@ -145,6 +150,7 @@ export class RemoteFeatureFlagController extends BaseController<
     this.#disabled = disabled;
     this.#clientConfigApiService = clientConfigApiService;
     this.#metaMetricsId = metaMetricsId;
+    this.#getMetaMetricsId = getMetaMetricsId;
   }
 
   /**
@@ -193,9 +199,9 @@ export class RemoteFeatureFlagController extends BaseController<
    * @param remoteFeatureFlags - The new feature flags to cache.
    * @private
    */
-  #updateCache(remoteFeatureFlags: FeatureFlags) {
+  async #updateCache(remoteFeatureFlags: FeatureFlags) {
     const processedRemoteFeatureFlags =
-      this.#processRemoteFeatureFlags(remoteFeatureFlags);
+      await this.#processRemoteFeatureFlags(remoteFeatureFlags);
     this.update(() => {
       return {
         remoteFeatureFlags: processedRemoteFeatureFlags,
@@ -204,10 +210,12 @@ export class RemoteFeatureFlagController extends BaseController<
     });
   }
 
-  #processRemoteFeatureFlags(remoteFeatureFlags: FeatureFlags): FeatureFlags {
+  async #processRemoteFeatureFlags(remoteFeatureFlags: FeatureFlags): Promise<FeatureFlags> {
     const processedRemoteFeatureFlags: FeatureFlags = {};
+
+    const metaMetricsId = this.#metaMetricsId ?? await this.#getMetaMetricsId;
     const thresholdValue = generateDeterministicRandomNumber(
-      this.#metaMetricsId || generateFallbackMetaMetricsId(),
+      metaMetricsId || generateFallbackMetaMetricsId(),
     );
 
     for (const [

Moblie PR with the promise value changes MetaMask/metamask-mobile@8769485


/* eslint-disable no-bitwise */
/**
* Generates a deterministic random number between 0 and 1 based on a profile ID.
Copy link
Contributor

Choose a reason for hiding this comment

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

based on metaMetricsId

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🕵🏻‍♂️ good catch ! addressed 63a7274

NicolasMassart
NicolasMassart previously approved these changes Dec 12, 2024
Copy link
Contributor

@NicolasMassart NicolasMassart left a comment

Choose a reason for hiding this comment

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

Looks good to me unless we really want ot use the promise param instead of metametricsId value directly.
Leaves this decision between you and @joaoloureirop.
Nice job!

@DDDDDanica
Copy link
Contributor Author

@joaoloureirop changed to a promise in 1f94695

Copy link

@joaoloureirop joaoloureirop left a comment

Choose a reason for hiding this comment

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

Looks good!

Copy link
Contributor

@NicolasMassart NicolasMassart left a comment

Choose a reason for hiding this comment

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

Looks good to me.

@DDDDDanica DDDDDanica merged commit 742e7d8 into main Dec 12, 2024
120 checks passed
@DDDDDanica DDDDDanica deleted the feature/3742-threshold branch December 12, 2024 16:16
* Generates UUIDv4 as a fallback metaMetricsId
* @returns A UUIDv4 string
*/
export function generateFallbackMetaMetricsId(): string {
Copy link
Member

Choose a reason for hiding this comment

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

This is non-deterministic, and therefore unsuitable for determining whether a feature flag should be enabled or not

Copy link
Member

Choose a reason for hiding this comment

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

I guess the best way of handling this would be to accept a callback for generating the metametrics ID? Ideally we'd use a messenger action, but that would be tricky because of the separate metrics implementations, so a callback should work instead.

Copy link
Member

Choose a reason for hiding this comment

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

Or as you've just pointed out in the extension PR @DDDDDanica , we can rely on getMetaMetricsId to always return a value (i.e. to generate if it's missing). But we'll still want to remove this fallback if we're expecting the callback to always return a value, and we'll want to update the type of the callback

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed in slack thread, we will remove the callback and add this logic to client side if undefined 🙏🏻

return ((acc << 5) - acc + chr) | 0;
}, 0);

return (hash >>> 0) / 0xffffffff;
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 be careful here about the expected size of the metaMetricsId. If the number we're dividing by here isn't the max size of the ID, we'll get an uneven distribution.

i.e. if our target threshold is 10% and 0xffffffff is double the max hash, then this will result in only 5% of users getting the feature. But if 0xffffffff is 1/10 of the max hash, it would result in ~99% of users having the feature enabled.

Correct distribution here is dependent upon each of the constants we're using here being exactly correct. e.g. the 5 bit shift above, and the 0xffffffff here. Ideally we'd assert in code that the max here is truly the max ID (using the length of the ID)

Copy link
Member

Choose a reason for hiding this comment

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

What's especially tricky is that the exact length/contents of the ID might differ between MetaMask versions or clients. Maybe not, but I haven't verified that. That could confuse things even more.

Copy link
Member

@Gudahtt Gudahtt Dec 13, 2024

Choose a reason for hiding this comment

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

Ideas for validating the distribution:

  • Assertion on metametrics length, and derive constants from expected length
  • Test cases for minimum and maximum ID (should get values 0 and 1 respectively )
  • Validate that all production metametrics IDs in both clients are of the expected length

Copy link
Contributor Author

@DDDDDanica DDDDDanica Dec 13, 2024

Choose a reason for hiding this comment

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

What's especially tricky is that the exact length/contents of the ID might differ between MetaMask versions or clients. Maybe not, but I haven't verified that. That could confuse things even more.

I had a discussion with @NicolasMassart on this topic, and we decided to go for uuidV4 on this one.

Currently for mobile side, metaMetricsId is uuidv4 format so we expect 36-character string, while in extension side is a hash hex with 66-characters string but it seems to be planned to migrate to uuidv4

DDDDDanica added a commit that referenced this pull request Jan 8, 2025
## Explanation
Changes introduced in this PR including:

- remove fallback of metaMetricsId and rely on client side always
returning a value
- refactor getMetaMetricsId to only handle synchronous (extension)
function

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

## References
Addressed feedback
#5051 (comment)
<!--
Are there any issues that this pull request is tied to?
Are there other links that reviewers should consult to understand these
changes better?
Are there client or consumer pull requests to adopt any breaking
changes?

For example:

* Fixes #12345
* Related to #67890
-->
<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

### `@metamask/remote-feature-flag-controller`

- **CHANGED**: Modify signature of `getMetaMetricsId` to handle only
synchronous function
- **CHANGED**:  Remove fallback of `metaMetricsId`

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
- [x] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
DDDDDanica added a commit that referenced this pull request Jan 10, 2025
…ion (#5110)

## Explanation

Replace hash-based random number generation with BigInt-based
implementation for better distribution and format support. The new
implementation properly handles both UUIDv4 and hex-format
metaMetricsIds, providing more consistent
and reliable user segmentation.
- Add support for UUIDv4 format with proper bit normalization
- Improve hex format handling using BigInt for precise calculations
- Remove char-by-char hashing algorithm to prevent potential collisions

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

## References
Addresses:
#5051 (comment)
<!--
Are there any issues that this pull request is tied to?
Are there other links that reviewers should consult to understand these
changes better?
Are there client or consumer pull requests to adopt any breaking
changes?

For example:

* Fixes #12345
* Related to #67890
-->

## Changelog

<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

### `@metamask/remote-feature-flag-controller`
- **CHANGED**: Modify `generateDeterministicRandomNumber` to handle both
uuidv4(mobile new) and hex(mobile old and extension) side

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
- [x] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
PatrykLucka pushed a commit that referenced this pull request Jan 13, 2025
## Explanation
Changes introduced in this PR including:

- remove fallback of metaMetricsId and rely on client side always
returning a value
- refactor getMetaMetricsId to only handle synchronous (extension)
function

<!--
Thanks for your contribution! Take a moment to answer these questions so
that reviewers have the information they need to properly understand
your changes:

* What is the current state of things and why does it need to change?
* What is the solution your changes offer and how does it work?
* Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?
* If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?
* If you had to upgrade a dependency, why did you do so?
-->

## References
Addressed feedback
#5051 (comment)
<!--
Are there any issues that this pull request is tied to?
Are there other links that reviewers should consult to understand these
changes better?
Are there client or consumer pull requests to adopt any breaking
changes?

For example:

* Fixes #12345
* Related to #67890
-->
<!--
If you're making any consumer-facing changes, list those changes here as
if you were updating a changelog, using the template below as a guide.

(CATEGORY is one of BREAKING, ADDED, CHANGED, DEPRECATED, REMOVED, or
FIXED. For security-related issues, follow the Security Advisory
process.)

Please take care to name the exact pieces of the API you've added or
changed (e.g. types, interfaces, functions, or methods).

If there are any breaking changes, make sure to offer a solution for
consumers to follow once they upgrade to the changes.

Finally, if you're only making changes to development scripts or tests,
you may replace the template below with "None".
-->

### `@metamask/remote-feature-flag-controller`

- **CHANGED**: Modify signature of `getMetaMetricsId` to handle only
synchronous function
- **CHANGED**:  Remove fallback of `metaMetricsId`

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
- [x] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants