> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Symbolication for stack traces

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

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](/docs/home/getting-started/ldcli-commands#use-ldcli-for-uploading-sourcemaps).

## Prerequisites

To use symbolication, you need:

* An application instrumented with the observability plugin for a supported mobile SDK:
  * [React Native](/docs/sdk/observability/react-native)
  * [Android](/docs/sdk/observability/android)
  * [iOS](/docs/sdk/observability/ios)
  * [Flutter](/docs/sdk/observability/flutter)
* [LaunchDarkly CLI](/docs/home/getting-started/ldcli) 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](/docs/home/observability/errors).

## 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:

<CodeGroup>
  ```js title="metro.config.js" lines wrap theme={null}
  const { withLaunchDarklySymbolsId } = require('@launchdarkly/observability-react-native/metro')

  module.exports = withLaunchDarklySymbolsId(config)
  ```
</CodeGroup>

2. Build your app in release mode. The composed source maps generate automatically in the `./build` directory:

<CodeGroup>
  ```bash title="Android release build" lines wrap theme={null}
  npx react-native run-android --mode release
  ```

  ```bash title="iOS release build" lines wrap theme={null}
  npx react-native run-ios --mode Release
  ```
</CodeGroup>

3. Upload the symbols to LaunchDarkly:

<CodeGroup>
  ```bash title="Upload React Native symbols" lines wrap theme={null}
  ldcli symbols upload \
  --type react-native \
  --path ./build \
  --project <project-key>
  ```
</CodeGroup>

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:

<CodeGroup>
  ```text title="proguard-rules.pro" lines wrap theme={null}
  -keepattributes SourceFile,LineNumberTable
  ```
</CodeGroup>

<Warning>
  **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.
</Warning>

3. 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:

<CodeGroup>
  ```kotlin title="Configure serviceVersion" lines wrap theme={null}
  val observabilityPlugin = Observability(
      application = this@MyApplication,
      mobileKey = mobileKey,
      options = ObservabilityOptions(
          serviceVersion = BuildConfig.VERSION_NAME,
      )
  )
  ```
</CodeGroup>

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

4. Build your release variant:

<CodeGroup>
  ```bash title="Android release build" lines wrap theme={null}
  ./gradlew :app:assembleRelease
  ```
</CodeGroup>

5. From your Android project root, use `ldcli` to upload the mapping to LaunchDarkly:

<CodeGroup>
  ```bash title="Upload Android symbols" lines wrap theme={null}
  ldcli symbols upload \
  --type android \
  --project <project-key>
  ```
</CodeGroup>

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:

<CodeGroup>
  ```bash title="Run Script build phase" lines wrap theme={null}
  export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
  ldcli symbols upload --type ios --project <project-key>
  ```
</CodeGroup>

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:

<CodeGroup>
  ```bash title="Upload Apple symbols" lines wrap theme={null}
  ldcli symbols upload \
  --type ios \
  --path <path to your dSYMs> \
  --project <project-key>
  ```
</CodeGroup>

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:

<CodeGroup>
  ```bash title="Upload Flutter symbols" lines wrap theme={null}
  ldcli symbols upload \
  --type flutter \
  --path <split-debug-info directory> \
  --project <project-key>
  ```
</CodeGroup>

## 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](/docs/home/getting-started/ldcli) 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:

<CodeGroup>
  ```bash title="Upload Apple symbols with source context" lines wrap theme={null}
  ldcli symbols upload \
  --type ios \
  --path <path to your dSYMs> \
  --include-sources \
  --project <project-key>
  ```
</CodeGroup>

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:

<CodeGroup>
  ```bash title="Upload Android symbols with source context" lines wrap theme={null}
  ldcli symbols upload \
  --type android \
  --include-sources \
  --source-path ./app/src/main \
  --project <project-key>
  ```
</CodeGroup>

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.
