Skip to content

Commit

Permalink
fix(3742): add validation to metametricsId
Browse files Browse the repository at this point in the history
  • Loading branch information
DDDDDanica committed Jan 9, 2025
1 parent 8724cc3 commit 3ae4dd8
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 3ae4dd8

Please sign in to comment.