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

# Getting started with observability for Android

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

This guide shows how to add LaunchDarkly observability to an Android application written in Kotlin or Java. When you follow this guide, you'll pair the Android SDK with two plugins that ship telemetry from the device back to LaunchDarkly. By the end, you'll have the SDK and plugins installed, observability and session replay turned on, and confirmation that data is flowing.

<Note>
  **This SDK is in early access**

  The Android observability plugin is in early access. Its APIs may change before the 1.x release.
</Note>

The Android SDK supports two complementary telemetry plugins:

* **Observability** captures errors, crashes, logs, and traces. Use it to debug failures in production, watch error rates for a release, and trace network calls end-to-end. Observability automatically reports uncaught exceptions and activity lifecycle events.
* **Session replay** captures UI snapshots of what a user encountered. Use it when a stack trace alone doesn't tell you why a user got stuck, and pair it with masking to keep sensitive fields out of the recording.

Both plugins install alongside the LaunchDarkly Android SDK and initialize in the same place. This guide explains how to enable both. To learn more about every configuration field, the full `LDObserve` method surface, and advanced distributed-tracing patterns, read the [Android SDK observability reference](/docs/sdk/observability/android).

## Prerequisites

To complete this guide, you need:

* An Android project targeting **Android 5.0 (API level 21) or higher**.
* The **LaunchDarkly Android SDK version 5.9.0 or later**.
* A build set up with **Gradle** (Groovy or Kotlin DSL).
* A **LaunchDarkly account** and a **mobile key** for the environment you're testing against. Find the mobile key on the **SDK keys** page under **Settings**. Keys are specific to each project and environment. Mobile keys are not secrets and are safe to ship in client-side code.

<Note>
  **Building UI with Jetpack Compose?**

  Mobile keys are safe to ship in client-side code; never embed a server-side SDK key in a client app.
</Note>

## Step 1: Install the plugin

Add both the LaunchDarkly Android SDK and the observability plugin as dependencies. In your module's `build.gradle`:

<CodeGroup>
  ```groovy title="build.gradle" lines wrap theme={null}
  implementation 'com.launchdarkly:launchdarkly-android-client-sdk:5.+'
  implementation 'com.launchdarkly:launchdarkly-observability-android:0.21.0'
  ```
</CodeGroup>

Then import the plugin where you'll initialize the client:

<CodeGroup>
  ```kotlin title="Import the plugin" lines wrap theme={null}
  import com.launchdarkly.sdk.*
  import com.launchdarkly.sdk.android.*
  import com.launchdarkly.observability.plugin.Observability
  import com.launchdarkly.sdk.android.integrations.Plugin
  ```
</CodeGroup>

## Step 2: Initialize the client

On Android, the LaunchDarkly client is initialized one time for the lifetime of the app, so the natural home is your `Application` subclass rather than an Activity. The `Observability` plugin constructor takes your application context and your mobile key:

<CodeGroup>
  ```kotlin title="BaseApplication.kt" expandable lines wrap theme={null}
  class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val mobileKey = "example-mobile-key"

        val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
            .mobileKey(mobileKey)
            .plugins(
                Components.plugins().setPlugins(
                    listOf(
                        Observability(this, mobileKey)
                    )
                )
            )
            .build()

        val context = LDContext.create("example-context-key")
        val client = LDClient.init(this, ldConfig, context, 0)
    }
  }
  ```
</CodeGroup>

To give your app a recognizable name in the LaunchDarkly UI, set `serviceName` through `ObservabilityOptions`:

<CodeGroup>
  ```kotlin title="Set serviceName" lines wrap theme={null}
  Observability(
    this,
    mobileKey,
    ObservabilityOptions(
        serviceName = "my-android-app",
        serviceVersion = "1.0.0"
    )
  )
  ```
</CodeGroup>

`serviceName` defaults to `observability-android` if you don't set it.

## Step 3: Turn on session replay

Add the `SessionReplay` plugin to the list — **after** `Observability`, per the ordering rule from Concepts:

<CodeGroup>
  ```kotlin title="Add session replay plugin" lines wrap theme={null}
  import com.launchdarkly.observability.plugin.Observability
  import com.launchdarkly.observability.replay.plugin.SessionReplay

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
        Components.plugins().setPlugins(
            listOf(
                Observability(this, mobileKey),
                SessionReplay()  // must come after Observability
            )
        )
    )
    .build()
  ```
</CodeGroup>

