feat(symbols): upload Java/Kotlin sources with R8 mappings (--include-sources) - #758
Merged
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>
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>
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>
There was a problem hiding this comment.
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).
❌ 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.
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>
Vadman97
approved these changes
Jul 28, 2026
* andrey/apple-source: fix(symbols): exclude toolchain and SDK sources from Apple bundles
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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