Skip to content

feat(symbols): zero-config Android uploads, and gzip what an upload sends - #767

Open
abelonogov-ld wants to merge 3 commits into
mainfrom
andrey/android-zero-config-upload
Open

feat(symbols): zero-config Android uploads, and gzip what an upload sends#767
abelonogov-ld wants to merge 3 commits into
mainfrom
andrey/android-zero-config-upload

Conversation

@abelonogov-ld

Copy link
Copy Markdown
Contributor

Summary

Three changes that together let ldcli symbols upload --type android run from an Android project root with no flags, and cut what every upload sends.

Find the mapping and app version in the build. The command previously needed a --path pointing at a directory holding mapping.txt and an --app-version matching what the app reports, so every project had to add build-script code to stage the mapping and plumb its version into CI. The Android Gradle Plugin already writes both — R8's mapping at <module>/build/outputs/mapping/<variant>/mapping.txt, and the packaged version in output-metadata.json beside the APK. Several obfuscated variants is an error naming them rather than a guess, since only one is the build being shipped. An Android mapping is also now stored as mapping.txt however deep it was found; symbolication reads it at <lane>/mapping.txt, so a mapping uploaded from a nested path used to be keyed where nothing looks.

Read the symbols id out of the packaged app. A build that stamps a content-derived id into assets/ld_symbols_id.txt had to also stage a mapping.txt.symbolsid sidecar next to a copy of the mapping, purely so the upload could be keyed by the id the app reports. The APK/AAB already carries that asset, and it is the app that will run, so reading it there needs nothing handed over by the build and leaves no way for the two to disagree. An id that doesn't match the 32-hex-char shape the SDK reports is ignored rather than keyed on.

Gzip what an upload sends. The e2e app's mapping is 61.3 MB and gzips to 5.0 MB in under half a second; React Native and Apple artifacts compress on the same order. Objects are marked Content-Encoding: gzip so they stay self-describing. In-memory artifacts are compressed before an upload URL is requested, because the digest that proves an object is already stored is compared against the ETag of the stored bytes — hashing the uncompressed artifact would never match and every source bundle would be re-sent. Files are streamed through the compressor so a large mapping is never held in memory whole. Anything gzip doesn't shrink is sent as-is, so a .srcbundle isn't wrapped twice.

Ordering

The backend read side must ship first: https://github.com/launchdarkly/observability/pull/1611. Until it is deployed, a compressed mapping would reach the parser as bytes it cannot read.

Test plan

  • go test ./cmd/symbols/
  • Discovery picks the single obfuscated variant and errors naming them when there are several
  • Symbols id is read from a packaged APK/AAB and ignored when malformed
  • Compression skips artifacts it cannot shrink, and digests are computed over the stored bytes

Made with Cursor

abelonogov-ld and others added 3 commits July 30, 2026 18:02
`ldcli symbols upload --type android` needed a --path pointing at a directory
holding mapping.txt and an --app-version matching what the app reports, so
every project had to add build script code to stage the mapping somewhere and
plumb its version into CI.

The Android Gradle Plugin already writes both: R8's mapping at
<module>/build/outputs/mapping/<variant>/mapping.txt, and the version it
packaged in output-metadata.json beside the APK. Read them, and the command
works from an Android project root with no flags and no build script at all.
Several obfuscated variants is an error naming them rather than a guess, since
only one of them is the build being shipped.

An Android mapping is now also stored as mapping.txt however deep it was found.
Symbolication reads it at <lane>/mapping.txt, so a mapping uploaded from a
nested path was previously keyed somewhere nothing looks.

Co-authored-by: Cursor <cursoragent@cursor.com>
A build that stamps a content-derived symbols id into assets/ld_symbols_id.txt
had to also stage a mapping.txt.symbolsid sidecar next to a copy of the mapping,
purely so the upload could be keyed by the same id the app reports.

The packaged APK/AAB already carries that asset, and it is the app that will
run: what it carries is exactly what will be reported. Reading it there needs
nothing handed over by the build and leaves no way for the two to disagree, so
the staging step goes away and a stamped build uploads on the Symbols Id Lane
with no flags.

An id that does not match the 32-hex-char shape the SDK reports is ignored
rather than keyed on, since it names a lane nothing would ever ask for.

Co-authored-by: Cursor <cursoragent@cursor.com>
A release R8 mapping is tens of megabytes of text and every build pushes it
again: the e2e app's is 61.3 MB, which gzips to 5.0 MB in under half a second.
React Native source maps and Apple symbol maps compress on the same order.

Each object is marked Content-Encoding: gzip, so it stays self-describing —
storage returns the header on read, an HTTP client inflates it in transit, and
the backend inflates whatever still arrives compressed.

Artifacts held in memory are compressed before an upload URL is asked for rather
than at the point of sending, because the digest that proves an object is
already stored is compared against the ETag of the stored bytes; hashing the
artifact instead would never match and every source bundle would be re-sent.
Files are compressed as they are read, streamed through the compressor so a
large mapping is never held in memory whole.

Anything that comes out of gzip no smaller is sent as it is, so a .srcbundle,
which already gzips its own entries, is not wrapped a second time.

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 1b66af8. Configure here.

Comment thread cmd/symbols/upload.go
return filepath.Base(relPath)
}
return relPath
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flattened mappings collide on upload

High Severity

uploadName always stores Android artifacts as mapping.txt, so a directory walk that finds more than one mapping (for example --path aimed at outputs/mapping with several variants, where AGP discovery does not run) builds identical storage keys. Later uploads overwrite earlier ones, and symbolication can bind the wrong mapping.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1b66af8. Configure here.

// A path naming one file is already an answer; a path naming nothing is
// reported by the ordinary search, whose error says what was looked for.
return upload, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Disambiguation drops build metadata

Medium Severity

When several variants are found, the error tells callers to pass --path using the listed mapping.txt paths, but resolveAndroidBuild treats a file path as final and never reads output-metadata.json or the packaged APK/AAB. App version and symbols id are left unset, so the upload falls back to the wrong lane despite those values being available beside that mapping.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1b66af8. Configure here.

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