Dart
For Dart, the debug info is the build's .symbols file. Obfuscated Flutter release builds strip names and line tables out of the binary, so a crash reports against raw machine code. The frames carry nothing but an offset into a snapshot section:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
pid: 1234, tid: 5678, name 1.ui
build_id: '8a5f0c2e1b7d4f93a6e0c8b1d2f3a4b5'
os: android arch: arm64 comp: yes sim: no
#00 abs 7f1c2a0040 virt 0000000000241040 _kDartIsolateSnapshotInstructions+0x12340
#01 abs 7f1c2a0a10 virt 0000000000241a10 _kDartIsolateSnapshotInstructions+0x12d10With the build's .symbols file uploaded, that resolves into named source frames, inline calls expanded:
#0 UserRepository.fetch (package:app/data/user_repository.dart:88:14)
#1 _ProfilePageState.build.<anonymous closure> (package:app/ui/profile_page.dart:142:20)
#2 StatelessElement.build (package:flutter/src/widgets/framework.dart:5678:28)This page covers the Dart-specific mechanics in the order you meet them: when a symbol upload is needed, how to upload, and then how a non-symbolic trace is recognized and resolved into source frames. For when symbolication runs, the cache, and the .tw format, see Architecture.
Obfuscated builds and .symbols files
A plain flutter build --release keeps enough symbol information that crash traces are already readable (function names and file:line), and they are reported as-is. You only need to upload symbols when you harden a build with --obfuscate and/or --split-debug-info:
--obfuscaterenames identifiers and strips names out of the binary, which shrinks the app and protects your source. Release traces then arrive as bare instruction offsets.--split-debug-info=<dir>writes the stripped debug information into a separate.symbolsfile per architecture, so it ships to you instead of inside the app.
flutter build apk --release --obfuscate --split-debug-info=build/symbolsThis writes ELF files like app.android-arm64.symbols, one per architecture you ship (arm64, arm, x64). Each is a stripped ELF carrying DWARF debug info and a .note.gnu.build-id note, plus the _kDartIsolateSnapshotInstructions and _kDartVmSnapshotInstructions symbols that anchor the snapshot sections. The engine reads the build ID from that note and the section base addresses from those symbols; that's everything a lookup needs.
Apple platforms are the exception: Mach-O builds carry no GNU build-id note, so for iOS and macOS the debug ID is the build's Mach-O UUID, which you pass explicitly at upload time.
Uploading .symbols files
Uploads go to the API host, authenticated with your project's Upload Token. Open the project's Connection page in the dashboard to copy it (generate one if the project has none). It is a separate credential from the ingest token your app runs with, and it is the only credential this endpoint accepts.
POST /api/symbols/upload
Authorization: Bearer <upload token>
Content-Type: multipart/form-dataOn Android the filename already carries the architecture and the file carries its own build ID, so the whole upload is one call with no extra fields:
curl -H "Authorization: Bearer $TRACEPATH_UPLOAD_TOKEN" \
-F "files=@build/symbols/app.android-arm64.symbols" \
-F "files=@build/symbols/app.android-arm.symbols" \
-F "files=@build/symbols/app.android-x64.symbols" \
https://app.tracepath.dev/api/symbols/upload| Field | Required | Notes |
|---|---|---|
files | yes | One or more .symbols files. Entries that are not a recognized symbol format are ignored. 200 MB per file. |
arch | sometimes | The architecture token (arm64, arm, x64, ia32). Optional when the filename ends in -<arch>.symbols, which Flutter's output already does. Applies to every file in the request, so send one architecture per request when you pass it explicitly. |
debug_id | sometimes | Required only when the file has no build-id note (Apple). When the file does have one, this must match it if provided. |
Apple builds need the debug_id field, because their .symbols file carries no build-id note. The value is the Mach-O UUID of the Dart snapshot inside the built app — the same value the runtime prints as build_id: — so read it on the build machine with dwarfdump and pass it through:
dwarfdump --uuid build/macos/Build/Products/Release/YourApp.app/Contents/Frameworks/App.framework/App
# UUID: 8A5F0C2E-1B7D-4F93-A6E0-C8B1D2F3A4B5 (arm64) .../App.framework/App
curl -H "Authorization: Bearer $TRACEPATH_UPLOAD_TOKEN" \
-F "files=@build/symbols/app.darwin-arm64.symbols" \
-F "debug_id=8A5F0C2E-1B7D-4F93-A6E0-C8B1D2F3A4B5" \
https://app.tracepath.dev/api/symbols/uploadPaste the UUID exactly as dwarfdump prints it; case and dashes are normalized server-side. The iOS path is the same with the app under build/ios/. No arch field is needed as long as the filename still ends in -<arch>.symbols.
The build ID is normalized (lowercased, hex digits only, so dashes are optional) and the arch too (x86_64/amd64 collapse to x64, aarch64 to arm64). The stored key is:
dartsymbols/{projectId}/{debugId}-{arch}.symbolsBecause the key is content-addressed by build ID, releases never collide, and re-uploading the same build is idempotent. Symbols are unique to each build, so upload on every release: a crash can only be symbolicated against the exact build it came from.
The response reports how many files were stored:
{ "uploaded": 3 }An unrecognized file is skipped without an error, so check that count against what you sent. Failure modes worth handling in CI:
| Status | When |
|---|---|
401 | Missing Authorization header, or a token that is not an upload token for a live project. |
400 | No files field, a single file over 200 MB, an unknown arch, or a file with no build-id note and no debug_id. |
413 | The whole request exceeds 250 MB. Split the batch. |
422 | The file is not a readable Dart .symbols file, the supplied debug_id contradicts the file's own build-id note, or the request carries too many parts (the limit is around a thousand). |
503 | Too many uploads in flight at once. The request waited for a slot and gave up; retry it. |
A dart run uploader that discovers build/symbols, derives each file's architecture and debug ID, and posts them ships with the TracePath Flutter SDK, which is not published yet. Until it is, the calls above are the whole contract — run them from your release job.
How a Dart trace is recognized
Dart symbolication runs on the /api/report ingest path, for projects created with the Flutter framework, and keys off a non-symbolic trace, identified by frame lines of this shape:
#00 abs 7f1c2a0040 virt 0000000000241040 _kDartIsolateSnapshotInstructions+0x12340Each frame names a snapshot section, _kDartIsolateSnapshotInstructions (the app's own code) or _kDartVmSnapshotInstructions (the Dart VM), and an offset into it. The parser pulls three things from the surrounding text:
| Field | Source line | Used for |
|---|---|---|
| build ID | build_id: '...' | matching the trace to an uploaded .symbols |
| architecture | os: ... arch: ... | picking the right per-arch .symbols |
| frames | #N ... _kDart...Instructions+0xOFFSET | the PC offsets to resolve |
A trace that is already symbolic, or carries no recognizable Dart frames, passes through untouched.
How an offset resolves
The build's .symbols is compiled into a range-lookup resolver (the .tw form). Each frame then resolves through these steps:
- Compute the program counter. The frame's offset is added to the base address of its section (isolate or VM), recovered from the snapshot instruction symbols. That absolute PC is what the lookup keys on.
- Look up the PC. A binary search over the resolver's range table finds the PC's entry and its span of frames. DWARF inline information means one machine address can stand for several source frames, so a single
#Noffset can expand into a chain of inlined calls, innermost first. - Render. Resolved frames print as
#N Function (file:line:col). A frame with no match (a PC outside any known range, or symbols that weren't uploaded) keeps its raw form,#N _kDartIsolateSnapshotInstructions+0xOFFSET, so the trace is never worse than it arrived. A maximum of 50 frames are emitted, inline expansions included.
If no symbols are found for the build, every frame falls back to its raw offset form.
The flat format
Where the JavaScript .tw maps generated positions to original positions, the Dart .tw maps PC ranges to inline frame spans. Building it flattens the DWARF debug info into a structure tuned for the lookup above:
- A header carrying the isolate and VM base addresses and the table sizes.
- A range table, sorted by PC, where each entry covers a contiguous PC range and points at a run of frames (more than one when calls were inlined).
- A frame table of
(file, line, column, function)records, with file and function names interned.
The general properties (memory-mapped, zero-copy on little-endian hosts, a versioned cache artifact rebuilt from the original debug info) are the same as every .tw; see Architecture.
How it differs from JavaScript
| JavaScript | Dart | |
|---|---|---|
| Raw artifact | source map + minified bundle | .symbols ELF (DWARF) |
| Frame input | file, line, column | PC offset into a snapshot section |
| Keyed by | filename or debug ID | build ID + architecture |
| Function names | bundle scope analysis | DWARF, directly |
| One frame in | one frame out | one or more frames out (inline expansion) |
The architecture dimension is the practical difference to remember: a single JavaScript build has one map per bundle, but a single Flutter build ships several .symbols files, and a trace only resolves against the one matching the device's arch. Upload all of them.
Related pages
- Flutter: reporting exceptions from a Flutter app
- Architecture: when symbolication runs, the cache tiers, and the
.twformat - Performance: what happens when an artifact is missing, and how to tell