Skip to content

Commit a66b49f

Browse files
committed
fix(3742): add validation to metametricsId
1 parent 8724cc3 commit a66b49f

File tree

2 files changed

+77
-9
lines changed

2 files changed

+77
-9
lines changed

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ const MOCK_METRICS_IDS = {
1313
MOBILE_MAX: 'ffffffff-ffff-4fff-bfff-ffffffffffff',
1414
EXTENSION_MIN: `0x${'0'.repeat(64) as string}`,
1515
EXTENSION_MAX: `0x${'f'.repeat(64) as string}`,
16+
UUID_V3: '00000000-0000-3000-8000-000000000000',
17+
INVALID_HEX_NO_PREFIX:
18+
'86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420',
19+
INVALID_HEX_SHORT:
20+
'0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d13642',
21+
INVALID_HEX_LONG:
22+
'0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d1364200',
23+
INVALID_HEX_INVALID_CHARS:
24+
'0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d13642g',
1625
};
1726

1827
const MOCK_FEATURE_FLAGS = {
@@ -125,6 +134,42 @@ describe('user-segmentation-utils', () => {
125134
});
126135
});
127136
});
137+
138+
describe('MetaMetrics ID validation', () => {
139+
it('throws an error if the MetaMetrics ID is not a valid UUIDv4', () => {
140+
expect(() =>
141+
generateDeterministicRandomNumber(MOCK_METRICS_IDS.UUID_V3),
142+
).toThrow('Invalid UUID version. Expected v4, got v3');
143+
});
144+
145+
it('throws an error if the MetaMetrics ID is not a valid hex string', () => {
146+
expect(() =>
147+
generateDeterministicRandomNumber(
148+
MOCK_METRICS_IDS.INVALID_HEX_NO_PREFIX,
149+
),
150+
).toThrow('Hex ID must start with 0x prefix');
151+
});
152+
153+
it('throws an error if the MetaMetrics ID is a short hex string', () => {
154+
expect(() =>
155+
generateDeterministicRandomNumber(MOCK_METRICS_IDS.INVALID_HEX_SHORT),
156+
).toThrow('Invalid hex ID length. Expected 64 characters, got 63');
157+
});
158+
159+
it('throws an error if the MetaMetrics ID is a long hex string', () => {
160+
expect(() =>
161+
generateDeterministicRandomNumber(MOCK_METRICS_IDS.INVALID_HEX_LONG),
162+
).toThrow('Invalid hex ID length. Expected 64 characters, got 65');
163+
});
164+
165+
it('throws an error if the MetaMetrics ID contains invalid hex characters', () => {
166+
expect(() =>
167+
generateDeterministicRandomNumber(
168+
MOCK_METRICS_IDS.INVALID_HEX_INVALID_CHARS,
169+
),
170+
).toThrow('Hex ID contains invalid characters');
171+
});
172+
});
128173
});
129174

130175
describe('isFeatureFlagWithScopeValue', () => {

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,60 @@ const MIN_UUID_V4 = '00000000-0000-4000-8000-000000000000';
1616
const MAX_UUID_V4 = 'ffffffff-ffff-4fff-bfff-ffffffffffff';
1717
const MIN_UUID_V4_BIGINT = uuidStringToBigInt(MIN_UUID_V4);
1818
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;
19+
const UUID_V4_VALUE_RANGE_BIGINT = MAX_UUID_V4_BIGINT - MIN_UUID_V4_BIGINT;
2120

2221
/**
2322
* Generates a deterministic random number between 0 and 1 based on a metaMetricsId.
2423
* This is useful for A/B testing and feature flag rollouts where we want
2524
* consistent group assignment for the same user.
2625
* @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')
26+
* - A UUIDv4 string (e.g., '123e4567-e89b-12d3-a456-426614174000'
27+
* - A hex string with '0x' prefix (e.g., '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420')
2928
* @returns A number between 0 and 1, deterministically generated from the input ID.
30-
* The same input will always produce the same output.
29+
* The same input will always produce the same output.
3130
*/
3231
export function generateDeterministicRandomNumber(
3332
metaMetricsId: string,
3433
): number {
34+
if (!metaMetricsId) {
35+
throw new Error('MetaMetrics ID cannot be empty');
36+
}
37+
3538
let idValue: bigint;
3639
let maxValue: bigint;
40+
3741
// uuidv4 format
38-
if (uuidValidate(metaMetricsId) && uuidVersion(metaMetricsId) === 4) {
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+
if (uuidValidate(metaMetricsId)) {
43+
if (uuidVersion(metaMetricsId) !== 4) {
44+
throw new Error(
45+
`Invalid UUID version. Expected v4, got v${uuidVersion(metaMetricsId)}`,
46+
);
47+
}
4248
idValue = uuidStringToBigInt(metaMetricsId) - MIN_UUID_V4_BIGINT;
4349
maxValue = UUID_V4_VALUE_RANGE_BIGINT;
4450
} else {
4551
// hex format with 0x prefix
52+
if (!metaMetricsId.startsWith('0x')) {
53+
throw new Error('Hex ID must start with 0x prefix');
54+
}
55+
4656
const cleanId = metaMetricsId.slice(2);
57+
const EXPECTED_HEX_LENGTH = 64; // 32 bytes = 64 hex characters
58+
59+
if (cleanId.length !== EXPECTED_HEX_LENGTH) {
60+
throw new Error(
61+
`Invalid hex ID length. Expected ${EXPECTED_HEX_LENGTH} characters, got ${cleanId.length}`,
62+
);
63+
}
64+
65+
if (!/^[0-9a-f]+$/iu.test(cleanId)) {
66+
throw new Error('Hex ID contains invalid characters');
67+
}
68+
4769
idValue = BigInt(`0x${cleanId}`);
4870
maxValue = BigInt(`0x${'f'.repeat(cleanId.length)}`);
4971
}
72+
5073
// Use BigInt division first, then convert to number to maintain precision
5174
return Number((idValue * BigInt(1_000_000)) / maxValue) / 1_000_000;
5275
}

0 commit comments

Comments
 (0)