feat(symbols): upload Java/Kotlin sources with R8 mappings (--include-sources)#758
feat(symbols): upload Java/Kotlin sources with R8 mappings (--include-sources)#758abelonogov-ld wants to merge 13 commits into
Conversation
Rename the symbol-map package/format from ldsm to dsymmap (magic DSMP), append the .dsymmap extension to the apple upload key, and accept apple/ios/dsym aliases for the --type flag. Co-authored-by: Cursor <cursoragent@cursor.com>
Parse Flutter ELF app.<arch>.symbols files (debug/elf + debug/dwarf), extract the Dart build id (.note.gnu.build-id) and DWARF, and compile them into the compact .dartmap symbol map (dsymmap codec). Upload to the Symbols Id lane (keyed by build id) and the Version lane (version + platform), and support local generation via `symbols generate`. Co-authored-by: Cursor <cursoragent@cursor.com>
Parse Flutter ELF app.<arch>.symbols files (debug/elf + debug/dwarf), extract the Dart build id (.note.gnu.build-id) and DWARF, and compile them into the compact .dartmap symbol map (dsymmap codec). Upload to the Symbols Id lane (keyed by build id) and the Version lane (version + platform), and support local generation via `symbols generate`. Co-authored-by: Cursor <cursoragent@cursor.com>
…y/ldcli into andrey/flutter-symbols * 'andrey/flutter-symbols' of ssh://github.com/launchdarkly/ldcli: feat(symbols): add Flutter (Dart AOT) symbol upload (--type flutter) panic fix fix demagling refactor(symbols): rename ldsm to dsymmap + apple type aliases apple dsym ingest
* andrey/flutter-symbols: fix
…-sources) Extends --include-sources to --type android, so a retraced Android frame can show the code around it instead of only class/method/line. The bundle is uploaded as sources.srcbundle beside mapping.txt, on the same lane, so sources are matched to a build exactly as the mapping is. Android needs a different approach than Apple. A dSYM's DWARF names every source file it covers, so nothing has to be searched for; an R8 mapping records no paths at all -- only (class, method, line) -- so ldcli has to scan for sources and pick a key the backend can reconstruct from a retraced class name. That key is the package-relative path (com/example/MainActivity.kt), taken from each file's own 'package' declaration rather than from its directory, because Kotlin does not require the two to agree. --source-path says where to scan, defaulting to the current directory: --path points at the mapping.txt output directory, which holds no sources. build/, .gradle/, .git/, .idea/ and node_modules/ are skipped, along with files over 2 MiB and bundles over 64 MiB, so an accidental scan from a repo root doesn't ship generated or vendored code. Off by default -- it stores your source in LaunchDarkly -- and a scan that finds nothing prints a note and uploads the mapping alone, so the flag cannot break an upload that would otherwise succeed. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cc54a1c. Configure here.
| if strings.HasPrefix(line, "import ") { | ||
| return "" // past the package slot | ||
| } | ||
| return "" // a real declaration: this file has no package |
There was a problem hiding this comment.
License blocks hide package below
High Severity
javaPackageOf treats any non-package line that is not skipped as proof the file has no package and returns immediately. Text inside multi-line block comments (without a leading * on each line) and lines that start with /* but also contain a package clause on the same line never reach the real package statement, so bundle keys fall back to bare filenames and no longer match what the backend builds from retraced class names.
Reviewed by Cursor Bugbot for commit cc54a1c. Configure here.
| return nil | ||
| } | ||
| total += len(data) | ||
| b.Add(androidSourceKey(data, d.Name()), data) |
There was a problem hiding this comment.
Duplicate keys inflate bundle budget
Low Severity
When two scanned files produce the same bundle key, srcbundle.Builder.Add keeps the first entry but total still increases by both file sizes. Later files can be dropped because the walk thinks the bundle cap is reached even though only one copy of the duplicate key is stored.
Reviewed by Cursor Bugbot for commit cc54a1c. Configure here.


Extends
--include-sourcesto--type android, so a retraced Android frame can show the code around it instead of only class/method/line. Stacks on #757 (Apple sources).Why Android can't reuse the Apple approach
A dSYM's DWARF names every source file it covers, so
ldclinever has to search for anything. An R8 mapping records no paths at all — only(class, method, line). So Android has to scan for sources and choose a key the backend can reconstruct from a retraced class name.That key is the package-relative path (
com/example/MainActivity.kt), taken from each file's ownpackagedeclaration rather than from its directory, because Kotlin does not require the two to agree. The backend then rebuilds it from the retraced class: package as the directory, top-level class as the file name.--source-pathNew flag saying where to scan, defaulting to the current directory —
--pathpoints at themapping.txtoutput directory, which holds no sources.Because the default can be a whole repo root, the scan skips
build/(R8's own generated code),.gradle/,.git/,.idea/andnode_modules/, plus files over 2 MiB and bundles over 64 MiB. Pointing--source-pathatapp/src/mainis faster and avoids bundling test or sample code.Safety and opt-in
Off by default, because it ships your source to LaunchDarkly. A scan that finds nothing prints a note and uploads the mapping alone, so the flag can't break an upload that would otherwise succeed.
Testing
./cmd/symbols/...passes. New tests cover package extraction across the shapes real files take (Java semicolons, Kotlin without, license headers,@file:annotations, trailing comments, default package, andimportbefore any package), keying by declared package rather than directory layout, skipping build output andnode_modules, oversize files, an empty scan yielding no bundle rather than an error, and rejecting a non-directory source path.Made with Cursor