Symbolication for stack traces

This topic explains how to use symbolication to display human-readable stack traces for errors from mobile applications in the LaunchDarkly user interface (UI). Release builds of mobile apps are typically obfuscated, minified, or compiled to bytecode, so their stack frames don’t directly map to your source code. Symbolication is the process of generating and uploading the symbol files that let LaunchDarkly retrace stack frames back to your original source.

Symbolication applies only to the client-side observability plugin SDKs for mobile platforms. If you are developing a web JavaScript application, use source maps instead of symbol files to display human-readable stack traces. To learn more, read Use ldcli for uploading sourcemaps.

Prerequisites

To use symbolication, you need:

  • An application instrumented with the observability plugin for a supported mobile SDK:
  • LaunchDarkly CLI version 3.9.0 or later.
  • An access token for the LaunchDarkly CLI. Run ldcli login to store a token on your machine. In a continuous integration (CI) job, or in any other environment with no stored token, set the LD_ACCESS_TOKEN environment variable or pass the --access-token option to each command. You can set LD_PROJECT in place of the --project option the same way.

How symbolication works

Symbolication uses a deterministic identifier, called a symbols ID, to match the running build of an application to the symbol files you upload.

Your application build tools generate symbol files and record a symbols ID for the build. When you use the LaunchDarkly CLI to upload symbols, it assigns the symbols ID for the build automatically.

At runtime, the LaunchDarkly SDK reports the symbols ID with every signal as the launchdarkly.symbols_id.htlhash resource attribute. When your app reports an error, LaunchDarkly uses the symbols ID in the attribute to find the matching symbol files and retrace the obfuscated stack frames into human-readable source locations. Retracing also expands the frames that your compiler inlined, so the stack trace includes calls that the optimized build omitted.

You can review the human-readable stack trace information for the application using the observability Errors page. To learn more, read Error monitoring.

React Native

React Native apps that run on Hermes execute bytecode in release mode, so their stack frames are bytecode offsets. To symbolicate them, LaunchDarkly requires the composed source map that combines the Metro packager map with the Hermes map.

Even though React Native symbolication uses a source map, you must upload it with ldcli symbols upload instead of ldcli sourcemaps upload. Use the sourcemaps upload command only for web JavaScript applications. React Native applications require the symbols ID workflow described in this topic.

To enable symbolication for a React Native app:

  1. Add the LaunchDarkly Metro plugin to your metro.config.js file. The plugin stamps each release bundle with a symbols ID and generates the *.symbolsid sidecar file:
metro.config.js
1const { withLaunchDarklySymbolsId } = require('@launchdarkly/observability-react-native/metro')
2
3module.exports = withLaunchDarklySymbolsId(config)
  1. Build your app in release mode. The composed source maps generate automatically in the ./build directory:
$npx react-native run-android --mode release
  1. Upload the symbols to LaunchDarkly:
Upload React Native symbols
$ldcli symbols upload \
>--type react-native \
>--path ./build \
>--project <project-key>

The command detects the symbols ID sidecar file automatically, so you do not need to pass the --symbols-id option.

Android

Android release builds use R8 for resource shrinking and obfuscation. To symbolicate an Android application, you upload the mapping.txt file that R8 produces for the build, so that LaunchDarkly can restore the original names and line numbers in the stack frame.

R8 records an identifier for each mapping it produces and, from Android Gradle plugin (AGP) 8.12 onward, stamps that identifier into your app. The LaunchDarkly CLI automatically reads the identifier as the symbols ID when you upload the mapping file.

To enable symbolication for an Android app:

  1. Open your app/build.gradle.kts file and confirm that isMinifyEnabled = true for the release build type.

  2. Open your app/proguard-rules.pro file and add or update -keepattributes to keep all of the metadata that stack traces are retraced from:

proguard-rules.pro
-keepattributes SourceFile,LineNumberTable
Renaming the SourceFile attribute limits functionality

R8 uses the SourceFile attribute to stamp each class with the identity of the build’s mapping. LaunchDarkly uses this attribute to distinguish one build’s mapping from another, including for different builds of the same application version. If you override the attribute with -renamesourcefileattribute, LaunchDarkly falls back to matching by the app version instead of the build version.

  1. Report the version you release. Set serviceVersion in the observability plugin options to the same version string you use for uploading the mapping. LaunchDarkly matches against this version when a build predates R8’s stamp:
Configure serviceVersion
1val observabilityPlugin = Observability(
2 application = this@MyApplication,
3 mobileKey = mobileKey,
4 options = ObservabilityOptions(
5 serviceVersion = BuildConfig.VERSION_NAME,
6 )
7)

BuildConfig.VERSION_NAME is generated only when your app sets buildFeatures { buildConfig = true }. Otherwise, pass the version however your build provides it.

  1. Build your release variant:
Android release build
$./gradlew :app:assembleRelease
  1. From your Android project root, use ldcli to upload the mapping to LaunchDarkly:
Upload Android symbols
$ldcli symbols upload \
>--type android \
>--project <project-key>

The CLI finds the R8 mapping file under app/build/outputs/ and reads the build’s version from its packaged metadata, so you do not need to pass --path or --app-version. It is safe to run ldcli symbols upload on every build. By default ldcli skips any mapping that LaunchDarkly already has. To send it anyway, include the --no-skip-existing option.

