Back to blog
Article

Crash reports that point at your source, not minified noise

Crash reports that point at your source, not minified noise
S

StriveBit

4 min readMobile Development

Crash reports that point at your source, not minified noise

A field-services app we maintain for a facilities client in Noida crashed on a Vivo Y53s last month. The report from Sentry showed `at f (main.bundle:1:48291)`. Line 1, column 48291 — the entire bundle on one line. The stack trace named a function called `f` and an anonymous component. Useless.

The crash was in a form-validation hook we had written three weeks earlier. We could not know that from the report. We had source maps in the build output but had not uploaded them. The fix is not hard, but it has to be wired into the build or it drifts.

React Native ships JavaScript to the device as a single minified bundle. In dev mode the bundle is readable. In production, Metro minifies it — variables become single letters, modules collapse, everything lands on line 1. A crash report from the device gives you the minified position. A source map maps that position back to the original TypeScript file and line.

Sentry does the mapping server-side. You upload the source map at release time; when a crash arrives, Sentry looks up the release, finds the map, translates the stack frame. The key is tying the upload to a release identifier that the SDK reports at runtime.

Generate a release identifier that is stable and unique per build. We use the git short hash:

export SENTRY_RELEASE=$(git rev-parse --short HEAD)

Pass it to the Sentry CLI when uploading, and set it in the SDK config so crash reports are tagged with the same value:

import * as Sentry from '@sentry/react-native';

Sentry.init({
  dsn: 'YOUR_DSN',
  release: process.env.SENTRY_RELEASE,
  environment: __DEV__ ? 'development' : 'production',
});

The upload step runs after the bundle is built but before the store upload. In a CI pipeline, that means after `npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle` and the same for iOS. Then:

sentry-cli releases files $SENTRY_RELEASE upload-sourcemaps \
  --platform android \
  --dist $(cd android && ./gradlew -q app:printBuildVariant) \
  index.android.bundle.map

One detail that trips us up every few months: Metro writes the source map to a path that depends on the bundler version and the platform. On RN 0.73+, the Android map lands at `android/app/build/generated/assets/react/release/index.android.bundle.map` during the Gradle build, not next to the bundle you created manually. If you upload the wrong file, Sentry accepts it and the stack traces stay broken. Verify by opening the map file and checking that it contains your original filenames — not just module IDs.

iOS is the same pattern but the paths differ. The map comes out of the Xcode build under `~/Library/Developer/Xcode/DerivedData/.../Build/Products/Release-iphoneos/main.jsbundle.map`. We upload it with `--platform ios` and no `--dist` flag.

There is a tradeoff in how much you minify. Metro's default minifier (Terser) renames functions and removes comments. You can disable function mangling to keep readable names in the bundle at the cost of a larger file — usually not worth it if your source maps are correct, since the maps give you the original names anyway. Keep mangling on, keep the bundle small, and rely on the maps.

One thing worth doing: set `SENTRY_RELEASE` in the native build at compile time, not just in the JS config. Android crashes that happen before the JS bundle loads — a native bridge failure, a missing native module — will use the release tag from the Gradle config, not from `Sentry.init`. Add the release name to `android/app/build.gradle` as a `manifestPlaceholders` value or a `resValue` so native crashes group under the same release.

We check the setup by deliberately throwing an error in a debug build, shipping a TestFlight and internal-track build, triggering the error, and confirming the report shows the original filename and line number. If it shows `main.bundle:1`, the upload did not match the release or the map file was wrong. The test takes ten minutes and catches the failure before a field worker hits it on a site visit.

The Vivo crash, once we uploaded the maps for that build, resolved to a null check we had skipped in a date-parsing helper. The report showed `parseJobDate` at `src/utils/dates.ts:47`. We shipped the fix in the next build and uploaded maps for that build too.

Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch