-
-
Notifications
You must be signed in to change notification settings - Fork 201
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
Conversation
f9326fa
to
19d6bb3
Compare
e025bda
to
96b3edc
Compare
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.
The only thing to change is Metametrics is actually a UUIDv4 and hot a hex hash
packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts
Outdated
Show resolved
Hide resolved
packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts
Outdated
Show resolved
Hide resolved
packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts
Show resolved
Hide resolved
packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts
Outdated
Show resolved
Hide resolved
packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts
Outdated
Show resolved
Hide resolved
packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts
Outdated
Show resolved
Hide resolved
1803083
to
209a912
Compare
209a912
to
f2bc3bf
Compare
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.
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
'123e4567-e89b-12d3-a456-426614174000', | ||
'987fcdeb-51a2-3c4b-9876-543210fedcba', | ||
'a1b2c3d4-e5f6-7890-abcd-ef1234567890', | ||
'f9e8d7c6-b5a4-3210-9876-543210fedcba', |
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.
these are not valid UUIDv4.
- Check 123e4567-e89b-12d3-a456-426614174000 is UUIDv1
- Check 987fcdeb-51a2-3c4b-9876-543210fedcba is UUIDv3
- Check a1b2c3d4-e5f6-7890-abcd-ef1234567890 is UUIDv7
- Check f9e8d7c6-b5a4-3210-9876-543210fedcba is UUIDv3
@@ -41,7 +41,7 @@ const MOCK_FLAGS_WITH_THRESHOLD = { | |||
], | |||
}; | |||
|
|||
const MOCK_METRICS_ID = '0x1234567890abcdef'; | |||
const MOCK_METRICS_ID = 'f9e8d7c6-b5a4-3210-9876-543210fedcba'; |
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.
Not a valid UUIDv4, this is UUIDv3
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.
Good catch ! addressed 93d8351
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.
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. |
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.
based on metaMetricsId
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.
🕵🏻♂️ good catch ! addressed 63a7274
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.
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!
@joaoloureirop changed to a promise in 1f94695 |
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.
Looks good!
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.
Looks good to me.
* Generates UUIDv4 as a fallback metaMetricsId | ||
* @returns A UUIDv4 string | ||
*/ | ||
export function generateFallbackMetaMetricsId(): string { |
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.
This is non-deterministic, and therefore unsuitable for determining whether a feature flag should be enabled or not
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 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.
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.
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
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.
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; |
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.
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)
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.
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.
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.
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
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.
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
## 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
…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
## 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
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 frommetametricsId
. If encounters a flag from api with scope astype: "threshold"
, controller will drill down to return the value that's matching the threshold generated based onmetametricsId
.Note:
metametricsId
will always be provided from client, a random SHA-256 hash will be used as fallbackReferences
Fixes https://github.com/MetaMask/MetaMask-planning/issues/3742
Changelog
@metamask/remote-feature-flag-controller
Checklist