skip to content

A crash report from an obfuscated Flutter sports-betting app shows only addresses; how do you turn it into a readable Dart stack trace?

level: seniorimportance: should knowfreq 35%

answer

  1. same build, same architecture
  2. flutter symbolize -i and -d
  3. app.android-arm64.symbols
  4. file by version and build number
  5. lost symbols are not recoverable

basics

~10 s

Save the trace to a file, find the symbols file from that exact build and architecture, and run flutter symbolize -i trace.txt -d app.android-arm64.symbols; without the matching file the trace cannot be decoded.

solid answer

~40 s

An app built with `--obfuscate --split-debug-info` prints Dart stack traces as addresses. To read one: take the crash's app version and build number to find the release, pick the symbols file for the device's architecture from that release's `--split-debug-info` directory (for example `app.android-arm64.symbols` or `app.ios-arm64.symbols`), and run `flutter symbolize -i crash.txt -d <symbols file>`, optionally with `-o` to write the result to a file. The command also accepts a `.dSYM` bundle and, for deferred components, `-u <unit id>:<file>` per loading unit. The file must come from the same build: symbols from a rebuild are not guaranteed to match, which is why they are archived as release artefacts.

code

bash · 7 lines
bash
flutter build appbundle --obfuscate --split-debug-info=build/symbols/4.2.0+311
# archive build/symbols/4.2.0+311 with the release

flutter symbolize \
  -i crash_311.txt \
  -d build/symbols/4.2.0+311/app.android-arm64.symbols \
  -o crash_311.readable.txt

go deeper

for a junior

Recall that an obfuscated trace needs flutter symbolize with -i for the trace file and -d for the symbols file.

for a middle

Explain matching by build and architecture, the per-architecture file names, and the -u option for deferred components.

for a senior

Run it as a process: per-release symbol archives keyed by version and build number, one build number across stores, and a rehearsed decoding path.

for a principal

Decide retention and access for symbol archives across many releases, and when to rely on a crash service's upload instead of manual symbolization.

## The situation A sports-betting app is released with `--obfuscate --split-debug-info=build/symbols/4.2.0+311`. On a busy match day a user's bet slip crashes, and the report shows a Dart trace made of addresses instead of method names. Nothing is wrong with the report: that is what an obfuscated, split-debug-info build prints. The names live in the `.symbols` files on the build side. ## Step by step 1. **Identify the build.** From the crash report take the app's build name and build number, here `4.2.0+311`, and the device's CPU architecture, such as arm64. 2. **Find the matching symbols.** In that release's archived `--split-debug-info` directory, pick the file for the architecture: `app.android-arm64.symbols` for a 64-bit Android device, `app.ios-arm64.symbols` for an iPhone. 3. **Save the trace** exactly as reported, including its header lines, to a text file. 4. **Run the tool:** ```bash flutter symbolize -i crash_311.txt -d symbols/4.2.0+311/app.android-arm64.symbols -o crash_311.readable.txt ``` 5. **Read the result**: the output replaces addresses with Dart function names, files and lines. ## flutter symbolize options | Option | Meaning | |---|---| | `-i`, `--input` | file containing the Dart stack trace; stdin if omitted | | `-d`, `--debug-info` | symbols file from `--split-debug-info`, or a `.dSYM` bundle | | `-u`, `--unit-id-debug-info` | `<unit id>:<file>` for a deferred-component loading unit, repeatable | | `-o`, `--output` | where to write the result; stdout if omitted | The tool refuses to run without either `--debug-info` or `--unit-id-debug-info`, and fails if it cannot find symbols for the root loading unit. ## Why the match must be exact - Each architecture gets its own file, so an arm64 trace needs the arm64 file. - The symbols describe one compiled binary. A rebuild of the same commit is **not guaranteed** to produce a matching file, so reproducing the build later is not a dependable recovery plan. - The docs say it directly: back up the symbols file, because you need it to de-obfuscate a stack trace if the original is lost. ## Making it routine - **Name the directory by version and build number**, such as `symbols/4.2.0+311`, so the crash's version points straight at its files. - **Store symbols with the release artefacts**, not on a CI runner's disk that is wiped after the job. - **Keep one build number across both stores** so a crash report's build number identifies one set of symbols per platform. - **Rehearse it**: symbolize a deliberately thrown test exception from a release build before a real incident needs it. - Crash-reporting services can symbolize automatically if you upload the same symbols; that upload is a separate concern from the local command. ## What a decoded trace tells you, and what it does not Once symbolized, the trace reads like a debug-mode trace: Dart function names, library URIs and line numbers. For the bet-slip crash it might point to a `null` check failing inside the widget that renders live odds when a market is suspended mid-update. That is enough to write a regression test and a fix. What the trace does not give you: - **State.** It shows where the code failed, not which market, odds or user input caused it; logs and breadcrumbs attached to the report supply that. - **Renamed values in logs.** If the app logged a type name at runtime, that log line still shows the obfuscated name; decode it with the saved obfuscation map, or stop logging type names. ## Limits to know - **Windows x64** builds produce a PDB instead of a `.symbols` file; `flutter symbolize` cannot read it, so a Windows debugger is used. - The command symbolizes **Dart** frames. Native frames from the engine or plugins come from the platform's own crash tooling. - An **obfuscation map** saved with `--extra-gen-snapshot-options=--save-obfuscation-map=<file>` helps decode an individual renamed identifier, for example in a log line, but it is not a substitute for the symbols file when reading a trace.

  • The symbols for build 311 were on a CI runner that has since been wiped; can you rebuild and symbolize?
    Not reliably. The symbols describe one compiled binary, and a rebuild of the same commit is not guaranteed to match. Some frames may decode wrongly or not at all. Archive symbols with each release so this cannot recur.
  • How does flutter symbolize handle an app that uses deferred components?
    Each loading unit has its own symbols file. Pass the root unit with `-d` and each deferred unit with `-u <unit id>:<path>`, repeated per unit. The tool rejects two different paths for the same unit ID.

The symbols file is the key to one specific cipher sheet: it decodes only messages written with that sheet, and making a new sheet from the same instructions does not reliably give you the same key.

saying these in an interview costs you the question

  • Any symbols file from the same version works, whatever the architecture.
  • Rebuilding the same commit later always recreates identical symbols.
  • flutter symbolize can decode the trace without a symbols file.
  • The symbols file is shipped inside the app bundle for later use.
  • flutter symbolize also resolves native engine and plugin frames.