<Note>
  **Gating session replay on consent or a flag**

  If you need to wait for user consent or roll session replay out behind a feature flag, construct it with `ReplayOptions(enabled = false)`. When you're ready to enable it, call `LDReplay.start()`. This is the privacy-friendly path for regulated apps, so we recommend not recording until you have permission.
</Note>

## Step 4: Decide what to mask

By default, text inputs are masked and other text is visible. Rather than memorize every flag, pick the situation you're in. Masking is configured through a `PrivacyProfile` on `ReplayOptions`. There are several configuration options depending on your use case.

They are:

* **Production** — mask everything, including non-input text and views flagged by sensitive keywords.
* **Local debugging** — turn masking off on your own test data so you can see what's going on.
* **Balanced** — mask inputs and anything matching sensitive keywords (passwords, etc.) but keep ordinary labels visible.

<CodeGroup>
  ```kotlin title="Production" lines wrap theme={null}
  SessionReplay(
    options = ReplayOptions(
        privacyProfile = PrivacyProfile(
            maskTextInputs = true,
            maskText = true,
            maskBySemanticsKeywords = true
        )
    )
  )
  ```

  ```kotlin title="Local debugging" lines wrap theme={null}
  PrivacyProfile(
    maskTextInputs = false,
    maskText = false,
    maskBySemanticsKeywords = false
  )
  ```

  ```kotlin title="Balanced" lines wrap theme={null}
  PrivacyProfile(
    maskTextInputs = true,
    maskText = false,
    maskBySemanticsKeywords = true
  )
  ```
</CodeGroup>

The simple rule is to **start fully masked, and only unmask what you've confirmed is safe to record.**

<Frame caption="A session replay with masking applied: the password field is captured as a blank block while non-sensitive labels remain readable.">
  <img src="https://mintcdn.com/launchdarkly/hutarVphEq2dY_zb/images/auto/guide-masking-example-android-app-SR.png?fit=max&auto=format&n=hutarVphEq2dY_zb&q=85&s=fdabf9896ded77f91296c112d14b9f62" alt="An Android session replay showing a login screen where the password field is rendered as a solid block while surrounding labels remain visible, demonstrating text-input masking." width="1913" height="964" data-path="images/auto/guide-masking-example-android-app-SR.png" />
</Frame>

### Masking a specific element

When a profile is almost right but one field is the exception, override it at the element level. The specific mechanisms to do this differ based on which UI toolkit you use:

* **Jetpack Compose** — add the `.ldMask()` modifier (or `.ldUnmask()` to reveal something the profile would hide).
* **Traditional XML views** — retrieve the `View` with `findViewById`, then call the `.ldMask()` extension function on the result.

<CodeGroup>
  ```kotlin title="Jetpack Compose" lines wrap theme={null}
  import com.launchdarkly.observability.api.ldMask

  @Composable
  fun CreditCardField() {
    var number by remember { mutableStateOf("") }
    TextField(
        value = number,
        onValueChange = { number = it },
        modifier = Modifier
            .fillMaxWidth()
            .ldMask()  // mask this composable in session replay
    )
  }
  ```

  ```kotlin title="XML views" lines wrap theme={null}
  import com.launchdarkly.observability.api.ldMask

  class LoginActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        val password = findViewById<EditText>(R.id.password)
        password.ldMask()  // mask this field in session replay
    }
  }
  ```
</CodeGroup>

## Step 5: Run it and confirm data is arriving

Build and run on a device or emulator:

<CodeGroup>
  ```bash title="Build and run" lines wrap theme={null}
  ./gradlew installDebug
  ```
</CodeGroup>

Test out the app by navigating between a couple of screens. Trigger an error deliberately if you want to verify that Observability captures it correctly. Because crash reporting is on by default, an uncaught exception shows up as an error automatically.

Then open the [Observability](/docs/home/observability) page in the LaunchDarkly UI. Logs and errors attributed to your `serviceName` appear there, as well as a session replay recording you can scrub through with your masking choices applied. When your service name appears in Observability, that is a confirmation that the SDK initialized successfully and is recording data.

<Frame caption="Logs and errors arriving in LaunchDarkly, attributed to the `serviceName` you set in Step 2.">
  <img src="https://mintcdn.com/launchdarkly/hutarVphEq2dY_zb/images/auto/guide-android-app-observability-logs.png?fit=max&auto=format&n=hutarVphEq2dY_zb&q=85&s=6968c594119d955ee1f5d9f4de7deb36" alt="The LaunchDarkly observability view listing logs and errors from an Android app, attributed to the service name set during initialization." width="1911" height="954" data-path="images/auto/guide-android-app-observability-logs.png" />
