Skip to content

Commit 3401245

Browse files
authored
A shared function to generate bundle IDs in Metro serializer; tests for it (#5380)
1 parent 90e7cb3 commit 3401245

File tree

2 files changed

+133
-12
lines changed

2 files changed

+133
-12
lines changed

packages/core/src/js/tools/sentryMetroSerializer.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
8282
// That needs to be done because when Metro 0.83.2 stopped importing `BabelSourceMapSegment`
8383
// from `@babel/generator` and defined it locally, it subtly changed the source map output format.
8484
// https://github.com/facebook/metro/blob/main/packages/metro-source-map/src/source-map.js#L47
85-
const hash = crypto.createHash('md5');
86-
hash.update(bundleCode);
87-
debugId = stringToUUID(hash.digest('hex'));
85+
debugId = calculateDebugId(bundleCode);
8886
// eslint-disable-next-line no-console
8987
console.log('info ' + `Bundle Debug ID (calculated): ${debugId}`);
9088
}
@@ -125,7 +123,7 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
125123
*/
126124
function createSentryBundleCallback(debugIdModule: Module<VirtualJSOutput> & { setSource: (code: string) => void }) {
127125
return (bundle: Bundle) => {
128-
const debugId = calculateDebugId(bundle);
126+
const debugId = calculateDebugId(bundle.pre, bundle.modules);
129127
debugIdModule.setSource(injectDebugId(debugIdModule.getSource().toString(), debugId));
130128
bundle.pre = injectDebugId(bundle.pre, debugId);
131129
return bundle;
@@ -153,16 +151,15 @@ function createDebugIdModule(debugId: string): Module<VirtualJSOutput> & { setSo
153151
return createVirtualJSModule(DEBUG_ID_MODULE_PATH, createDebugIdSnippet(debugId));
154152
}
155153

156-
function calculateDebugId(bundle: Bundle): string {
154+
function calculateDebugId(bundleCode: string, modules?: Array<[id: number, code: string]>): string {
157155
const hash = crypto.createHash('md5');
158-
hash.update(bundle.pre);
159-
for (const [, code] of bundle.modules) {
160-
hash.update(code);
156+
hash.update(bundleCode);
157+
if (modules) {
158+
for (const [, code] of modules) {
159+
hash.update(code);
160+
}
161161
}
162-
hash.update(bundle.post);
163-
164-
const debugId = stringToUUID(hash.digest('hex'));
165-
return debugId;
162+
return stringToUUID(hash.digest('hex'));
166163
}
167164

168165
function injectDebugId(code: string, debugId: string): string {

packages/core/test/tools/sentryMetroSerializer.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,130 @@ describe('Sentry Metro Serializer', () => {
108108
expect(debugIdMatch2?.[1]).toBe(debugId);
109109
}
110110
});
111+
112+
describe('calculateDebugId', () => {
113+
// We need to access the private function for testing
114+
const crypto = require('crypto');
115+
const { stringToUUID } = require('../../src/js/tools/utils');
116+
117+
function calculateDebugId(bundleCode: string, modules?: Array<[id: number, code: string]>): string {
118+
const hash = crypto.createHash('md5');
119+
hash.update(bundleCode);
120+
if (modules) {
121+
for (const [, code] of modules) {
122+
hash.update(code);
123+
}
124+
}
125+
return stringToUUID(hash.digest('hex'));
126+
}
127+
128+
test('generates a valid UUID v4 format', () => {
129+
const bundleCode = 'console.log("test");';
130+
const debugId = calculateDebugId(bundleCode);
131+
132+
expect(debugId).toMatch(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
133+
});
134+
135+
test('generates deterministic debug ID for the same bundle code', () => {
136+
const bundleCode = 'console.log("test");';
137+
const debugId1 = calculateDebugId(bundleCode);
138+
const debugId2 = calculateDebugId(bundleCode);
139+
140+
expect(debugId1).toBe(debugId2);
141+
});
142+
143+
test('generates different debug IDs for different bundle code', () => {
144+
const bundleCode1 = 'console.log("test1");';
145+
const bundleCode2 = 'console.log("test2");';
146+
const debugId1 = calculateDebugId(bundleCode1);
147+
const debugId2 = calculateDebugId(bundleCode2);
148+
149+
expect(debugId1).not.toBe(debugId2);
150+
});
151+
152+
test('handles undefined modules parameter', () => {
153+
const bundleCode = 'console.log("test");';
154+
const debugId = calculateDebugId(bundleCode, undefined);
155+
156+
expect(debugId).toMatch(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
157+
});
158+
159+
test('handles empty modules array', () => {
160+
const bundleCode = 'console.log("test");';
161+
const debugId1 = calculateDebugId(bundleCode, []);
162+
const debugId2 = calculateDebugId(bundleCode);
163+
164+
// Should generate the same debug ID as without modules
165+
expect(debugId1).toBe(debugId2);
166+
});
167+
168+
test('includes modules in debug ID calculation', () => {
169+
const bundleCode = 'console.log("test");';
170+
const modules: Array<[id: number, code: string]> = [
171+
[1, 'function foo() { return "bar"; }'],
172+
[2, 'function baz() { return "qux"; }'],
173+
];
174+
175+
const debugIdWithModules = calculateDebugId(bundleCode, modules);
176+
const debugIdWithoutModules = calculateDebugId(bundleCode);
177+
178+
expect(debugIdWithModules).not.toBe(debugIdWithoutModules);
179+
});
180+
181+
test('generates different debug IDs when modules differ', () => {
182+
const bundleCode = 'console.log("test");';
183+
const modules1: Array<[id: number, code: string]> = [[1, 'function foo() { return "bar"; }']];
184+
const modules2: Array<[id: number, code: string]> = [[1, 'function foo() { return "baz"; }']];
185+
186+
const debugId1 = calculateDebugId(bundleCode, modules1);
187+
const debugId2 = calculateDebugId(bundleCode, modules2);
188+
189+
expect(debugId1).not.toBe(debugId2);
190+
});
191+
192+
test('generates same debug ID when modules have same content but different IDs', () => {
193+
const bundleCode = 'console.log("test");';
194+
const modules1: Array<[id: number, code: string]> = [[1, 'function foo() { return "bar"; }']];
195+
const modules2: Array<[id: number, code: string]> = [[2, 'function foo() { return "bar"; }']];
196+
197+
const debugId1 = calculateDebugId(bundleCode, modules1);
198+
const debugId2 = calculateDebugId(bundleCode, modules2);
199+
200+
// Module IDs are not used in the hash calculation, only the code
201+
expect(debugId1).toBe(debugId2);
202+
});
203+
204+
test('generates different debug IDs when module order differs', () => {
205+
const bundleCode = 'console.log("test");';
206+
const modules1: Array<[id: number, code: string]> = [
207+
[1, 'function foo() { return "bar"; }'],
208+
[2, 'function baz() { return "qux"; }'],
209+
];
210+
const modules2: Array<[id: number, code: string]> = [
211+
[2, 'function baz() { return "qux"; }'],
212+
[1, 'function foo() { return "bar"; }'],
213+
];
214+
215+
const debugId1 = calculateDebugId(bundleCode, modules1);
216+
const debugId2 = calculateDebugId(bundleCode, modules2);
217+
218+
// Order matters in hash calculation
219+
expect(debugId1).not.toBe(debugId2);
220+
});
221+
222+
test('handles empty bundle code', () => {
223+
const debugId = calculateDebugId('');
224+
225+
expect(debugId).toMatch(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
226+
});
227+
228+
test('handles large bundle code', () => {
229+
const largeBundleCode = 'console.log("test");'.repeat(10000);
230+
const debugId = calculateDebugId(largeBundleCode);
231+
232+
expect(debugId).toMatch(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/);
233+
});
234+
});
111235
});
112236

113237
function mockMinSerializerArgs(options?: {

0 commit comments

Comments
 (0)