Setting Up Source Map Upload for React Native Crash Deobfuscation
In Firebase Crashlytics, a crash appears something like this:
Fatal Exception: com.facebook.react.common.JavascriptException
[email protected]:1:92847
[email protected]:1:15234
Line 1:92847 in a minified bundle. Without source maps, it's impossible to understand which file and function the crash occurred in. Source map mapping transforms this stack trace into something readable: onButtonPress @ src/screens/PaymentScreen.tsx:147:23.
How Deobfuscation Works
React Native production bundles minify and combine all JS code into a single file (index.android.bundle / main.jsbundle). In parallel, a source map is generated (index.android.bundle.map) — a lookup table between positions in the bundle and the original source code.
Crashlytics and Sentry accept these source maps and store them on their servers. When a crash report arrives, they automatically apply the mapping and display the original stack trace.
Generating Source Maps
# Android
react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output android/app/src/main/assets/index.android.bundle \
--sourcemap-output android/app/src/main/assets/index.android.bundle.map
# iOS
react-native bundle \
--platform ios \
--dev false \
--entry-file index.js \
--bundle-output ios/main.jsbundle \
--sourcemap-output ios/main.jsbundle.map
With the standard ./gradlew bundleRelease, source maps are generated automatically in app/build/generated/sourcemaps/react/release/. However, there's a catch: with hermes compilation, you need a composite source map — hermes creates a second level of mapping (bytecode → JS bundle) that must be composed with the first (JS bundle → TypeScript).
Hermes and Composite Source Maps
For projects using Hermes (enabled by default since RN 0.70+):
# Compose source maps: hermes-engine + JS bundle
node_modules/react-native/scripts/compose-source-maps.js \
android/app/build/generated/sourcemaps/react/release/index.android.bundle.packager.map \
android/app/build/generated/sourcemaps/react/release/index.android.bundle.compiler.map \
-o android/app/build/generated/sourcemaps/react/release/index.android.bundle.map
Without this step, Firebase Crashlytics will show a stack trace referencing lines in the JS bundle, not the original TypeScript source files.
Uploading to Firebase Crashlytics
# Install Firebase CLI
npm install -g firebase-tools
# Upload source map
firebase crashlytics:mappingfile:upload \
--app "$FIREBASE_APP_ID" \
android/app/build/generated/sourcemaps/react/release/index.android.bundle.map
In CI, this is a step after building:
- name: Upload Source Maps to Crashlytics
run: |
# Compose Hermes source maps
node node_modules/react-native/scripts/compose-source-maps.js \
android/app/build/generated/sourcemaps/react/release/index.android.bundle.packager.map \
android/app/build/generated/sourcemaps/react/release/index.android.bundle.compiler.map \
-o /tmp/composed.map
firebase crashlytics:mappingfile:upload \
--app "$FIREBASE_APP_ID_ANDROID" \
/tmp/composed.map
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
Uploading to Sentry
Sentry uses the sentry-cli CLI:
npm install -g @sentry/cli
sentry-cli releases new "$RELEASE_VERSION"
sentry-cli sourcemaps upload \
--org "$SENTRY_ORG" \
--project "$SENTRY_PROJECT" \
--release "$RELEASE_VERSION" \
android/app/build/generated/sourcemaps/react/release/
In Sentry, you need to pass release to the SDK:
Sentry.init({
dsn: Config.SENTRY_DSN,
release: `${DeviceInfo.getBundleId()}@${DeviceInfo.getVersion()}+${DeviceInfo.getBuildNumber()}`,
});
The version must match what's passed during source map upload.
Version Management Issue
A common mistake: source maps are uploaded for one version (1.2.3), but the SDK uses a different one (1.2.3+42). Crashes don't get deobfuscated. You need to fix a single version format and use it in both places — a CI environment variable like VERSION=$APP_VERSION+$BUILD_NUMBER.
Process
Check Hermes enablement → configure source map generation in the build pipeline → create a compose script for Hermes → add upload step to CI → test deobfuscation with a test crash → document version format standards.
Timeline: 4 hours to 2 days depending on current CI setup and Hermes configuration. Cost is calculated individually.







