Crashlytics shows unreadable Dart stack traces, or a Missing dSYM alert, for an obfuscated Flutter release; what symbol uploads are missing on iOS and Android?
answer
- split-debug-info strips Dart symbols
- build_id matches report to symbols
- iOS: upload-symbols run script, last phase
- Android: firebase crashlytics:symbols:upload
- upload before the crash, per build
basics
~20 sBuilds with --split-debug-info (and --obfuscate) need their symbols in Crashlytics: on iOS the firebase_crashlytics upload-symbols run script must exist as the last build phase; on Android run firebase crashlytics:symbols:upload with the Firebase App ID and the symbols directory.
solid answer
~50 sWhen a release is built with `--split-debug-info`, optionally with `--obfuscate`, the Dart symbol information leaves the binary. Crash frames then arrive as addresses tagged with a `build_id`, which firebase_crashlytics extracts from the stack trace so the backend can match symbols. On Apple platforms, symbols travel as dSYMs. The plugin tries to add a run script called `[firebase_crashlytics] Crashlytics Upload Symbols` to the Runner target. It must be the **last** build phase, and with Flutter 3.12+ and plugin 3.3.4+ it uploads the Flutter dSYMs too. If it is missing, the console shows **Missing dSYM** and holds events until the files arrive. On Android, run `firebase crashlytics:symbols:upload --app=<Firebase Android App ID> <split-debug-info dir>` with Firebase CLI 11.9.0+, **before** crashes from that build are reported. The App ID is not the package name. Native Android obfuscation mappings go up through the Crashlytics Gradle plugin.
code
bash · 9 lines# Build an obfuscated Android release with separate Dart symbols
flutter build appbundle --obfuscate --split-debug-info=build/symbols
# Upload the Dart symbols before crashes from this build arrive
# --app is the Firebase Android App ID (mobilesdk_app_id), not the package name
firebase crashlytics:symbols:upload --app=1:567383003300:android:17104a2ced0c9b9b build/symbols
# iOS: the Xcode run script uploads dSYMs during the build
flutter build ipa --obfuscate --split-debug-info=build/symbolsgo deeper
Know that obfuscated or split-debug-info releases need their symbols uploaded, or Crashlytics shows unreadable Dart frames.
Explain the iOS run script and why it must be last, the Android CLI upload with the Firebase App ID and the symbols directory, and what build_id does.
Show a CI pipeline that uploads symbols from the same job that builds, archives them per version, and verifies readability with a forced release-mode report.
Treat symbol management as release infrastructure: retention, access to archives, and a gate that blocks a rollout when symbols for a build are missing.
## Why traces become unreadable A Flutter release compiles Dart ahead of time into machine code. Two build options, covered in detail in the release-versioning topic, change what a crash report can show: - `--split-debug-info=<dir>` writes the Dart debug symbols to a directory instead of the binary. Stack traces become "non-symbolic": addresses plus a `build_id`, with no function names. - `--obfuscate`, which requires `--split-debug-info`, also renames identifiers. **firebase_crashlytics** extracts the `build_id`, and any deferred-component `loading_unit` lines, from such traces and sends them with each report. The backend can only turn addresses back into names if it holds the symbols for that exact `build_id`. Uploading them is therefore part of shipping a release. ## iOS and macOS: dSYMs through a run script 1. Adding the plugin, and running `flutterfire configure`, tries to add a run script named **`[firebase_crashlytics] Crashlytics Upload Symbols`** to the Runner target's Build Phases. The CocoaPods integration does this through the plugin's podspec. 2. Open `ios/Runner.xcworkspace` and check that it exists. If not, add a Run Script phase that calls `$PODS_ROOT/FirebaseCrashlytics/upload-symbols` with your Firebase Apple App ID. The Crashlytics guide gives the exact command. 3. It must be the **last** build phase, or Crashlytics cannot process the dSYMs. 4. With **Flutter 3.12.0+ and plugin 3.3.4+**, that setup also generates and uploads the Flutter `App.framework` dSYMs when you use `--split-debug-info`. Without them, the console shows a **Missing dSYM** alert, and the guide says exceptions are **held** until the missing files are uploaded. ## Android: upload with the Firebase CLI The Dart symbols from `--split-debug-info` are not uploaded by the Gradle build. You upload them yourself: ```bash firebase crashlytics:symbols:upload --app=1:567383003300:android:17104a2ced0c9b9b build/symbols ``` - `--app` takes the **Firebase Android App ID**: `mobilesdk_app_id` in `google-services.json`, or the value in Project settings. It is not the package name. - The path is the **same directory** you passed to `--split-debug-info`. - The guide requires Firebase CLI **11.9.0+** and says to upload **before** reporting a crash from that obfuscated build. - Separately, native Java/Kotlin code shrunk by R8 needs its mapping file. The Crashlytics Gradle plugin handles that; `flutterfire configure` tries to add it. ## Making it reliable in CI | Step | iOS | Android | |---|---|---| | Build | `flutter build ipa --obfuscate --split-debug-info=build/symbols` | `flutter build appbundle --obfuscate --split-debug-info=build/symbols` | | Upload | run script in the Xcode build | `firebase crashlytics:symbols:upload --app=... build/symbols` | | Keep | archive the dSYMs | archive `build/symbols` per version | - Upload in the **same pipeline job** that builds the release, so every shipped `build_id` has symbols. - **Archive** the symbols directory for each version. If an upload fails, you can re-upload later, but only if you still have the exact files. - For a quick check, record a Dart error from a release build with `recordError(e, st, fatal: true)` and confirm the trace shows function names. The plugin's `crash()` forces a native crash without a stack trace, so it cannot verify Dart symbols. ## Quick diagnosis - Readable native frames, unreadable Dart frames: the Dart symbols for that `build_id` were never uploaded. - "Missing dSYM" on iOS: the run script is missing or not last, or the dSYM upload failed. - Android traces obfuscated only in Java/Kotlin frames: the mapping-file upload is missing from the Gradle setup.
- The symbols upload for version 4.2.0 failed and crashes are already coming in. Can you recover?Yes, if you archived that build's symbols directory, or its dSYMs on iOS. Upload them for the same Firebase App ID. Matching is by `build_id`, so the files must come from the exact build that shipped. Rebuilding the same source produces a different `build_id` and cannot be used.
- Why does the iOS run script have to be the last build phase?It uploads the dSYMs the build has just produced, including the Flutter `App.framework` dSYM. If it runs before those are generated, there is nothing complete to upload. The Crashlytics guide states that Crashlytics cannot properly process dSYMs otherwise.
saying these in an interview costs you the question
- Crashlytics deobfuscates Dart traces without any uploaded symbols.
- The --app argument takes the Android package name.
- Symbols can be regenerated later by rebuilding the same commit.
- The Gradle build uploads the split-debug-info directory automatically.
- The dSYM upload script can sit anywhere in the build phases.