</Frame>

Here is an example session replay from an Android app in LaunchDarkly:

<div style={{ position: 'relative', width: '100%', paddingBottom: '56.25%', height: 0 }}>
  <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/SPR-Q_Vp-Yw" title="LaunchDarkly session replay of an Android app" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen />
</div>

## Complete code sample

<CodeGroup>
  ```kotlin title="BaseApplication.kt (complete)" expandable lines wrap theme={null}
  import android.app.Application
  import com.launchdarkly.sdk.*
  import com.launchdarkly.sdk.android.*
  import com.launchdarkly.observability.plugin.Observability
  import com.launchdarkly.observability.replay.plugin.SessionReplay
  import com.launchdarkly.observability.replay.ReplayOptions
  import com.launchdarkly.observability.replay.PrivacyProfile

  class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val mobileKey = "example-mobile-key"

        val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
            .mobileKey(mobileKey)
            .plugins(
                Components.plugins().setPlugins(
                    listOf(
                        Observability(
                            this,
                            mobileKey,
                            ObservabilityOptions(
                                serviceName = "my-android-app",
                                serviceVersion = "1.0.0"
                            )
                        ),
                        SessionReplay(
                            options = ReplayOptions(
                                privacyProfile = PrivacyProfile(
                                    maskTextInputs = true,
                                    maskText = true,
                                    maskBySemanticsKeywords = true
                                )
                            )
                        )
                    )
                )
            )
            .build()

        val context = LDContext.create("example-context-key")
        val client = LDClient.init(this, ldConfig, context, 0)
    }
  }
  ```
</CodeGroup>

## What to explore next

* **Automatic HTTP and interaction instrumentation**: Add the ByteBuddy Gradle plugin and the OkHttp or HttpURLConnection instrumentation dependencies to trace network calls automatically. To learn more, read the Android SDK docs' [Configure additional instrumentations](/docs/sdk/observability/android#configure-additional-instrumentations).
* **Every configuration field**: The complete `ObservabilityOptions`, including `instrumentations`, `tracesApi`, `metricsApi`, and `logsApiLevel`, and `ReplayOptions` tables are in the [SDK reference documentation](/docs/sdk/observability/android) and in [Configuration for client-side observability](/docs/sdk/features/observability-config-client-side).
* **Custom and distributed tracing**: Create spans with `LDObserve.startSpan(name, attributes)`, and propagate context across coroutines, threads, and services using the OpenTelemetry `Context` API and `W3CTraceContextPropagator`.
* **Working with your data**: When telemetry is flowing, set up alerts and dashboards, then investigate issues with [Vega](/docs/home/getting-started/vega).

## Troubleshooting

Use this section to investigate issues.

### No data appearing

* Confirm you pasted the **mobile key** for the environment you're viewing in the UI, and not for a different environment.
* Make sure initialization runs in your `Application` subclass and that the subclass is registered in `AndroidManifest.xml` with `android:name`.
* Test the app a bit more and refresh. Data isn't always instantaneous, so it may take a few minutes to display correctly.

### Session replay isn't recording, but observability works

* Check plugin order: `SessionReplay` must be listed **after** `Observability`. If it's first or `Observability` is absent, session replay logs an error and stays inactive.
* If you configured `ReplayOptions(enabled = false)` for consent-gated rollout, confirm `LDReplay.start()` is actually being called.

<Frame caption="Android Observability SDK plugin ordering error">
  <img src="https://mintcdn.com/launchdarkly/A4UXoRTW8ATNw4yq/images/auto/observability_plugins_matter.png?fit=max&auto=format&n=A4UXoRTW8ATNw4yq&q=85&s=715416061ee2e7722925252625c4578a" alt="Android Observability SDK plugin ordering error." width="4112" height="2658" data-path="images/auto/observability_plugins_matter.png" />
</Frame>

### Masking not applied to a specific view

* For Compose, confirm `.ldMask()` is applied as a `Modifier` on the composable. For XML, confirm `.ldMask()` is called on the resolved `View` after `findViewById`.

## Conclusion

In this guide, you instrumented an Android application for LaunchDarkly observability. You can now:

* Capture errors, crashes, logs, and session replays from a Kotlin or Java app
* Control what session replay records through `PrivacyProfile` presets and per-element masking in both Compose and XML
* Confirm telemetry is flowing in the LaunchDarkly UI, ready for alerts, dashboards, and Vega
