Skip to content

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

Merged
abelonogov-ld merged 22 commits into
mainfrom
andrey/android-source
Jul 28, 2026
Merged

feat(symbols): upload Java/Kotlin sources with R8 mappings (--include-sources)#758
abelonogov-ld merged 22 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>
Comment thread cmd/symbols/android_sources.go
Comment thread cmd/symbols/android_sources.go Outdated
@abelonogov-ld
abelonogov-ld changed the base branch from andrey/apple-source to main July 25, 2026 01:09
abelonogov-ld added a commit to launchdarkly/observability-sdk that referenced this pull request Jul 27, 2026
Documents the `--include-sources` / `--source-path` flags added in
launchdarkly/ldcli#758, which upload your `.java`/`.kt` files so the
errors page can show the code around each retraced frame.

Explains why Android needs `--source-path` at all (an R8 mapping records
no file paths, unlike a dSYM's DWARF), how files are keyed by their
**declared package** so a Kotlin file in a directory that doesn't mirror
its package still resolves, the skipped directories and size limits, and
that the flag is off by default because it stores your source in
LaunchDarkly.

Also notes the side benefit: R8's `SourceFile` placeholder forces the
backend to guess `.java` for every frame, and a resolved source replaces
that guess with the real path — so Kotlin frames finally show `.kt`.

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
abelonogov-ld and others added 4 commits July 27, 2026 16:22
A merge while stacking on the Flutter branch left a second, byte-identical
typeFlutter handler directly below the first in both generate.go and upload.go.
The first one returns, so the copies were unreachable.

Co-authored-by: Cursor <cursoragent@cursor.com>
* andrey/apple-source:
  fix(symbols): drop duplicated Flutter dispatch blocks
  chore(main): release 3.4.0 (#760)
  feat(symbols): add Flutter (Dart AOT) symbol upload (--type flutter) (#756)

@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 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

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 b966267. Configure here.

Comment thread cmd/symbols/android_sources.go
abelonogov-ld and others added 2 commits July 27, 2026 18:57
javaPackageOf skipped comments with per-line prefix rules, which cannot
tell that a line sits inside a block comment. A license header whose
continuation lines carry no leading "*" — the common shape — looked like
source, so the first such line was read as a declaration proving the file
had no package. The file was then keyed by its bare name and no longer
matched the package-relative path the backend rebuilds from a retraced
class name, silently costing those frames their source snippet. A package
sharing a line with a block comment was lost the same way.

Comments are now stripped before scanning, honouring string literals so a
"//" inside an annotation argument stays an argument, and tracking depth
for Kotlin's nested block comments. Keyword matching also requires a word
boundary, so "packageName" is no longer read as "package".

Co-authored-by: Cursor <cursoragent@cursor.com>
--include-sources decided what to pack by whether a file could be read,
which the comments described as skipping SDK and system code. Readability
is not ownership: on any machine with Xcode the SDK headers a build
referenced are present and read fine, so they were packed and uploaded.

The filter in fact inverted. On a real dSYM all five referenced SDK
headers were readable while all ninety-nine app sources were not (built
in a DerivedData tree since cleaned), so the bundle would have carried
nothing but Apple's headers.

Provenance is now decided by path: toolchain, SDK, CommandLineTools and
OS roots are excluded wherever Xcode is installed, checking the DWARF key
as well as the resolved path. Third-party code the project builds itself
(SPM checkouts, Pods) is still included -- it is part of the app and
useful in a trace.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld requested a review from Vadman97 July 28, 2026 15:21
abelonogov-ld and others added 3 commits July 28, 2026 11:23
* andrey/apple-source:
  fix(symbols): exclude toolchain and SDK sources from Apple bundles
* main:
  chore(main): release 3.5.0 (#761)
  feat(symbols): upload Swift/ObjC sources with dSYMs (--include-sources) (#757)

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	cmd/symbols/upload.go
Bundle keys are a package plus a file name, and two files can hold the same
one: defining a class per build variant is ordinary Gradle practice, so
src/debug and src/main can each hold com/example/Config.kt. The builder keeps
whichever file arrives first, and the walk is lexical, so "debug" beat "main"
and the variant's copy became the source the backend serves for every retraced
frame in that class.

Candidates are now ranked by source set, main winning, before anything is
handed to the builder. Variants tie with each other and with files outside the
src/<set>/ layout, and walk order settles those, so a tree always bundles the
same file. Test source sets (src/test, src/androidTest and their
variant-specific siblings) are pruned outright: they are compiled into neither
the app nor its mapping.txt, so no retraced frame can point at them, and a
test-only class sharing a production class's package and file name would
otherwise compete for its key.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld merged commit a303f6c into main Jul 28, 2026
8 checks passed
@abelonogov-ld
abelonogov-ld deleted the andrey/android-source branch July 28, 2026 18:46
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.

2 participants