Skip to content

feat(symbols): upload Java/Kotlin sources with R8 mappings (--include-sources)#758

Open
abelonogov-ld wants to merge 13 commits into
mainfrom
andrey/android-source
Open

feat(symbols): upload Java/Kotlin sources with R8 mappings (--include-sources)#758
abelonogov-ld wants to merge 13 commits into
mainfrom
andrey/android-source

Conversation

@abelonogov-ld

Copy link
Copy Markdown
Contributor

Extends --include-sources to --type android, so a retraced Android frame can show the code around it instead of only class/method/line. Stacks on #757 (Apple sources).

_sym/android/id/<symbolsID>/mapping.txt         # existing
_sym/android/id/<symbolsID>/sources.srcbundle   # new, same lane

Why Android can't reuse the Apple approach

A dSYM's DWARF names every source file it covers, so ldcli never 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 own package declaration 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-path

New flag saying where to scan, defaulting to the current directory — --path points at the mapping.txt output 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/ and node_modules/, plus files over 2 MiB and bundles over 64 MiB. Pointing --source-path at app/src/main is 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, and import before any package), keying by declared package rather than directory layout, skipping build output and node_modules, oversize files, an empty scan yielding no bundle rather than an error, and rejecting a non-directory source path.

Made with Cursor

abelonogov-ld and others added 13 commits July 21, 2026 19:16
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
…-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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cc54a1c. Configure here.

return nil
}
total += len(data)
b.Add(androidSourceKey(data, d.Name()), data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cc54a1c. Configure here.

@abelonogov-ld
abelonogov-ld changed the base branch from andrey/apple-source to main July 25, 2026 01:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant