Skip to content

Commit 8724cc3

Browse files
committed
fix(3742): improve UUID random number distribution
1 parent 87cd8f5 commit 8724cc3

File tree

1 file changed

+32
-23
lines changed

1 file changed

+32
-23
lines changed

packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,52 @@ import { validate as uuidValidate, version as uuidVersion } from 'uuid';
33

44
import type { FeatureFlagScopeValue } from '../remote-feature-flag-controller-types';
55

6+
/**
7+
* Converts a UUID string to a BigInt by removing dashes and converting to hexadecimal.
8+
* @param uuid - The UUID string to convert
9+
* @returns The UUID as a BigInt value
10+
*/
11+
function uuidStringToBigInt(uuid: string): bigint {
12+
return BigInt(`0x${uuid.replace(/-/gu, '')}`);
13+
}
14+
15+
const MIN_UUID_V4 = '00000000-0000-4000-8000-000000000000';
16+
const MAX_UUID_V4 = 'ffffffff-ffff-4fff-bfff-ffffffffffff';
17+
const MIN_UUID_V4_BIGINT = uuidStringToBigInt(MIN_UUID_V4);
18+
const MAX_UUID_V4_BIGINT = uuidStringToBigInt(MAX_UUID_V4);
19+
const UUID_V4_VALUE_RANGE_BIGINT =
20+
MAX_UUID_V4_BIGINT - MIN_UUID_V4_BIGINT;
21+
622
/**
723
* Generates a deterministic random number between 0 and 1 based on a metaMetricsId.
824
* This is useful for A/B testing and feature flag rollouts where we want
925
* consistent group assignment for the same user.
10-
*
11-
* Supports two metaMetricsId formats:
12-
* - UUIDv4 format (new mobile implementation)
13-
* - Hex format with 0x prefix (extension or old mobile implementation)
14-
*
15-
* For UUIDv4 format, the following normalizations are applied:
16-
* - Removes all dashes from the UUID
17-
* - Remove version (4) bits and replace with 'f'
18-
* - Converts the remaining hex string to a BigInt for calculation
19-
*
20-
* For hex format:
21-
* - Expects a hex string with '0x' prefix (e.g., '0x1234abcd')
22-
* - Removes the '0x' prefix before conversion
23-
* - Converts the remaining hex string to a BigInt for calculation
24-
*
25-
* @param metaMetricsId - The unique identifier used to generate the deterministic random number, can be a UUIDv4 or hex string
26-
* @returns A number between 0 and 1 that is deterministic for the given metaMetricsId
26+
* @param metaMetricsId - The unique identifier used to generate the deterministic random number. Must be either:
27+
* - A UUIDv4 string (e.g., '123e4567-e89b-12d3-a456-426614174000')
28+
* - A hex string with '0x' prefix (e.g., '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420')
29+
* @returns A number between 0 and 1, deterministically generated from the input ID.
30+
* The same input will always produce the same output.
2731
*/
2832
export function generateDeterministicRandomNumber(
2933
metaMetricsId: string,
3034
): number {
31-
let cleanId: string;
35+
let idValue: bigint;
36+
let maxValue: bigint;
3237
// uuidv4 format
3338
if (uuidValidate(metaMetricsId) && uuidVersion(metaMetricsId) === 4) {
34-
cleanId = metaMetricsId.replace(/-/gu, '').replace(/^(.{12})4/u, '$1f');
39+
// Normalize the UUIDv4 range to start from 0 by subtracting MIN_UUID_V4_BIGINT.
40+
// This ensures uniform distribution across the entire range, since UUIDv4
41+
// has restricted bits for version (4) and variant (8-b) that would otherwise skew the distribution
42+
idValue = uuidStringToBigInt(metaMetricsId) - MIN_UUID_V4_BIGINT;
43+
maxValue = UUID_V4_VALUE_RANGE_BIGINT;
3544
} else {
3645
// hex format with 0x prefix
37-
cleanId = metaMetricsId.slice(2);
46+
const cleanId = metaMetricsId.slice(2);
47+
idValue = BigInt(`0x${cleanId}`);
48+
maxValue = BigInt(`0x${'f'.repeat(cleanId.length)}`);
3849
}
39-
const value = BigInt(`0x${cleanId}`);
40-
const maxValue = BigInt(`0x${'f'.repeat(cleanId.length)}`);
4150
// Use BigInt division first, then convert to number to maintain precision
42-
return Number((value * BigInt(1_000_000)) / maxValue) / 1_000_000;
51+
return Number((idValue * BigInt(1_000_000)) / maxValue) / 1_000_000;
4352
}
4453

4554
/**

0 commit comments

Comments
 (0)