Skip to content

Commit f1cfc70

Browse files
authored
ref(utils): Improve uuid generation (#5426)
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
1 parent 3665831 commit f1cfc70

File tree

2 files changed

+48
-29
lines changed

2 files changed

+48
-29
lines changed

packages/utils/src/misc.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,33 @@ interface MsCryptoWindow extends Window {
1212
msCrypto?: Crypto;
1313
}
1414

15+
/** Many browser now support native uuid v4 generation */
16+
interface CryptoWithRandomUUID extends Crypto {
17+
randomUUID?(): string;
18+
}
19+
1520
/**
1621
* UUID4 generator
1722
*
1823
* @returns string Generated UUID4.
1924
*/
2025
export function uuid4(): string {
2126
const global = getGlobalObject() as MsCryptoWindow;
22-
const crypto = global.crypto || global.msCrypto;
27+
const crypto = (global.crypto || global.msCrypto) as CryptoWithRandomUUID;
2328

24-
if (!(crypto === void 0) && crypto.getRandomValues) {
25-
// Use window.crypto API if available
26-
const arr = new Uint16Array(8);
27-
crypto.getRandomValues(arr);
28-
29-
// set 4 in byte 7
30-
// eslint-disable-next-line no-bitwise
31-
arr[3] = (arr[3] & 0xfff) | 0x4000;
32-
// set 2 most significant bits of byte 9 to '10'
33-
// eslint-disable-next-line no-bitwise
34-
arr[4] = (arr[4] & 0x3fff) | 0x8000;
29+
if (crypto && crypto.randomUUID) {
30+
return crypto.randomUUID().replace(/-/g, '');
31+
}
3532

36-
const pad = (num: number): string => {
37-
let v = num.toString(16);
38-
while (v.length < 4) {
39-
v = `0${v}`;
40-
}
41-
return v;
42-
};
33+
const getRandomByte =
34+
crypto && crypto.getRandomValues ? () => crypto.getRandomValues(new Uint8Array(1))[0] : () => Math.random() * 16;
4335

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

5844
/**

packages/utils/test/misc.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
checkOrSetAlreadyCaught,
77
getEventDescription,
88
stripUrlQueryAndFragment,
9+
uuid4,
910
} from '../src/misc';
1011

1112
describe('getEventDescription()', () => {
@@ -298,3 +299,35 @@ describe('checkOrSetAlreadyCaught()', () => {
298299
expect((exception as any).__sentry_captured__).toBe(true);
299300
});
300301
});
302+
303+
describe('uuid4 generation', () => {
304+
// Jest messes with the global object, so there is no global crypto object in any node version
305+
// For this reason we need to create our own crypto object for each test to cover all the code paths
306+
it('returns valid uuid v4 ids via Math.random', () => {
307+
for (let index = 0; index < 1_000; index++) {
308+
expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i);
309+
}
310+
});
311+
312+
it('returns valid uuid v4 ids via crypto.getRandomValues', () => {
313+
// eslint-disable-next-line @typescript-eslint/no-var-requires
314+
const cryptoMod = require('crypto');
315+
316+
(global as any).crypto = { getRandomValues: cryptoMod.getRandomValues };
317+
318+
for (let index = 0; index < 1_000; index++) {
319+
expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i);
320+
}
321+
});
322+
323+
it('returns valid uuid v4 ids via crypto.randomUUID', () => {
324+
// eslint-disable-next-line @typescript-eslint/no-var-requires
325+
const cryptoMod = require('crypto');
326+
327+
(global as any).crypto = { randomUUID: cryptoMod.randomUUID };
328+
329+
for (let index = 0; index < 1_000; index++) {
330+
expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i);
331+
}
332+
});
333+
});

0 commit comments

Comments
 (0)