Skip to content

Commit

Permalink
ref(utils): Improve uuid generation (#5426)
Browse files Browse the repository at this point in the history
Since [modern browsers](https://caniuse.com/mdn-api_crypto_randomuuid) support `crypto.randomUUID()` we make use of it when generating UUIDs. This patch does the following:

- Shaves ~160 bytes off the browser bundle
- Modern platforms bail out quickly with `crypto.randomUUID()`
- Less modern browsers (including IE11 and Safari < v15.4) use `crypto.getRandomValues()`
- Node.js 
  - `< v15` uses `Math.random()`
  - `v15 > v16.7 uses` `crypto.getRandomValues()`
  - `>= v16.7 uses` `crypto.randomUUID()`
- Validated that all code paths do in fact return valid uuidv4 ids with hyphens removed!
- Added tests to test all different kinds of random value and uuid creation
  • Loading branch information
timfish committed Jul 22, 2022
1 parent 3665831 commit f1cfc70
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 29 deletions.
44 changes: 15 additions & 29 deletions packages/utils/src/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,47 +12,33 @@ interface MsCryptoWindow extends Window {
msCrypto?: Crypto;
}

/** Many browser now support native uuid v4 generation */
interface CryptoWithRandomUUID extends Crypto {
randomUUID?(): string;
}

/**
* UUID4 generator
*
* @returns string Generated UUID4.
*/
export function uuid4(): string {
const global = getGlobalObject() as MsCryptoWindow;
const crypto = global.crypto || global.msCrypto;
const crypto = (global.crypto || global.msCrypto) as CryptoWithRandomUUID;

if (!(crypto === void 0) && crypto.getRandomValues) {
// Use window.crypto API if available
const arr = new Uint16Array(8);
crypto.getRandomValues(arr);

// set 4 in byte 7
// eslint-disable-next-line no-bitwise
arr[3] = (arr[3] & 0xfff) | 0x4000;
// set 2 most significant bits of byte 9 to '10'
// eslint-disable-next-line no-bitwise
arr[4] = (arr[4] & 0x3fff) | 0x8000;
if (crypto && crypto.randomUUID) {
return crypto.randomUUID().replace(/-/g, '');
}

const pad = (num: number): string => {
let v = num.toString(16);
while (v.length < 4) {
v = `0${v}`;
}
return v;
};
const getRandomByte =
crypto && crypto.getRandomValues ? () => crypto.getRandomValues(new Uint8Array(1))[0] : () => Math.random() * 16;

return (
pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + pad(arr[5]) + pad(arr[6]) + pad(arr[7])
);
}
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, c => {
// eslint-disable-next-line no-bitwise
const r = (Math.random() * 16) | 0;
// Concatenating the following numbers as strings results in '10000000100040008000100000000000'
return (([1e7] as unknown as string) + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, c =>
// eslint-disable-next-line no-bitwise
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
((c as unknown as number) ^ ((getRandomByte() & 15) >> ((c as unknown as number) / 4))).toString(16),
);
}

/**
Expand Down
33 changes: 33 additions & 0 deletions packages/utils/test/misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
checkOrSetAlreadyCaught,
getEventDescription,
stripUrlQueryAndFragment,
uuid4,
} from '../src/misc';

describe('getEventDescription()', () => {
Expand Down Expand Up @@ -298,3 +299,35 @@ describe('checkOrSetAlreadyCaught()', () => {
expect((exception as any).__sentry_captured__).toBe(true);
});
});

describe('uuid4 generation', () => {
// Jest messes with the global object, so there is no global crypto object in any node version
// For this reason we need to create our own crypto object for each test to cover all the code paths
it('returns valid uuid v4 ids via Math.random', () => {
for (let index = 0; index < 1_000; index++) {
expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i);
}
});

it('returns valid uuid v4 ids via crypto.getRandomValues', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const cryptoMod = require('crypto');

(global as any).crypto = { getRandomValues: cryptoMod.getRandomValues };

for (let index = 0; index < 1_000; index++) {
expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i);
}
});

it('returns valid uuid v4 ids via crypto.randomUUID', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const cryptoMod = require('crypto');

(global as any).crypto = { randomUUID: cryptoMod.randomUUID };

for (let index = 0; index < 1_000; index++) {
expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i);
}
});
});

0 comments on commit f1cfc70

Please sign in to comment.