From 3ae4dd85c499f9f05d40d84425ad6235757493dd Mon Sep 17 00:00:00 2001 From: dddddanica Date: Thu, 9 Jan 2025 12:46:14 +0000 Subject: [PATCH] fix(3742): add validation to metametricsId --- .../src/utils/user-segmentation-utils.test.ts | 51 +++++++++++++++++++ .../src/utils/user-segmentation-utils.ts | 41 +++++++++++---- 2 files changed, 83 insertions(+), 9 deletions(-) diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts index cef18cdb9e..6a8b96f3cd 100644 --- a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts @@ -13,6 +13,15 @@ const MOCK_METRICS_IDS = { MOBILE_MAX: 'ffffffff-ffff-4fff-bfff-ffffffffffff', EXTENSION_MIN: `0x${'0'.repeat(64) as string}`, EXTENSION_MAX: `0x${'f'.repeat(64) as string}`, + UUID_V3: '00000000-0000-3000-8000-000000000000', + INVALID_HEX_NO_PREFIX: + '86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420', + INVALID_HEX_SHORT: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d13642', + INVALID_HEX_LONG: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d1364200', + INVALID_HEX_INVALID_CHARS: + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d13642g', }; const MOCK_FEATURE_FLAGS = { @@ -125,6 +134,48 @@ describe('user-segmentation-utils', () => { }); }); }); + + describe('MetaMetrics ID validation', () => { + it('throws an error if the MetaMetrics ID is empty', () => { + expect(() => generateDeterministicRandomNumber('')).toThrow( + 'MetaMetrics ID cannot be empty', + ); + }); + + it('throws an error if the MetaMetrics ID is not a valid UUIDv4', () => { + expect(() => + generateDeterministicRandomNumber(MOCK_METRICS_IDS.UUID_V3), + ).toThrow('Invalid UUID version. Expected v4, got v3'); + }); + + it('throws an error if the MetaMetrics ID is not a valid hex string', () => { + expect(() => + generateDeterministicRandomNumber( + MOCK_METRICS_IDS.INVALID_HEX_NO_PREFIX, + ), + ).toThrow('Hex ID must start with 0x prefix'); + }); + + it('throws an error if the MetaMetrics ID is a short hex string', () => { + expect(() => + generateDeterministicRandomNumber(MOCK_METRICS_IDS.INVALID_HEX_SHORT), + ).toThrow('Invalid hex ID length. Expected 64 characters, got 63'); + }); + + it('throws an error if the MetaMetrics ID is a long hex string', () => { + expect(() => + generateDeterministicRandomNumber(MOCK_METRICS_IDS.INVALID_HEX_LONG), + ).toThrow('Invalid hex ID length. Expected 64 characters, got 65'); + }); + + it('throws an error if the MetaMetrics ID contains invalid hex characters', () => { + expect(() => + generateDeterministicRandomNumber( + MOCK_METRICS_IDS.INVALID_HEX_INVALID_CHARS, + ), + ).toThrow('Hex ID contains invalid characters'); + }); + }); }); describe('isFeatureFlagWithScopeValue', () => { diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts index f0d62cf65e..b14056c453 100644 --- a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts @@ -16,37 +16,60 @@ const MIN_UUID_V4 = '00000000-0000-4000-8000-000000000000'; const MAX_UUID_V4 = 'ffffffff-ffff-4fff-bfff-ffffffffffff'; const MIN_UUID_V4_BIGINT = uuidStringToBigInt(MIN_UUID_V4); const MAX_UUID_V4_BIGINT = uuidStringToBigInt(MAX_UUID_V4); -const UUID_V4_VALUE_RANGE_BIGINT = - MAX_UUID_V4_BIGINT - MIN_UUID_V4_BIGINT; +const UUID_V4_VALUE_RANGE_BIGINT = MAX_UUID_V4_BIGINT - MIN_UUID_V4_BIGINT; /** * Generates a deterministic random number between 0 and 1 based on a metaMetricsId. * This is useful for A/B testing and feature flag rollouts where we want * consistent group assignment for the same user. * @param metaMetricsId - The unique identifier used to generate the deterministic random number. Must be either: - * - A UUIDv4 string (e.g., '123e4567-e89b-12d3-a456-426614174000') - * - A hex string with '0x' prefix (e.g., '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420') + * - A UUIDv4 string (e.g., '123e4567-e89b-12d3-a456-426614174000' + * - A hex string with '0x' prefix (e.g., '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420') * @returns A number between 0 and 1, deterministically generated from the input ID. - * The same input will always produce the same output. + * The same input will always produce the same output. */ export function generateDeterministicRandomNumber( metaMetricsId: string, ): number { + if (!metaMetricsId) { + throw new Error('MetaMetrics ID cannot be empty'); + } + let idValue: bigint; let maxValue: bigint; + // uuidv4 format - if (uuidValidate(metaMetricsId) && uuidVersion(metaMetricsId) === 4) { - // Normalize the UUIDv4 range to start from 0 by subtracting MIN_UUID_V4_BIGINT. - // This ensures uniform distribution across the entire range, since UUIDv4 - // has restricted bits for version (4) and variant (8-b) that would otherwise skew the distribution + if (uuidValidate(metaMetricsId)) { + if (uuidVersion(metaMetricsId) !== 4) { + throw new Error( + `Invalid UUID version. Expected v4, got v${uuidVersion(metaMetricsId)}`, + ); + } idValue = uuidStringToBigInt(metaMetricsId) - MIN_UUID_V4_BIGINT; maxValue = UUID_V4_VALUE_RANGE_BIGINT; } else { // hex format with 0x prefix + if (!metaMetricsId.startsWith('0x')) { + throw new Error('Hex ID must start with 0x prefix'); + } + const cleanId = metaMetricsId.slice(2); + const EXPECTED_HEX_LENGTH = 64; // 32 bytes = 64 hex characters + + if (cleanId.length !== EXPECTED_HEX_LENGTH) { + throw new Error( + `Invalid hex ID length. Expected ${EXPECTED_HEX_LENGTH} characters, got ${cleanId.length}`, + ); + } + + if (!/^[0-9a-f]+$/iu.test(cleanId)) { + throw new Error('Hex ID contains invalid characters'); + } + idValue = BigInt(`0x${cleanId}`); maxValue = BigInt(`0x${'f'.repeat(cleanId.length)}`); } + // Use BigInt division first, then convert to number to maintain precision return Number((idValue * BigInt(1_000_000)) / maxValue) / 1_000_000; }