Skip to content

Commit

Permalink
feat: Random uuid for telemetry package. (#689)
Browse files Browse the repository at this point in the history
Originally this implementation was done for the telemetry package. When
implementing the browser package we needed the same implementation.

Currently this cannot be easily shared, but a task has been created to
followup and change that.

This is now a copy of the version in the browser package.

Subsequent PRs will make use of this functionality.
  • Loading branch information
kinyoklion authored Nov 15, 2024
1 parent 8ad7d46 commit 4cf34f9
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-bitwise */
import { fallbackUuidV4, formatDataAsUuidV4 } from '../src/randomUuidV4';

it('formats conformant UUID', () => {
// For this test we remove the random component and just inspect the variant and version.
const idA = formatDataAsUuidV4(Array(16).fill(0x00));
const idB = formatDataAsUuidV4(Array(16).fill(0xff));
const idC = fallbackUuidV4();

// 32 characters and 4 dashes
expect(idC).toHaveLength(36);
const versionA = idA[14];
const versionB = idB[14];
const versionC = idB[14];

expect(versionA).toEqual('4');
expect(versionB).toEqual('4');
expect(versionC).toEqual('4');

// Keep only the top 2 bits.
const specifierA = parseInt(idA[19], 16) & 0xc;
const specifierB = parseInt(idB[19], 16) & 0xc;
const specifierC = parseInt(idC[19], 16) & 0xc;

// bit 6 should be 0 and bit 8 should be one, which is 0x8
expect(specifierA).toEqual(0x8);
expect(specifierB).toEqual(0x8);
expect(specifierC).toEqual(0x8);
});
102 changes: 102 additions & 0 deletions packages/telemetry/browser-telemetry/src/randomUuidV4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// This implementation is the same as in the browser package. Eventually we
// will want a common package for this type of code. (SDK-905)

// The implementation in this file generates UUIDs in v4 format and is suitable
// for use as a UUID in LaunchDarkly events. It is not a rigorous implementation.

// It uses crypto.randomUUID when available.
// If crypto.randomUUID is not available, then it uses random values and forms
// the UUID itself.
// When possible it uses crypto.getRandomValues, but it can use Math.random
// if crypto.getRandomValues is not available.

// UUIDv4 Struct definition.
// https://www.rfc-archive.org/getrfc.php?rfc=4122
// Appendix A. Appendix A - Sample Implementation
const timeLow = {
start: 0,
end: 3,
};
const timeMid = {
start: 4,
end: 5,
};
const timeHiAndVersion = {
start: 6,
end: 7,
};
const clockSeqHiAndReserved = {
start: 8,
end: 8,
};
const clockSeqLow = {
start: 9,
end: 9,
};
const nodes = {
start: 10,
end: 15,
};

function getRandom128bit(): number[] {
if (crypto && crypto.getRandomValues) {
const typedArray = new Uint8Array(16);
crypto.getRandomValues(typedArray);
return [...typedArray.values()];
}
const values = [];
for (let index = 0; index < 16; index += 1) {
// Math.random is 0-1 with inclusive min and exclusive max.
values.push(Math.floor(Math.random() * 256));
}
return values;
}

function hex(bytes: number[], range: { start: number; end: number }): string {
let strVal = '';
for (let index = range.start; index <= range.end; index += 1) {
strVal += bytes[index].toString(16).padStart(2, '0');
}
return strVal;
}

/**
* Given a list of 16 random bytes generate a UUID in v4 format.
*
* Note: The input bytes are modified to conform to the requirements of UUID v4.
*
* @param bytes A list of 16 bytes.
* @returns A UUID v4 string.
*/
export function formatDataAsUuidV4(bytes: number[]): string {
// https://www.rfc-archive.org/getrfc.php?rfc=4122
// 4.4. Algorithms for Creating a UUID from Truly Random or
// Pseudo-Random Numbers

// Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and
// one, respectively.
// eslint-disable-next-line no-bitwise, no-param-reassign
bytes[clockSeqHiAndReserved.start] = (bytes[clockSeqHiAndReserved.start] | 0x80) & 0xbf;
// Set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to
// the 4-bit version number from Section 4.1.3.
// eslint-disable-next-line no-bitwise, no-param-reassign
bytes[timeHiAndVersion.start] = (bytes[timeHiAndVersion.start] & 0x0f) | 0x40;

return (
`${hex(bytes, timeLow)}-${hex(bytes, timeMid)}-${hex(bytes, timeHiAndVersion)}-` +
`${hex(bytes, clockSeqHiAndReserved)}${hex(bytes, clockSeqLow)}-${hex(bytes, nodes)}`
);
}

export function fallbackUuidV4(): string {
const bytes = getRandom128bit();
return formatDataAsUuidV4(bytes);
}

export default function randomUuidV4(): string {
if (typeof crypto !== undefined && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}

return fallbackUuidV4();
}

0 comments on commit 4cf34f9

Please sign in to comment.