If your project has more than one obfuscated variant, the CLI reports the ambiguity. Include the --path option to point at the mapping file for the variant you want to ship.

Troubleshoot obfuscated Android stack traces

If stack frames still appear obfuscated on the Errors page, verify that:

  • You uploaded the mapping file for that build. A mapping file belongs to a single R8 run. Rebuilding the same version produces a different mapping, and only the uploaded mapping can retrace stack traces from the build it came from.
  • The versions match. When LaunchDarkly matches a build by version instead of by R8’s identifier, serviceVersion must exactly match the --app-version value you used to upload the mapping.
  • The build is obfuscated. Debug builds usually are not obfuscated, so their stack traces arrive readable and have nothing to retrace.

iOS

For Apple platforms, symbolication uses the debug symbols (dSYM) that Xcode generates for a release build. The LaunchDarkly CLI converts the debug information into a compact .dsymmap symbol map and uploads it using the image UUID as the symbols ID. LaunchDarkly matches each stack frame to its dSYM by that UUID, so there is no version string to keep in sync. The SDK enables crash reporting by default.

Symbolication requires a dSYM for each configuration you release. In Xcode, confirm that Debug Information Format in your target’s build settings is set to DWARF with dSYM File. Xcode sets this value for the Release configuration in its application templates. The Debug configuration uses DWARF, which produces no dSYM to upload, so debug stack traces already arrive readable.

We recommend uploading each dSYM from the build that produced it, using an Xcode Run Script build phase.

Upload from an Xcode build phase

To upload symbols during the build:

  1. Open your app target in Xcode and click Build Phases.
  2. Click + and select New Run Script Phase.
  3. Move the new phase so that it runs after Compile Sources.
  4. Uncheck Based on dependency analysis so that the phase runs on every build.
  5. Enter the following script:
Run Script build phase
$export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
$ldcli symbols upload --type ios --project <project-key>

A build phase does not inherit the PATH value from your shell, so the first line must point to the directory where you installed ldcli.

Because the --path option is absent, the CLI reads the dSYM location from the DWARF_DSYM_FOLDER_PATH environment variable that Xcode sets during the build. A configuration that produces no dSYM, such as a typical Debug build, is skipped rather than treated as an error, so the phase does not need a guard for Debug builds.

Do not include an access token in the script, because Xcode stores the script in your project. A build phase does not inherit your shell’s environment, but it does read the token that ldcli login stores on the build machine.

Uploading the same build twice sends nothing the second time, because a build UUID that LaunchDarkly already has can only belong to the same binary.

Upload a dSYM manually

To upload a dSYM you already have, such as one from an archive or one you downloaded from App Store Connect after Apple re-signed the build, point the --path option at it. An explicit --path takes precedence over DWARF_DSYM_FOLDER_PATH, so uploads from an archive work the same way inside and outside a build phase:

Upload Apple symbols
$ldcli symbols upload \
>--type ios \
>--path <path to your dSYMs> \
>--project <project-key>

The --type ios option also accepts aliases for other Apple platforms, such as ipados, tvos, watchos, visionos, macos, apple, and dsym.

Flutter

Flutter apps compiled with Dart ahead-of-time (AOT) compilation produce ELF symbol files (app.<arch>.symbols) when you build with split debug information. The LaunchDarkly CLI parses these files, extracts the Dart build ID, and compiles them into a compact .dartmap symbol map keyed by that build ID.

To upload Flutter symbols, use the ldcli symbols upload command with --type flutter. The command also accepts the dart alias. Point --path at the directory that contains your split debug information:

Upload Flutter symbols
$ldcli symbols upload \
>--type flutter \
>--path <split-debug-info directory> \
>--project <project-key>

Include source context

By default, a symbolicated stack frame shows the file name, function, and line number. For Apple and Android apps, you can also upload the source files that your build references. This enables the Errors page to display the lines of code around each frame. Uploading source files requires LaunchDarkly CLI version 3.6.0 or later.

Source upload is an optional feature because it stores your source code in LaunchDarkly. You can enable the feature by including the --include-sources option when you upload symbols. Run the upload from the machine that built the app, so ldcli can read the referenced source files. If the CLI finds no eligible source files, it uploads only the symbol map.

iOS

Add --include-sources to your ldcli upload command. The CLI reads the source paths recorded in the dSYM’s debug information, so you do not need to specify where your source is stored:

Upload Apple symbols with source context
$ldcli symbols upload \
>--type ios \
>--path <path to your dSYMs> \
>--include-sources \
>--project <project-key>

The CLI skips SDK and system sources, files larger than 2 MiB, and any file types that the Errors page cannot render. Some frames will still show only a module and offset, because the Swift compiler attributes some optimizer-generated code to <compiler-generated> without providing a source line.

Android

An R8 mapping file does not include source paths, so you must include --source-path with --include-sources to tell ldcli where to search for .java and .kt files to upload. --source-path defaults to the current directory. Point it at your app’s source directory to search faster and to avoid accidentally uploading test or sample code:

Upload Android symbols with source context
$ldcli symbols upload \
>--type android \
>--include-sources \
>--source-path ./app/src/main \
>--project <project-key>

The search skips build output and the .gradle, .git, .idea, and node_modules directories, individual files larger than 2 MiB, and source bundles larger than 64 MiB.