fix(core): Resolve Metro from project root for source map generation - #6625
Conversation
The vendored default serializer required Metro internals resolvable from the SDK's own location. In this monorepo the core package has its own Metro dev dependency (0.84.4) that shadows the app's Metro, so bundling the RN 0.87 sample generated source maps with the mismatched older Metro and threw `Unexpected module with full source map found` on the metro-runtime require.js polyfill. Resolve Metro internals from `options.projectRoot` (the app being bundled) with a fallback to the previous SDK-local resolution. In a normal install both resolve the same Metro instance, so behavior is unchanged; in the monorepo the sample now bundles with its own 0.87 Metro. Resolution is deferred to the first serialization so `options.projectRoot` is available, and memoized thereafter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d8893b4 to
addbd1d
Compare
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
📲 Install BuildsAndroid
|
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ecf47a2+dirty | 3835.20 ms | 1222.43 ms | -2612.77 ms |
| 3817909+dirty | 1183.90 ms | 1187.50 ms | 3.60 ms |
| 5ee78d6+dirty | 3847.52 ms | 1224.27 ms | -2623.25 ms |
| 890d145+dirty | 1223.59 ms | 1231.37 ms | 7.78 ms |
| e763471+dirty | 3841.16 ms | 1218.06 ms | -2623.09 ms |
| 3d31fcf+dirty | 3838.09 ms | 1223.46 ms | -2614.63 ms |
| 9210ae6+dirty | 3815.93 ms | 1214.14 ms | -2601.79 ms |
| 26843eb+dirty | 3861.60 ms | 1238.77 ms | -2622.82 ms |
| 7a89652+dirty | 3861.46 ms | 1229.61 ms | -2631.85 ms |
| 9ad4522+dirty | 3850.40 ms | 1222.40 ms | -2628.00 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ecf47a2+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 3817909+dirty | 3.38 MiB | 4.73 MiB | 1.35 MiB |
| 5ee78d6+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 890d145+dirty | 3.38 MiB | 4.77 MiB | 1.38 MiB |
| e763471+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 3d31fcf+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| 9210ae6+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 26843eb+dirty | 4.98 MiB | 6.53 MiB | 1.55 MiB |
| 7a89652+dirty | 5.15 MiB | 6.70 MiB | 1.55 MiB |
| 9ad4522+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
Previous results on branch: fix/metro-resolve-project-root
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ade1508+dirty | 3839.98 ms | 1220.75 ms | -2619.23 ms |
| a1cdaff+dirty | 3856.69 ms | 1221.58 ms | -2635.11 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ade1508+dirty | 5.08 MiB | 6.72 MiB | 1.64 MiB |
| a1cdaff+dirty | 5.08 MiB | 6.80 MiB | 1.72 MiB |
Android (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| f9c1ed4+dirty | 431.00 ms | 466.22 ms | 35.22 ms |
| c2e182c+dirty | 471.64 ms | 553.59 ms | 81.95 ms |
| 41d6254+dirty | 424.45 ms | 474.34 ms | 49.89 ms |
| af33f3b+dirty | 434.90 ms | 506.14 ms | 71.24 ms |
| 882f8ae+dirty | 399.98 ms | 427.06 ms | 27.08 ms |
| 0b5120f+dirty | 503.22 ms | 538.60 ms | 35.38 ms |
| 774257e+dirty | 407.09 ms | 447.46 ms | 40.37 ms |
| bf168a4+dirty | 418.21 ms | 489.74 ms | 71.53 ms |
| 5569641+dirty | 406.43 ms | 428.51 ms | 22.08 ms |
| c151573+dirty | 530.34 ms | 559.43 ms | 29.09 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| f9c1ed4+dirty | 49.74 MiB | 54.86 MiB | 5.12 MiB |
| c2e182c+dirty | 49.74 MiB | 54.85 MiB | 5.11 MiB |
| 41d6254+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| af33f3b+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 882f8ae+dirty | 48.30 MiB | 53.60 MiB | 5.29 MiB |
| 0b5120f+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 774257e+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| bf168a4+dirty | 49.74 MiB | 55.09 MiB | 5.35 MiB |
| 5569641+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| c151573+dirty | 48.30 MiB | 53.54 MiB | 5.24 MiB |
Previous results on branch: fix/metro-resolve-project-root
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a1cdaff+dirty | 421.02 ms | 430.26 ms | 9.24 ms |
| ade1508+dirty | 485.55 ms | 545.23 ms | 59.68 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a1cdaff+dirty | 49.74 MiB | 55.66 MiB | 5.91 MiB |
| ade1508+dirty | 49.74 MiB | 55.45 MiB | 5.71 MiB |
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3b6e9f9+dirty | 3822.77 ms | 1208.00 ms | -2614.77 ms |
| 5a23c47+dirty | 3843.54 ms | 1226.05 ms | -2617.50 ms |
| 100ce80+dirty | 3843.57 ms | 1226.46 ms | -2617.12 ms |
| 0bd8916+dirty | 3816.17 ms | 1215.05 ms | -2601.12 ms |
| a0d8cf8+dirty | 3826.15 ms | 1213.12 ms | -2613.03 ms |
| 0a9e622+dirty | 3825.35 ms | 1219.04 ms | -2606.31 ms |
| 21a1e70+dirty | 3826.22 ms | 1217.74 ms | -2608.47 ms |
| 9ad4522+dirty | 3862.28 ms | 1226.27 ms | -2636.01 ms |
| 88735e9+dirty | 3837.80 ms | 1216.39 ms | -2621.41 ms |
| d0e3b3e+dirty | 3846.64 ms | 1226.27 ms | -2620.38 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 3b6e9f9+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 5a23c47+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 100ce80+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
| 0bd8916+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| a0d8cf8+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
| 0a9e622+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
| 21a1e70+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| 9ad4522+dirty | 4.98 MiB | 6.56 MiB | 1.58 MiB |
| 88735e9+dirty | 4.98 MiB | 6.46 MiB | 1.49 MiB |
| d0e3b3e+dirty | 4.98 MiB | 6.51 MiB | 1.53 MiB |
Previous results on branch: fix/metro-resolve-project-root
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ade1508+dirty | 3846.26 ms | 1221.43 ms | -2624.83 ms |
| a1cdaff+dirty | 3841.65 ms | 1224.58 ms | -2617.07 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ade1508+dirty | 5.08 MiB | 6.72 MiB | 1.64 MiB |
| a1cdaff+dirty | 5.08 MiB | 6.80 MiB | 1.72 MiB |
… over a nested SDK copy requireMetroModule now tries each candidate path shape against the app (projectRoot) before the SDK's own location, so the app's Metro wins even when the two copies expose internals via different subpaths (app on metro/src/*, SDK on metro/private/*). Adds projectRoot-preference and ordering-regression tests using on-disk fake-Metro fixtures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 15d4514+dirty | 413.63 ms | 449.62 ms | 35.99 ms |
| 26843eb+dirty | 442.02 ms | 506.29 ms | 64.27 ms |
| ca9d079+dirty | 460.67 ms | 512.54 ms | 51.87 ms |
| f9c1ed4+dirty | 520.57 ms | 594.84 ms | 74.27 ms |
| a0d8cf8+dirty | 533.71 ms | 564.25 ms | 30.54 ms |
| 20fbd51+dirty | 594.38 ms | 655.35 ms | 60.97 ms |
| 0b0f006+dirty | 446.31 ms | 458.31 ms | 12.00 ms |
| 1d3572b+dirty | 444.48 ms | 478.42 ms | 33.94 ms |
| 4953e94+dirty | 398.80 ms | 431.81 ms | 33.01 ms |
| 4e0b819+dirty | 599.31 ms | 654.31 ms | 55.00 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 15d4514+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 26843eb+dirty | 49.74 MiB | 55.26 MiB | 5.52 MiB |
| ca9d079+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| f9c1ed4+dirty | 49.74 MiB | 54.86 MiB | 5.12 MiB |
| a0d8cf8+dirty | 48.30 MiB | 53.49 MiB | 5.19 MiB |
| 20fbd51+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
| 0b0f006+dirty | 49.74 MiB | 55.66 MiB | 5.91 MiB |
| 1d3572b+dirty | 49.74 MiB | 55.38 MiB | 5.63 MiB |
| 4953e94+dirty | 43.94 MiB | 48.94 MiB | 5.00 MiB |
| 4e0b819+dirty | 49.74 MiB | 54.81 MiB | 5.07 MiB |
Previous results on branch: fix/metro-resolve-project-root
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a1cdaff+dirty | 428.06 ms | 458.04 ms | 29.98 ms |
| ade1508+dirty | 491.94 ms | 520.06 ms | 28.12 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a1cdaff+dirty | 49.74 MiB | 55.66 MiB | 5.91 MiB |
| ade1508+dirty | 49.74 MiB | 55.45 MiB | 5.71 MiB |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0645154. Configure here.
| */ | ||
| // oxlint-disable-next-line typescript-eslint(no-explicit-any) | ||
| function requireMetroModule(candidates: string[], projectRoot: string | undefined): any { | ||
| const roots = projectRoot ? [projectRoot, __dirname] : [__dirname]; |
There was a problem hiding this comment.
could projectRoot be an empty string here? it may silently fail if thats the case
There was a problem hiding this comment.
I think this should work since an empty string should fallback to [__dirname]
| ); | ||
| if (typeof sourceMapString !== 'function') { | ||
| throw new Error(` | ||
| [@sentry/react-native/metro] Cannot find sourceMapString function in 'metro/src/DeltaBundler/Serializers/sourceMapString'. |
There was a problem hiding this comment.
Isn't it also required on metro/private/DeltaBundler/Serializers/sourceMapString ?
There was a problem hiding this comment.
Good point 👍 Update with 79ea609 to be path agnostic
| sourceMapString = (sourceMapString as { sourceMapString: typeof sourceMapStringType }).sourceMapString; | ||
| return (entryPoint, preModules, graph, options) => { | ||
| if (!internals) { | ||
| internals = resolveMetroInternals(options.projectRoot); |
There was a problem hiding this comment.
Seems like an edge-case but local AI complained about the following:
resolveMetroInternals() (and its sourceMapString shape check/throw) now runs before isHot is
computed. Previously the throw only fired on non-hot builds; now it can break the dev server too.
Now it fires unconditionally on the very first bundle serialized, hot or not, so a bad Metro shape that used to only break production/export builds can now break yarn start itself.
lucas-zimerman
left a comment
There was a problem hiding this comment.
overall looks good, there are some bits to be polished, and after that we can merge it :D
Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>
📢 Type of change
📜 Description
The default Metro serializer resolved Metro's internal modules (
baseJSBundle,bundleToString,sourceMapString) from the SDK's own location (__dirname). In a workspace/monorepo where a different Metro version is nested under@sentry/react-nativethan the one the app uses, that meant source maps were generated with a mismatched Metro version.The serializer now resolves Metro internals preferring the app's
options.projectRoot, falling back to the SDK-resolvable Metro. Resolution is deferred to the first serialization (whenprojectRootis available) and memoized. In a normal single-Metro install both paths resolve to the same Metro, so behavior is unchanged.💡 Motivation and Context
Surfaced while bumping the React Native sample to 0.87 (Metro 0.87) while
packages/corepins Metro0.84.4as a devDependency: the nested SDK Metro shadowed the app's Metro 0.87 and broke production source-map generation. The fix also benefits real users who have multiple Metro versions resolvable in a monorepo.Split out of #6617 so the sample bump carries no core changes.
💚 How did you test it?
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps
options.projectRootwhen a different Metro is nested under the SDK.🤖 Generated with Claude Code