---
title: Issues & Crashes
---
# Issues & Crashes

The Capture SDK can automatically detect, capture, and upload issues and errors that affect your app.

!!! note ""
    See the [Product > Issues Overview](../../product/issues/overview.md#features) to learn more about how to navigate this information in the product.

## Setup

Fatal issue reporting is **enabled by default** in the Capture SDK since 0.18.7 version but can be disabled as shown below.

## Issue Report Callback

<img alt="Capture SDK Support" src="https://img.shields.io/badge/Android-0.22.12-006C9C">
<img alt="Capture SDK Support" src="https://img.shields.io/badge/iOS-0.22.12-006C9C">
<img alt="Capture SDK Support" src="https://img.shields.io/badge/React%20Native-0.11.13-006C9C">

You can register a callback to receive issue report metadata right before the SDK sends a report.

!!! example "Experimental"
    This callback API is experimental.

Use `issueCallbackConfiguration` when creating the SDK configuration.

=== "Android (Kotlin)"

    ```kotlin
    import io.bitdrift.capture.Configuration
    import io.bitdrift.capture.Capture
    import io.bitdrift.capture.experimental.ExperimentalBitdriftApi
    import io.bitdrift.capture.reports.IssueCallbackConfiguration
    import io.bitdrift.capture.reports.IssueReportCallback
    import io.bitdrift.capture.reports.Report

    @OptIn(ExperimentalBitdriftApi::class)
    val config = Configuration(
        enableFatalIssueReporting = true,
        issueCallbackConfiguration = IssueCallbackConfiguration(
            executor = java.util.concurrent.Executors.newSingleThreadExecutor(),
            issueReportCallback = IssueReportCallback { report ->
                // TODO: Add your custom handling for report metadata.
            },
        ),
    )
    ```

=== "iOS (Swift)"

    ```swift
    import Capture
    import Foundation

    final class CustomerIssueReportCallback: NSObject, IssueReportCallback {
      func onBeforeReportSend(report: IssueReport) {
        // TODO: Add your custom handling for report metadata.
      }
    }

    let config = Capture.Configuration(
      enableFatalIssueReporting: true,
      issueCallbackConfiguration: IssueCallbackConfiguration(
        callbackQueue: DispatchQueue(label: "customer-issue-report-callback"),
        issueReportCallback: CustomerIssueReportCallback()
      )
    )
    ```

=== "iOS (Objective-C)"

    ```objective-c
    #import <Capture/Capture.h>

    @interface CustomerIssueReportCallback : NSObject <IssueReportCallback>
    @end

    @implementation CustomerIssueReportCallback

    - (void)onBeforeReportSendWithReport:(CAPIssueReport *)report {
      // TODO: Add your custom handling for report metadata.
    }

    @end

    CAPIssueCallbackConfiguration *issueCallbackConfiguration =
      [[CAPIssueCallbackConfiguration alloc] initWithCallbackQueue:dispatch_get_main_queue()
                                               issueReportCallback:[CustomerIssueReportCallback new]];

    CAPConfiguration *config = [[CAPConfiguration alloc]
      initWithEnableFatalIssueReporting:YES
      enableURLSessionIntegration:YES
      sleepMode:NO
      apiURL:nil
      rootFileURL:nil
      issueCallbackConfiguration:issueCallbackConfiguration];
    ```

    !!! note
        Objective-C cannot construct Swift's `Capture.Configuration` struct directly. Use `CAPConfiguration` and `CAPIssueCallbackConfiguration` from the Objective-C API surface instead.

=== "React Native (JavaScript)"

    ```javascript
    import { init, SessionStrategy } from '@bitdrift/react-native';

    init('<api-key>', SessionStrategy.Activity, {
      crashReporting: {
        UNSTABLE_onBeforeReportSend: (report) => {
          // Add your custom handling for report metadata.
          // report contains: reportType, reason, details, sessionId, fields
        },
      },
    });
    ```

`Report` (Android `Report` / iOS `CAPIssueReport` / React Native object) includes:

| Field | Type | Description |
| --- | --- | --- |
| `reportType` | `String` | High-level issue type. One of `Crash`, `Native Crash`, or `ANR`. |
| `reason` | `String` | Primary issue identifier (for example exception class such as `java.lang.NullPointerException`, or signal name such as `SIGSEGV`). |
| `details` | `String` | Additional issue details (for example the exception message). |
| `sessionId` | `String` | bitdrift session ID associated with the report. Casing differs by platform: `sessionId` on Android and React Native, `sessionID` on iOS (Swift/Obj-C). |
| `fields` | `Map<String, String>` / `[String: String]` | Additional report fields available at callback time (for example memory pressure metadata). |

#### Semantics

- The callback is **observational only**. It returns `Unit` / `Void` and cannot modify, enrich, or prevent the report from being sent.
- It is invoked **before** the report is uploaded to the backend.
- The callback is dispatched on the `Executor` (Android) or `DispatchQueue` (iOS) you provide.

## Previous Run Info

<img alt="Capture SDK Support" src="https://img.shields.io/badge/Android-0.22.13-006C9C">
<img alt="Capture SDK Support" src="https://img.shields.io/badge/iOS-0.22.13-006C9C">
<img alt="Capture SDK Support" src="https://img.shields.io/badge/React%20Native-0.11.14-006C9C">

You can query whether the app fatally terminated in the previous run.

!!! example "Experimental"
    This API is experimental.

=== "Android (Kotlin)"

    ```kotlin
    import io.bitdrift.capture.Capture
    import io.bitdrift.capture.experimental.ExperimentalBitdriftApi

    @OptIn(ExperimentalBitdriftApi::class)
    val previousRunInfo = Capture.Logger.getPreviousRunInfo()

    // previousRunInfo?.hasFatallyTerminated
    // previousRunInfo?.terminationReason
    ```

=== "iOS (Swift)"

    ```swift
    import Capture

    let previousRunInfo = Logger.previousRunInfo

    // previousRunInfo?.hasFatallyTerminated
    // previousRunInfo?.wasCleanTermination
    // previousRunInfo?.terminationReason
    ```

=== "iOS (Objective-C)"

    ```objective-c
    #import <Capture/Capture.h>

    NSDictionary *previousRunInfo = [CAPLogger previousRunInfo];

    // previousRunInfo[@"hasFatallyTerminated"]
    // previousRunInfo[@"wasCleanTermination"]
    // previousRunInfo[@"terminationReason"]
    ```

=== "React Native (JavaScript)"

    ```javascript
    import { getPreviousRunInfo } from '@bitdrift/react-native';

    const previousRunInfo = getPreviousRunInfo();

    // previousRunInfo?.hasFatallyTerminated
    // previousRunInfo?.terminationReason
    ```

- Call this API after `Logger.start()` / `init()`. It returns `null` (or `nil`) when previous-run info is not yet available.

#### Return value

The returned object differs by platform:

=== "Android"

    ```kotlin
    data class PreviousRunInfo(
        val hasFatallyTerminated: Boolean,
        val terminationReason: ExitReason? = null,  // null when not known
    )
    ```

    - `hasFatallyTerminated` is `true` only when `terminationReason` is `JvmCrash`, `NativeCrash`, or `AppNotResponding`.
    - On Android API 30+ (Android 11+) the SDK derives `terminationReason` from [`ApplicationExitInfo`](https://developer.android.com/reference/android/app/ApplicationExitInfo).
    - On Android API < 30, only `JvmCrash` can be detected; for any other previous exit, `hasFatallyTerminated` is `false` and `terminationReason` is `null`.

    `ExitReason` values:

    | Enum | String value | Fatal? | Meaning |
    | --- | --- | --- | --- |
    | `JvmCrash` | `JVM_CRASH` | yes | Uncaught JVM/Kotlin exception. |
    | `NativeCrash` | `CRASH_NATIVE` | yes | Native (NDK) crash. |
    | `AppNotResponding` | `ANR` | yes | Fatal Application Not Responding. |
    | `Signaled` | `SIGNALED` | no | Process terminated by a signal (non-crash). |
    | `LowMemory` | `LOW_MEMORY` | no | Killed by the OS due to low memory. |
    | `ExitSelf` | `EXIT_SELF` | no | App exited itself (e.g. `System.exit`). |
    | `InitializationFailure` | `INITIALIZATION_FAILURE` | no | App initialization failed. |
    | `PermissionChange` | `PERMISSION_CHANGE` | no | Killed due to a permission change. |
    | `ExcessiveResourceUsage` | `EXCESSIVE_RESOURCE_USAGE` | no | Killed for excessive resource usage. |
    | `UserRequested` | `USER_REQUESTED` | no | User-requested exit (e.g. force stop). |
    | `UserStopped` | `USER_STOPPED` | no | Stopped by the user from system UI. |
    | `DependencyDied` | `DEPENDENCY_DIED` | no | A dependent process died. |
    | `Freezer` | `FREEZER` | no | Frozen by the OS. |
    | `Other` | `OTHER` | no | Other OS-reported reason. |
    | `Unknown` | `UNKNOWN` | no | Reason reported by the OS does not map to any known value. |

=== "iOS"

    <img alt="Capture SDK Support" src="https://img.shields.io/badge/iOS-0.23.9-006C9C">

    ```swift
    public struct PreviousRunInfo: Equatable {
        public let terminationReason: ExitReason
        public var hasFatallyTerminated: Bool { get }
        public var wasCleanTermination: Bool { get }
    }
    ```

    - `terminationReason` reports the best deterministic status the SDK can infer for the previous app run.
    - `hasFatallyTerminated` is `true` when `terminationReason` is `fatalCrash`.
    - `wasCleanTermination` is `true` when `terminationReason` is `cleanExit`.

    `ExitReason` values:

    | Enum | Fatal? | Meaning |
    | --- | --- | --- |
    | `cleanExit` | no | The app received a termination notification before dying. |
    | `fatalCrash` | yes | The in-process crash reporter captured a fatal crash during the previous run. |
    | `appUpdate` | no | The app or binary changed since the last launch, for example after installing an update. |
    | `osUpdate` | no | The OS version changed since the last launch. |
    | `debuggerAttached` | no | The previous run had a debugger attached. Only reported in debug builds. |
    | `reboot` | no | The device rebooted between runs. |
    | `unknown` | no | The previous run status could not be determined at startup. |

    The Objective-C surface returns an `NSDictionary` with the keys `"hasFatallyTerminated"`, `"wasCleanTermination"`, and `"terminationReason"`.

    !!! info "Launch-time vs asynchronous signals"
        On iOS, `PreviousRunInfo` is designed to return the best answer the SDK can provide as early as possible during app launch.

        Some termination details are only delivered later through asynchronous platform mechanisms. For example, iOS may provide additional context for exits such as `SIGKILL` via MetricKit after launch.

        In those cases, the SDK may already know at launch time that the previous run did **not** exit cleanly, but still not have enough information to classify the reason yet. When that happens, `wasCleanTermination` is `false` and `terminationReason` may remain `unknown`.

        If you want the most accurate termination details available, the best option is to keep using the [Issue Report Callback](#issue-report-callback), since later-arriving context can be included there when the issue report is processed.

## Memory Pressure Context

<img alt="Capture SDK Support" src="https://img.shields.io/badge/iOS-0.23.2-006C9C">
<img alt="Capture SDK Support" src="https://img.shields.io/badge/Android-0.23.2-006C9C">

The SDK automatically tracks memory pressure during a session. When a crash or fatal issue occurs, the report includes the last known memory pressure level observed before the crash, helping you diagnose out-of-memory (OOM)-related failures without any additional configuration. This enrichment is enabled automatically alongside fatal issue reporting.

### Reported Fields

When the crash report is generated, it may include:

- **Memory Pressure Level**: The last memory pressure state recorded, one of `normal`, `warning`, or `critical`. Both iOS and Android report these three levels. This value can be used to filter issues in the Issues Overview and Group Detail pages. For more information, see the [Filtering](../../product/issues/filtering.md) documentation.
- **Memory Pressure Timestamp**: Timestamp (in microseconds since epoch) of when the last memory pressure event was observed.

The field is omitted when no pressure event was recorded for that session (i.e. the device never left a normal memory state).

### How It Works

The SDK tracks memory pressure using platform-specific mechanisms and persists the last observed state to local storage:

- **iOS**: Subscribes to `DispatchSource.MemoryPressureEvent` notifications from the OS.
- **Android**: Periodically samples JVM heap usage during resource utilization ticks and classifies it against configurable thresholds.

On the next app launch after a crash, the crash handler reads the persisted state from the previous run and includes it in the report. The memory pressure level in a crash report reflects the state of the *session that crashed*, not the current session.

## Disable Issue Reporting

If you wish to disable automatic fatal issue reporting, you can set `enableFatalIssueReporting = false` in Configuration [`Logger.start()`](./configuration.md).

=== "Android (Kotlin)"

    ```kotlin
    Logger.start(
      apiKey = "<your-api-key>",
      sessionConfiguration = SessionConfiguration(),
      configuration = Configuration(enableFatalIssueReporting = false),
    )
    ```

=== "Android (Java)"

    ```java
    boolean enableFatalIssueReporting = false;
    Logger.start(
      "<your-api-key>",
      new SessionConfiguration(),
      new Configuration(new SessionReplayConfiguration(), enableFatalIssueReporting)
    );
    ```

=== "iOS (Swift)"

    ```swift
    Logger.start(
      withAPIKey: "<your-api-key>",
      sessionConfiguration: SessionConfiguration(),
      configuration: Capture.Configuration(enableFatalIssueReporting: false),
    )
    ```

=== "iOS (Objective-C)"

    ```objective-c
    #import <Capture/Capture.h>

    CAPConfiguration *config = [[CAPConfiguration alloc]
      initWithEnableFatalIssueReporting:NO
      enableURLSessionIntegration:YES];

    CAPSessionConfiguration *sessionConfiguration =
      [[CAPSessionConfiguration alloc] initWithInitialSessionID:nil
                                      inactivityTimeoutSeconds:nil
                                           onSessionIDChanged:nil];

    [CAPLogger startWithAPIKey:@"your-api-key"
          sessionConfiguration:sessionConfiguration
                  configuration:config];
    ```
    !!! note
        Issue reports on iOS are uploaded only for actual devices and not simulators

=== "React Native"

    ```javascript
    import { init, SessionStrategy } from '@bitdrift/react-native';

    init('<api-key>', SessionStrategy.Activity, {
      crashReporting: {
        enableNativeFatalIssues: false,
      }
    });
    ```

## React Native JavaScript Support

The React Native SDK supports capturing global JavaScript errors in addition to platform crashes. This feature is experimental and can be enabled via the `UNSTABLE_enableJsErrors` configuration flag.

!!! info "Supported Configuration"
    JavaScript error reporting is currently supported on Expo builds using the **New Architecture** and **Hermes** engine.

### Configuration

```javascript
import { init, SessionStrategy } from '@bitdrift/react-native';

init('<api-key>', SessionStrategy.Activity, {
  crashReporting: {
    enableNativeFatalIssues: true,
    UNSTABLE_enableJsErrors: true,
  }
});
```

- `enableNativeFatalIssues`: When `true`, enables reporting of native issues including crashes, ANRs (Application Not Responding), and other critical errors. Defaults to `true`.
- `UNSTABLE_enableJsErrors`: When `true`, enables reporting of JavaScript errors via React Native's global error handler. Captures unhandled exceptions with stack traces. Defaults to `false`.

!!! example "Experimental"
    The `UNSTABLE_enableJsErrors` flag is experimental. This feature is under active development and may change in future releases.

## Behavior

### Session Uploads
Fatal issue reports and their session information (logs, etc) are automatically uploaded and do not require a [Record Session](../../product/workflows/actions.md#record-session) to be running in a Workflow.

### Android

#### Platform Support

| Android version | JVM crashes | Native (NDK) crashes | ANRs |
| --- | --- | --- | --- |
| Android 12+ (API level 31+) | Full | Full | Full |
| Android 11 (API level 30) | Full | Partial (no stack traces) | Full |
| Android 6-10 (API level 23-29) | Full | None | None |

Native crash detection on Android 11+ uses [`ApplicationExitInfo`](https://developer.android.com/reference/android/app/ApplicationExitInfo). On API level 30, Android reports the fatal native exit but does not provide the tombstone data needed to reconstruct a stack trace, so reports include the signal and exit metadata but not frames. Full native stack traces are available on API level 31+.

#### Native Crash Rationale

On Android 11, bitdrift intentionally relies on the platform-native `ApplicationExitInfo` path instead of shipping a separate native crash-capture library just to recover stack traces on older OS versions.

This is an intentional tradeoff. Shipping a separate native crash-capture library would add size, startup cost, and memory overhead for all customers in order to recover stack traces for Android 11 only. In return, bitdrift keeps the platform-native tombstone path, which on Android 12+ provides higher quality crash data than custom signal-handler-based integrations typically can: stack traces for all threads, native frames alongside de-obfuscated JVM frames, and broader OEM symbol support. Native crash reports also stay aligned with Android's process-exit reporting and include the rest of the fatal issue context in bitdrift, such as crash reason, device and app metadata, feature flags, custom fields, and the session timeline.

If a native crash occurs on Android 11, bitdrift still detects and uploads the fatal issue so it can be triaged, but the report will not include a stack trace. This is the expected behavior for API level 30.

#### R8/ProGuard Settings

To preserve line numbers and source file names for stack traces, make sure this rule is added to your app's ProGuard configuration:

`-keepattributes SourceFile,LineNumberTable`

!!! note
    - `SourceFile`: This attribute is kept by default since Android Gradle Plugin (AGP) 8.2, and only needs to be explicitly kept in apps using earlier versions of AGP.
    - `LineNumberTable`: This attribute is kept by default since Android Gradle Plugin (AGP) 8.6, and only needs to be explicitly kept in apps using earlier versions of AGP.

### iOS

#### Platform Support

The SDK combines MetricKit with in-process crash reporting to provide all the information needed to understand and investigate crashes. MetricKit is the primary source of platform diagnostics because it can report terminations and provide diagnostic details for events that occur outside the app process, many of which in-process crash reporters cannot capture accurately, such as [SIGKILLs](https://developer.apple.com/documentation/xcode/sigkill). In-process crash reporting is also useful as it provides crash context captured while the app is running that MetricKit cannot provide.

!!! info "Fatal issue reports are uploaded only for physical devices"
    MetricKit is unavailable in the simulator, so it cannot provide the complete diagnostic experience there.

#### Out of Memory Reports

<img alt="Capture SDK Support" src="https://img.shields.io/badge/iOS-0.25.0-006C9C">

When an app **is built with Xcode 27 and runs on iOS 27 or later**, Capture uses MetricKit's newer
[`MetricManager`](https://developer.apple.com/documentation/metrickit/metricmanager) API. This new API provides memory exception diagnostics that are unavailable through the legacy [`MXMetricManager`](https://developer.apple.com/documentation/metrickit/mxmetricmanager?language=objc) API. The SDK reports these OOM terminations as native issues with the exception name `EXC_RESOURCE` and the reason `Out Of Memory`.

## Uploading Debug Information Files

!!! warning "SDK-key uploads are deprecated"
    The `BITDRIFT_API_KEY` / `--api-key` value used throughout this section must be an API key with the
    `DEBUG_FILE_UPLOAD` permission. This applies to CLI, Gradle, Xcode, and direct API uploads,
    including their `HEAD` preflight requests. SDK-key authentication remains available during the
    migration, but is deprecated for new upload automation.

Create a least-privilege upload key with `bd key create api --permissions debug-file-upload`, then
store it in CI as `BITDRIFT_API_KEY`. See the [permission
reference](../../api/bitdrift_public_unary_admin_v1_Permission.md) for details.

To generate human readable stack traces, bitdrift needs your apps' debug information files.

### bd cli tool

Our command-line tool makes it easy to upload debug information files to bitdrift.

#### Installation

##### Homebrew (macOS)

To install the CLI directly on your Mac, use Homebrew:

```sh
brew tap bitdriftlabs/bd
brew install bd
```

##### Direct Download

Direct downloads for other platforms are also available, which are especially useful for integrating into your CI or release process. You can download the latest binaries directly from:

=== "macOS (Universal)"
    <https://dl.bitdrift.io/bd-cli/latest/bd-cli-mac-universal-apple-darwin.tar.gz/bd>
=== "macOS (arm64)"
    <https://dl.bitdrift.io/bd-cli/latest/bd-cli-mac-arm64.tar.gz/bd>
=== "macOS (x86_64)"
    <https://dl.bitdrift.io/bd-cli/latest/bd-cli-mac-x86_64.tar.gz/bd>
=== "Linux (x86_64)"
    <https://dl.bitdrift.io/bd-cli/latest/bd-cli-linux-x86_64.tar.gz/bd>

!!! info
    You can download a specific version of the bd-cli by replacing `latest` above with the version number, e.g. `0.2.23`

#### Usage

=== "Proguard Mappings (Android)"
    ```sh
    bd debug-files upload-proguard
      --api-key <API_KEY>
      --app-id <APP_ID>
      --app-version <APP_VERSION>
      --version-code <VERSION_CODE>
      <PROGUARD_MAPPING_FILE>
    ```
=== "Native Symbols (iOS / Android)"
    ```sh
    bd debug-files upload
      --api-key <API_KEY>
      <FILE_OR_DIRECTORY> # MACH-O, ELF
    ```
=== "Sourcemaps (React Native)"
    ```sh
    bd debug-files upload-source-map \
      --source-map <SOURCEMAP_FILE> \
      --bundle <BUNDLE_FILE> \
      --api-key <API_KEY>
    ```

    Example:
    ```sh
    bd debug-files upload-source-map \
      --source-map ./android/app/build/generated/sourcemaps/react/release/index.android.bundle.map \
      --bundle ./android/app/build/generated/assets/createBundleReleaseJsAndAssets/index.android.bundle \
      --api-key "$BITDRIFT_API_KEY"
    ```

!!! note
    For BYOC deployments use `--base-domain` to target your deployment.

### Platform Tools

Bitdrift supports build-time integration to upload the required Debug Information Files easily.

#### Gradle Plugin (Android)

<img alt="Capture SDK Support" src="https://img.shields.io/badge/Android-0.22.3-006C9C">

Debug information files (proguard mappings, native symbols) can be located and uploaded via the Capture Gradle Plugin.

!!! info
    The following requires the usage of the **Capture Gradle Plugin**, learn how to configure it in [Android Quickstart Guide](../quickstart.md#gradle-plugin).

After a project sync, the Capture Gradle Plugin will automatically generate the `bdUpload+` gradle tasks under the `Upload` group. You can then call them after succesfully assembling a release build (via `assembleRelease`):

- `bdUploadMapping` - Locate and upload only mapping files to Bitdrift
- `bdUploadSymbols` - Locate and upload only symbols to Bitdrift
- `bdUploadSourceMap` - Locate and upload only sourcemap files to Bitdrift
- `bdUpload` - Locate and upload all mapping files, symbols, and sourcemaps to Bitdrift (when applicable)

??? warning "Set `BITDRIFT_API_KEY` (preferred) or the deprecated `API_KEY` before calling these tasks"
    e.g.
    ```bash
    BITDRIFT_API_KEY=MY-API-KEY ./gradlew bdUploadMapping
    ```
    or:
    ```bash
    export BITDRIFT_API_KEY=MY-API-KEY
    ./gradlew bdUploadMapping
    ```

!!! warning "Gradle Plugin Version Compatibility"
    Capture Gradle Plugin versions before `0.24.0` require `API_KEY`. Version `0.24.0` and
    later support `BITDRIFT_API_KEY` and continue to support `API_KEY` for compatibility.

#### Xcode Build Phase Script (iOS)

In the "Build Phases" tab of your Xcode build target, add a run script phase (click the "+" symbol in the top-left, then select "New Run Script Phase"), and use the following script:

???+ note "Build Script"
    ```bash
    ########################################
    # Configuration
    ########################################

    UPLOAD_ON_CONFIGURATIONS=(
        # Debug
        Release
    )

    ########################################
    # Script
    ########################################
    {% raw %}
    set -eu -o pipefail

    if [[ ! " ${UPLOAD_ON_CONFIGURATIONS[*]} " =~ [[:space:]]${CONFIGURATION}[[:space:]] ]]; then
        echo "This build step is not configured to upload symbols when using build configuration \"$CONFIGURATION\", so doing nothing."
        exit 0
    fi

    if [[ "$(uname -m)" == arm64 ]]; then
        BD_ARCH=arm64
    else
        BD_ARCH=x86_64
    fi
    BD_URL="https://dl.bitdrift.io/bd-cli/latest/bd-cli-mac-${BD_ARCH}.tar.gz/bd"
    BD="$TEMP_DIR/bd"

    dsym_files=()
    if [ $ENABLE_USER_SCRIPT_SANDBOXING == "YES" ]; then
        if [ $SCRIPT_INPUT_FILE_COUNT -le 0 ]; then
            echo 'ENABLE_USER_SCRIPT_SANDBOXING is enabled. All dSYMs must be declared in the "Input Files" section of this run script when sandboxing is enabled.'
            echo 'Please either disable ENABLE_USER_SCRIPT_SANDBOXING, or add the following into the "Input Files" section of this run script:'
            echo '    ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}'
            exit 1
        fi

        for ((n=0;n<$SCRIPT_INPUT_FILE_COUNT;n++)); do
            declare name=SCRIPT_INPUT_FILE_${n}
            if [ -f "${!name}" ]; then
                dsym_files+=("${!name}")
            fi
        done
    else
        dsym_files+=("${DWARF_DSYM_FOLDER_PATH}")
    fi

    if [ ${#dsym_files[@]} -eq 0 ]; then
        echo "Did not find any dSYM files to upload to Bitdrift. By default, Debug builds don't include dSYM files."
        echo "If you want to generate dSYM files in a debug build, modify your 'Debug Information Format' build setting and set it to 'DWARF with dSYM File'."
        exit 0
    fi

    if [ -z ${BITDRIFT_API_KEY+x} ]; then
        echo "Bitdrift requires the 'BITDRIFT_API_KEY' environment value to be set."
        echo "Please set BITDRIFT_API_KEY to your Bitdrift API key value, then launch xcode or xcodebuild from that environment."
        exit 1
    fi

    if [ ! -f "$BD" ]; then
        curl -fSL "$BD_URL" -o "$BD"
        curl -fSL "$BD_URL.sha256" -o "$BD.sha256" || {
            echo "ERROR: Failed to download bd-cli checksum from $BD_URL.sha256"
            rm -f "$BD"
            exit 1
        }

        EXPECTED_CHECKSUM=$(cat "$BD.sha256")
        ACTUAL_CHECKSUM=$(shasum -a 256 "$BD" | awk '{print $1}')

        if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
            echo "ERROR: Checksum verification failed for bd-cli binary"
            echo "Expected: $EXPECTED_CHECKSUM"
            echo "Actual:   $ACTUAL_CHECKSUM"
            rm -f "$BD" "$BD.sha256"
            exit 1
        fi

        chmod a+x "$BD"
        rm -f "$BD.sha256"
    fi

    for path in "${dsym_files[@]}"; do
        echo "Uploading to Bitdrift: $path"
        "$BD" debug-files upload --api-key="$BITDRIFT_API_KEY" "$path"
    done
    ```
    {% endraw %}

!!! info
    You will need to set the `BITDRIFT_API_KEY` environment variable to your Bitdrift API key, and run your build under that environment

!!! info
    If you have script sandboxing enabled on your build, you'll need to expose the symbol files so that the script can see them. This can be done by adding an entry with `${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}` to the "Input Files" section of the run script build phase.

!!! note
    For BYOC deployments use `--base-domain` to target your deployment.

#### Manual CLI Uploads (Apple)

If you do not use the Xcode build phase, upload the debug information from each Apple release yourself. bitdrift needs the Mach-O debug binary from every relevant dSYM to symbolicate its frames. This includes your app's primary dSYM and the dSYMs for every third-party framework or dependency that may appear in a crash stack trace.

To upload a single dSYM, pass its Mach-O debug binary to the CLI:

```sh
bd debug-files upload \
  --api-key "$BITDRIFT_API_KEY" \
  "/path/to/MyApp.app.dSYM/Contents/Resources/DWARF/MyApp"
```

To upload all of the dSYMs produced by an archive, pass the directory that contains them. The CLI searches it recursively and uploads each supported Mach-O debug binary it finds:

```sh
bd debug-files upload \
  --api-key "$BITDRIFT_API_KEY" \
  "/path/to/archive/dSYMs"
```

Before uploading each file, the CLI sends a `HEAD` request using the file's SHA-256 hash. If bitdrift already has that file, the CLI skips the upload, avoiding unnecessary transfer of unchanged symbol files.

For the most complete symbolication, run this command in CI for every release build after all dSYMs, including third-party dependency dSYMs, are available. You can instead upload files manually whenever a third-party dependency changes, but bitdrift does not retroactively re-symbolicate previously captured crash reports. Running the command in CI and relying on its `HEAD` check ensures new symbols are available before new crash reports arrive without repeatedly uploading unchanged files.

### Direct Native Symbol API Uploads (iOS / Android)

If you cannot use the `bd` CLI, call the debug information upload API directly. It accepts native Mach-O debug binaries from Apple dSYMs and ELF debug binaries from Android. Make one API upload for every binary that needs symbolication, including relevant third-party dependencies. Unlike the CLI, the API does not recursively discover files in a directory.

The following example checks whether the file has already been uploaded before sending it. Replace `FILE` with either a Mach-O debug binary from an Apple dSYM or an Android ELF debug binary, and run it once for every relevant binary:

```sh
# iOS: /path/to/MyApp.app.dSYM/Contents/Resources/DWARF/MyApp
# Android: /path/to/libmy-library.so
FILE="/path/to/native-debug-binary"
UPLOAD_URL="https://api.bitdrift.io/v1/debug_info/upload"
SHA256="$(openssl dgst -sha256 -r "$FILE" | awk '{print $1}')"
FILE_SIZE="$(wc -c < "$FILE" | tr -d ' ')"
QUERY="file_type=debug_file&sha256=$SHA256"

http_status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
  --request HEAD \
  --header "x-bitdrift-api-key: $BITDRIFT_API_KEY" \
  --header "content-length: 0" \
  "$UPLOAD_URL?$QUERY")"

case "$http_status" in
  200)
    echo "Debug information file already uploaded; skipping."
    ;;
  404)
    curl --fail --show-error \
      --request POST \
      --header "x-bitdrift-api-key: $BITDRIFT_API_KEY" \
      --header "content-length: $FILE_SIZE" \
      --data-binary "@$FILE" \
      "$UPLOAD_URL?$QUERY"
    ;;
  *)
    echo "Unexpected response to debug information file HEAD request: $http_status" >&2
    exit 1
    ;;
esac
```

The API requires the `x-bitdrift-api-key` header, a `sha256` query parameter containing the lowercase hexadecimal SHA-256 hash of the complete file, and `file_type=debug_file`. A `200 OK` response to `HEAD` means the file already exists; only send the `POST` after a `404 Not Found` response. Any other response, including an authentication or server error, should stop the upload.

!!! note
    For BYOC deployments, replace `api.bitdrift.io` in `UPLOAD_URL` with `api.<your-base-domain>`.

### ProGuard Mapping and Source Map Uploads

ProGuard mappings and React Native source maps use different matching mechanisms from native symbols. Upload the artifact produced by the same release as the report you want to resolve.

#### ProGuard Mappings (Android)

ProGuard mappings deobfuscate Java and Kotlin frames. Unlike native debug binaries, a mapping file is scoped to a specific Android app release. The `bd debug-files upload-proguard` command uses the app ID, app version, and version code to associate the mapping with that release; the version code is sent to the API as `build_id`.

For a direct API integration, use `/v1/debug_info/upload_app_scoped` and the same `HEAD`-then-`POST` flow shown for native symbols above. Set the file and query values as follows:

```sh
FILE="./app/build/outputs/mapping/release/mapping.txt"
APP_ID="com.example.app"
APP_VERSION="1.2.3"
BUILD_ID="456" # Android version code
UPLOAD_URL="https://api.bitdrift.io/v1/debug_info/upload_app_scoped"
SHA256="$(openssl dgst -sha256 -r "$FILE" | awk '{print $1}')"
FILE_SIZE="$(wc -c < "$FILE" | tr -d ' ')"
QUERY="app_id=$APP_ID&app_version=$APP_VERSION&build_id=$BUILD_ID&file_type=proguard&platform=android&sha256=$SHA256"
```

Send the API key in `x-bitdrift-api-key`, send `content-length: 0` with the `HEAD` request, and send the mapping file with `POST` only after a `404 Not Found` response. A `200 OK` response means bitdrift already has the mapping file.

#### Source Maps (React Native)

Source maps resolve JavaScript frames in React Native reports. A source map is matched to its generated JavaScript bundle, so both files must come from the same release. The `bd debug-files upload-source-map` command packages the source map into a source bundle, derives its debug ID from the MD5 hash of the matching bundle, and uploads that source bundle as a debug file.

!!! warning
    Do not send a raw `.map` file to `/v1/debug_info/upload`. Use `bd debug-files upload-source-map` or the Gradle plugin's `bdUploadSourceMap` task so the source bundle and bundle-derived debug ID are created correctly. If you call the API directly, first create the source bundle, then upload it to `/v1/debug_info/upload` with `file_type=debug_file`, its SHA-256 hash, and the same `HEAD`-then-`POST` flow used for native symbols.

### Verifying Uploads & Symbolication Timing

There is currently no UI in the bitdrift dashboard to confirm that debug information files have been uploaded successfully. A successful exit code from the `bd` CLI (or Gradle task) indicates that the upload was accepted.

Once uploaded, new crash reports will be symbolicated within approximately one minute. Note that uploading debug information files does **not** retroactively re-symbolicate previously captured crash reports.
