Skip to content

Commit c1f3a96

Browse files
antonisclaude
andauthored
fix: Prevent Metro serializer crash on non-standard serializer output (#6652)
* fix: Prevent Metro serializer crash on non-standard serializer output The Metro serializer used `'map' in serializerResult` to detect a `{ code, map }` bundle. For arrays that check is always true because of `Array.prototype.map`, so an array result (e.g. Expo's static/EAS Update export) yielded `{ code: undefined }` and crashed in `determineDebugIdFromBundleSource` with "Cannot read properties of undefined (reading 'match')". Detect a bundle with a positive check (object, not array, string `code`) and pass non-standard output through untouched instead of crashing. Fixes #6650 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: Point changelog entry to PR #6652 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7e4c6fb commit c1f3a96

3 files changed

Lines changed: 59 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
### Fixes
1717

18+
- Fix Metro bundler crash on Expo static/EAS Update exports ([#6652](https://github.com/getsentry/sentry-react-native/pull/6652))
1819
- No longer logs `NSNull cannot be converted` warnings on iOS with the New Architecture when clearing a scope context ([#6651](https://github.com/getsentry/sentry-react-native/pull/6651))
1920
- `time_to_initial_display`/`time_to_full_display` now measure the actual screen render for apps whose first navigation happens well after app start ([#6626](https://github.com/getsentry/sentry-react-native/pull/6626))
2021

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { MixedOutput, Module, ReadOnlyGraph } from 'metro';
22

33
import * as crypto from 'crypto';
44

5-
import type { Bundle, MetroSerializer, MetroSerializerOutput, SerializedBundle, VirtualJSOutput } from './utils';
5+
import type { Bundle, MetroSerializer, SerializedBundle, VirtualJSOutput } from './utils';
66

77
import {
88
createDebugIdSnippet,
@@ -73,8 +73,18 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
7373
const modifiedPreModules = prependModule(preModules, debugIdModule);
7474

7575
// Run wrapped serializer
76-
const serializerResult = serializer(entryPoint, modifiedPreModules, graph, options);
77-
const { code: bundleCode, map: bundleMapString } = await extractSerializerResult(serializerResult);
76+
const serializerResult = await serializer(entryPoint, modifiedPreModules, graph, options);
77+
const bundle = extractSerializerResult(serializerResult);
78+
if (!bundle) {
79+
// The wrapped serializer returned a non-standard output that is not a single
80+
// `{ code, map }` bundle (for example Expo's static/EAS Update export, which returns an
81+
// array of serial assets). We can't inject a Sentry Debug ID into such output here, so we
82+
// return it untouched to avoid crashing the bundler. Debug IDs for these outputs are added
83+
// by Expo's own serializer.
84+
// https://github.com/getsentry/sentry-react-native/issues/6650
85+
return serializerResult;
86+
}
87+
const { code: bundleCode, map: bundleMapString } = bundle;
7888

7989
// Add debug id comment to the bundle
8090
let debugId = determineDebugIdFromBundleSource(bundleCode);
@@ -133,21 +143,31 @@ function createSentryBundleCallback(debugIdModule: Module<VirtualJSOutput> & { s
133143
};
134144
}
135145

136-
async function extractSerializerResult(serializerResult: MetroSerializerOutput): Promise<SerializedBundle> {
146+
/**
147+
* Normalizes an (already awaited) Metro serializer result into a `{ code, map }` bundle.
148+
*
149+
* Returns `null` when the result is not a standard single bundle (for example an array of serial
150+
* assets produced by Expo's static export), so callers can skip Debug ID injection instead of
151+
* crashing. We must not use `'map' in result` to detect a bundle: for arrays that is always `true`
152+
* because of `Array.prototype.map`, which would yield `{ code: undefined }`.
153+
* https://github.com/getsentry/sentry-react-native/issues/6650
154+
*/
155+
function extractSerializerResult(serializerResult: unknown): SerializedBundle | null {
137156
if (typeof serializerResult === 'string') {
138157
return { code: serializerResult, map: '{}' };
139158
}
140159

141-
if ('map' in serializerResult) {
142-
return { code: serializerResult.code, map: serializerResult.map };
143-
}
144-
145-
const awaitedResult = await serializerResult;
146-
if (typeof awaitedResult === 'string') {
147-
return { code: awaitedResult, map: '{}' };
160+
if (
161+
serializerResult !== null &&
162+
typeof serializerResult === 'object' &&
163+
!Array.isArray(serializerResult) &&
164+
typeof (serializerResult as Partial<SerializedBundle>).code === 'string'
165+
) {
166+
const { code, map } = serializerResult as SerializedBundle;
167+
return { code, map: typeof map === 'string' ? map : '{}' };
148168
}
149169

150-
return { code: awaitedResult.code, map: awaitedResult.map };
170+
return null;
151171
}
152172

153173
function createDebugIdModule(debugId: string): Module<VirtualJSOutput> & { setSource: (code: string) => void } {

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,32 @@ describe('Sentry Metro Serializer', () => {
111111
}
112112
});
113113

114+
test('does not crash when wrapped serializer returns an array (Expo static export)', async () => {
115+
// Expo's Metro serializer returns a non-standard output (e.g. an array of serial assets)
116+
// when doing a static/EAS Update export. The array must not be mistaken for a { code, map }
117+
// bundle. Note `'map' in []` is `true` because of Array.prototype.map, which used to make the
118+
// serializer return `{ code: undefined }` and crash in determineDebugIdFromBundleSource.
119+
// https://github.com/getsentry/sentry-react-native/issues/6650
120+
const serialAssets = [{ filename: 'index.js', source: 'console.log("a");' }];
121+
const customSerializer = (() => serialAssets) as unknown as MetroSerializer;
122+
123+
const serializer = createSentryMetroSerializer(customSerializer);
124+
const bundle = await serializer(...mockMinSerializerArgs());
125+
126+
// The original non-standard result is returned untouched (no debug ID injection, no crash).
127+
expect(bundle).toBe(serialAssets);
128+
});
129+
130+
test('does not crash when wrapped serializer returns a promise resolving to an array', async () => {
131+
const serialAssets = [{ filename: 'index.js', source: 'console.log("a");' }];
132+
const customSerializer = (() => Promise.resolve(serialAssets)) as unknown as MetroSerializer;
133+
134+
const serializer = createSentryMetroSerializer(customSerializer);
135+
const bundle = await serializer(...mockMinSerializerArgs());
136+
137+
expect(bundle).toBe(serialAssets);
138+
});
139+
114140
describe('calculateDebugId', () => {
115141
// We need to access the private function for testing
116142
const crypto = require('crypto');

0 commit comments

Comments
 (0)