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

# Android SDK observability reference

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

<Note>
  **This LaunchDarkly observability plugin is available for early access**

  This LaunchDarkly observability plugin is currently available in Early Access, and APIs are subject to change until a 1.x version is released.
</Note>

This topic documents how to get started with the LaunchDarkly observability plugin for the Android SDK.

The Android SDK supports the **observability plugin** for error monitoring, logging, tracing, and **session replay**.

<Note>
  **SDK quick links**

  LaunchDarkly SDKs are open source. In addition to this reference guide, we provide source, API reference documentation, and a sample application:

  <table>
    <thead>
      <tr>
        <th>Resource</th>
        <th>Location</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td>SDK API documentation</td>

        <td>
          [Observability plugin API
          docs](https://launchdarkly.github.io/observability-sdk/sdk/@launchdarkly/observability-android/)
        </td>
      </tr>

      <tr>
        <td>GitHub repository</td>

        <td>
          [@launchdarkly/observability-android](https://github.com/launchdarkly/observability-sdk/tree/main/sdk/%40launchdarkly/observability-android)
        </td>
      </tr>

      <tr>
        <td>Published module</td>
        <td>[Maven](https://mvnrepository.com/artifact/com.launchdarkly/launchdarkly-observability-android)</td>
      </tr>
    </tbody>
  </table>
</Note>

## Prerequisites and dependencies

This reference guide assumes that you are somewhat familiar with the LaunchDarkly [Android SDK](/docs/sdk/client-side/android).

The observability plugin is compatible with the [Android SDK](/docs/sdk/client-side/android), version 5.9.0 and later.

The LaunchDarkly Android SDK is compatible with Android SDK versions 21 and higher (Android 5.0, Lollipop).

## Get started

Follow these steps to get started:

* [Install the plugin](#install-the-plugin)
* [Initialize the Android SDK client](#initialize-the-client)
* [Configure the plugin options](#configure-the-plugin-options)
* [Configure additional instrumentations](#configure-additional-instrumentations)
* [Configure product analytics event collection](#configure-product-analytics-event-collection)
* [Track screen views](#track-screen-views)
* [Configure session replay](#configure-session-replay)
* [Explore supported features](#explore-supported-features)
* [Review observability data in LaunchDarkly](#review-observability-data-in-launchdarkly)

## Install the plugin

LaunchDarkly uses a plugin to the Android SDK to provide observability.

The first step is to make both the SDK and the observability plugin available as dependencies.

Here's how:

<CodeGroup>
  ```java title="Gradle Groovy" lines wrap theme={null}
  implementation 'com.launchdarkly:launchdarkly-android-client-sdk:5.+'
  implementation 'com.launchdarkly:launchdarkly-observability-android:0.21.0'
  ```

  ```kotlin title="Gradle Kotlin" 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 into your code:

<CodeGroup>
  ```java title="Import for Java" 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;
  ```

  ```kotlin title="Import for Kotlin" 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>

## Initialize the client

Next, initialize the SDK and the plugin.

To initialize, you need your LaunchDarkly environment's mobile key and the context for which you want to evaluate flags. This authorizes your application to connect to a particular environment within LaunchDarkly. To learn more, read [Initialize the client](/docs/sdk/client-side/android#initialize-the-client) in the Android SDK reference guide.

<Warning>
  **Android observability SDK credentials**

  The Android observability SDK uses a mobile key. Keys are specific to each project and environment. They are available on the **SDK keys** page under **Settings**. To learn more about key types, read [Keys](/docs/sdk/concepts/client-side-server-side#keys-and-credentials).

  Mobile keys are not secret and you can expose them in your client-side code without risk. However, never embed a server-side SDK key into a client-side application.
</Warning>

Here's how to initialize the SDK and plugin:

<CodeGroup>
  ```java title="Android SDK v5.x (Java)" lines wrap theme={null}
  String mobileKey = "example-mobile-key";

  LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(Components.plugins().setPlugins(
      Collections.singletonList<Plugin>(Observability(this.getApplication(), mobileKey))
    ))
    // other options
    .build();

  // You'll need this context later, but you can ignore it for now.
  LDContext context = LDContext.create("example-context-key");

  LDClient client = LDClient.init(this.getApplication(), ldConfig, context, 0);
  ```

  ```kotlin title="Android SDK v5.x (Kotlin)" lines wrap theme={null}
  val mobileKey = "example-mobile-key"

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(Components.plugins().setPlugins(
      listOf(Observability(this@BaseApplication, mobileKey))
    ))
    // other options
    .build()

  // You'll need this context later, but you can ignore it for now.
  val context = LDContext.create("example-context-key")

  val client: LDClient = LDClient.init(this@BaseApplication, ldConfig, context, 0)
  ```
</CodeGroup>

## Configure the plugin options

You can configure options for the observability plugin when you initialize the SDK. The plugin constructor takes an optional object with the configuration details.

Here is an example:

<CodeGroup>
  ```java title="Plugin options, Android SDK v5.9+" expandable lines wrap theme={null}
  String mobileKey = "example-mobile-key";

  LDConfig ldConfig = new LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        Collections.singletonList<Plugin>(
          Observability(
            this.getApplication(),
            mobileKey,
            ObservabilityOptions(
              resourceAttributes = Attributes.of(
                AttributeKey.stringKey("serviceName"), "example-service"
              )
            )
          )
        )
      )
    )
    .build();
  ```
</CodeGroup>

For more information on plugin options, read [Configuration for client-side observability](/docs/sdk/features/observability-config-client-side).

### Advanced configuration options

You can customize the observability plugin with additional options:

<CodeGroup>
  ```kotlin title="Advanced options example" expandable lines wrap theme={null}
  val mobileKey = "example-mobile-key"

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(
            this@BaseApplication,
            mobileKey,
            ObservabilityOptions(
              serviceName = "my-android-app",
              serviceVersion = "1.0.0",
              debug = true,
              logsApiLevel = ObservabilityOptions.LogLevel.WARN,
              tracesApi = ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = false),
              metricsApi = ObservabilityOptions.MetricsApi.disabled(),
              instrumentations = ObservabilityOptions.Instrumentations(
                crashReporting = false,
                launchTime = true,
                userTaps = true,
                screens = true
              ),
              resourceAttributes = Attributes.of(
                AttributeKey.stringKey("environment"), "production",
                AttributeKey.stringKey("team"), "mobile"
              ),
              customHeaders = mapOf(
                "X-Custom-Header" to "custom-value"
              )
            )
          )
        )
      )
    )
    .build()
  ```
</CodeGroup>

The available `ObservabilityOptions` configuration options are:

* **logsApiLevel**: Minimum log severity to export. Defaults to `INFO`. Set to `ObservabilityOptions.LogLevel.NONE` to disable log exporting.
* **tracesApi**: Controls trace recording. Defaults to enabled. Use `ObservabilityOptions.TracesApi.disabled()` to disable all tracing, or set `includeErrors`/`includeSpans` individually.
* **metricsApi**: Controls metric export. Defaults to enabled. Use `ObservabilityOptions.MetricsApi.disabled()` to disable metrics.
* **instrumentations**: Enables or disables specific automatic instrumentations:
  * `crashReporting`: If `true`, automatically reports uncaught exceptions as errors. Defaults to `true`.
  * `launchTime`: If `true`, automatically measures and reports application startup time as metrics. Defaults to `false`.
  * `userTaps`: If `true`, runs tap detection. The `analytics.taps` option separately controls whether the plugin publishes detected taps as `click` spans. If `userTaps` is `false`, the plugin publishes no `click` spans regardless of the `analytics.taps` value. Neither option affects session replay capture. Defaults to `true`.
  * `screens`: If `true`, automatically detects screen changes from Android `Activity` lifecycle callbacks. Screen detection drives both the automatic `screen_view` span and the session replay `Navigate` events. Defaults to `true`. The `analytics.screenViews` option gates `screen_view` spans.
* **sessionBackgroundTimeout**: How long the app can stay in the background before the current session ends. In Kotlin, this is a `kotlin.time.Duration`, such as `30.minutes`. In Java, use the `sessionBackgroundTimeoutMillis(long)` builder method instead. Defaults to 15 minutes.
* **serviceName**: The service name for the application. Defaults to "observability-android".
* **serviceVersion**: The version of the service. Defaults to the SDK version.
* **debug**: Enables verbose internal logging and debug functionality. Defaults to `false`.
* **resourceAttributes**: Additional resource attributes to include in telemetry data.
* **customHeaders**: Custom headers to include with OTLP exports.

## Configure additional instrumentations

To enable HTTP request instrumentation and user interaction instrumentation, add the following plugin and dependencies to your top level application's Gradle file.

<CodeGroup>
  ```java title="Gradle Groovy" lines wrap theme={null}
  plugins {
      id 'net.bytebuddy.byte-buddy-gradle-plugin' version '1.+'
  }

  dependencies {
      // Android HTTP Url instrumentation
      implementation 'io.opentelemetry.android.instrumentation:httpurlconnection-library:0.11.0-alpha'
      byteBuddy 'io.opentelemetry.android.instrumentation:httpurlconnection-agent:0.11.0-alpha'

      // OkHTTP instrumentation
      implementation 'io.opentelemetry.android.instrumentation:okhttp3-library:0.11.0-alpha'
      byteBuddy 'io.opentelemetry.android.instrumentation:okhttp3-agent:0.11.0-alpha'
  }
  ```

  ```kotlin title="Gradle Kotlin" lines wrap theme={null}
  plugins {
      id("net.bytebuddy.byte-buddy-gradle-plugin") version "1.+"
  }

  dependencies {
      // Android HTTP Url instrumentation
      implementation("io.opentelemetry.android.instrumentation:httpurlconnection-library:0.11.0-alpha")
      byteBuddy("io.opentelemetry.android.instrumentation:httpurlconnection-agent:0.11.0-alpha")

      // OkHTTP instrumentation
      implementation("io.opentelemetry.android.instrumentation:okhttp3-library:0.11.0-alpha")
      byteBuddy("io.opentelemetry.android.instrumentation:okhttp3-agent:0.11.0-alpha")
  }
  ```
</CodeGroup>

## Configure product analytics event collection

The Android observability plugin can record the following product analytics events as OpenTelemetry spans:

* **Taps** (automatic): A `click` span for each user tap, with details about the tapped element and screen location. Enabled by default.
* **Track events** (automatic): A `track` span when your code calls `track()`. Enabled by default. To learn more, read [Recording product analytics events](#recording-product-analytics-events).
* **Screen views** (automatic and manual): A `screen_view` span when the app shows a screen, either detected automatically or recorded manually when your code calls `trackScreenView()`. To learn more, read [Recording product analytics events](#recording-product-analytics-events).
* **App lifecycle** (automatic): An `app_foreground` or `app_background` span as the app moves between the foreground and background states. Enabled by default.
* **App launches** (automatic): An `app_launch` span once per process launch, with the launch type and version information, plus an `app.start` span event that records the cold or warm startup dimension. Enabled by default. To learn more, read [App launch events](/docs/home/observability/product-analytics#app-launch-events).

All product analytics span events also include information about the LaunchDarkly context that generated the event.

Use the generated span events to create custom product analytics charts, such as time series and funnels. To learn more, read [Product analytics events](/docs/home/observability/product-analytics).

The plugin records all event types by default. To customize, pass an `ObservabilityOptions.Analytics` object to the `analytics` parameter:

<CodeGroup>
  ```kotlin title="Customize individual event types" lines wrap theme={null}
  ObservabilityOptions(
  analytics = ObservabilityOptions.Analytics(
    taps = true,
    trackEvents = true,
    screenViews = true,
    appLifecycle = true,
    appLaunch = true,
  )
  )
  ```
</CodeGroup>

The `ObservabilityOptions.Analytics` options are:

* **taps**: Emits a `click` span for each detected tap. Defaults to `true`.
* **trackEvents**: Emits a `track` span when a custom event is tracked with `track()`. Defaults to `true`.
* **screenViews**: Emits a `screen_view` span when the app displays a screen. Defaults to `true`.
* **appLifecycle**: Emits `app_foreground` and `app_background` spans as the app moves between states. Defaults to `true`.
* **appLaunch**: Emits an `app_launch` span once per process launch. Defaults to `true`.

## Track screen views

The plugin emits a `screen_view` span each time your app shows a screen. Each `screen_view` span uses the `event.*` attribute namespace, and includes the `event.previous_screen` attribute from a shared navigation stack. The span broadcasts a session replay `Navigate` event to reflect navigation in the replay timeline.

### Capture activities automatically

When `instrumentations.screens` is `true` (the default), the plugin captures every Android `Activity` as it resumes. It derives the screen name from the class name. For example, `ProfileActivity` becomes `Profile`. The plugin populates the `event.screen_class` and `event.screen_id` attributes from the `Activity` class.

To customize the reported name or category, implement `LDScreenNameProvider` on your `Activity`:

<CodeGroup>
  ```kotlin title="Custom screen name (Kotlin)" lines wrap theme={null}
  import com.launchdarkly.observability.client.screen.LDScreenNameProvider

  class CheckoutActivity : ComponentActivity(), LDScreenNameProvider {
    override val ldScreenName: String = "Checkout"
    override val ldScreenCategory: String = "Commerce"
  }
  ```
</CodeGroup>

### Capture fragments and compose destinations manually

The plugin does not automatically capture screens that lack a distinct `Activity`, such as Fragments and Jetpack Compose destinations. Record these kinds of screens with `LDObserve.trackScreenView`. To learn more, read [Recording product analytics events](#recording-product-analytics-events).

Only one call per appearance is required. The plugin resolves `event.previous_screen` through the shared navigation stack, which handles both re-appearance and back navigation.

### About detection and span emission

Two separate options control screen views:

* `instrumentations.screens` controls automatic screen detection. It drives both the automatic `screen_view` span and the session replay `Navigate` event.
* `analytics.screenViews` only gates the `screen_view` span. Detection and the `Navigate` event are independent of this option, and manual `trackScreenView` calls still work if you disable `instrumentations.screens`.

## Configure session replay

The Android SDK supports session replay, which captures snapshots of your app's UI at regular intervals. This helps you review user sessions in LaunchDarkly to better understand user behavior and diagnose issues.

To enable session replay, add the `SessionReplay` plugin to the plugins list **after** the `Observability` plugin. Session replay depends on the Observability plugin being present and initialized first.

Here's how:

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

  val mobileKey = "example-mobile-key"

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(this@BaseApplication, mobileKey),
          SessionReplay()  // depends on Observability being present first
        )
      )
    )
    .build()
  ```
</CodeGroup>

**Important notes:**

* `SessionReplay` depends on `Observability`. If Observability is missing or listed after SessionReplay, the plugin logs an error and stays inactive.
* Observability runs fine without SessionReplay. Adding SessionReplay extends the Observability pipeline to include session recording.

### Initialize the plugins after the SDK client

You can initialize the session replay plugin manually, after the SDK client is initialized.

This approach supports feature-flagged rollouts or dynamic initialization after end user consent. Set `enabled` to `false` in `ReplayOptions`, then call `LDReplay.start()` when you're ready to begin recording.

First, configure the plugin with `enabled = false`:

<CodeGroup>
  ```kotlin title="Manual start configuration" expandable lines wrap theme={null}
  import com.launchdarkly.observability.plugin.Observability
  import com.launchdarkly.observability.replay.plugin.SessionReplay
  import com.launchdarkly.observability.replay.ReplayOptions

  val mobileKey = "example-mobile-key"

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(this@BaseApplication, mobileKey),
          SessionReplay(
            options = ReplayOptions(
              enabled = false // Don't start recording automatically
            )
          )
        )
      )
    )
    .build()

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

Then, start the session replay plugin when appropriate, such as after receiving end user consent or when a feature flag enables session replay.

<CodeGroup>
  ```kotlin title="Start recording" lines wrap theme={null}
  import com.launchdarkly.observability.sdk.LDReplay

  // Start recording after user consent or feature flag check
  LDReplay.start()
  ```

  ```kotlin title="Start recording with feature flag" lines wrap theme={null}
  import com.launchdarkly.observability.sdk.LDReplay

  // Start recording based on a feature flag
  val replayEnabled = LDClient.get().boolVariation("enable-session-replay", false)
  if (replayEnabled) {
    LDReplay.start()
  }
  ```

  ```kotlin title="Stop recording" lines wrap theme={null}
  import com.launchdarkly.observability.sdk.LDReplay

  LDReplay.stop()
  ```

  ```kotlin title="Flush buffered events" lines wrap theme={null}
  import com.launchdarkly.observability.sdk.LDReplay

  // Immediately export any queued replay events
  LDReplay.flush()
  ```
</CodeGroup>

This approach allows you to:

* Feature-flag the rollout of session replay to a subset of end users
* Wait for end user consent before starting data collection
* Dynamically enable session replay based on runtime conditions
* Maintain compliance with privacy regulations

### Session replay configuration options

You can customize session replay behavior by passing a `ReplayOptions` object to the `SessionReplay` constructor:

<CodeGroup>
  ```kotlin title="Session replay options" expandable lines wrap theme={null}
  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

  val mobileKey = "example-mobile-key"

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(this@BaseApplication, mobileKey),
          SessionReplay(
            options = ReplayOptions(
              privacyProfile = PrivacyProfile(
                maskTextInputs = true,
                maskText = true
              ),
              capturePeriodMillis = 1000,
              debug = false
            )
          )
        )
      )
    )
    .build()
  ```
</CodeGroup>

The available `ReplayOptions` configuration options are:

* **privacyProfile**: Controls how UI elements are masked in the replay. To learn more, read [Privacy options](#privacy-options).
* **capturePeriodMillis**: Period between UI captures in milliseconds. Defaults to `1000` (1 second).
* **debug**: Enables verbose logging when set to `true`. Defaults to `false`.

**Note:** Service configuration options like `serviceName` and `serviceVersion` are set in `ObservabilityOptions`, not in `ReplayOptions`.

### Privacy options

The `PrivacyProfile` class controls how UI elements are masked during session replay. Session replay for Android uses Jetpack Compose semantics to identify and mask UI elements. By default, text inputs are masked to protect user privacy.

Here's how to configure privacy settings:

<CodeGroup>
  ```kotlin title="Privacy profile configuration" expandable lines wrap theme={null}
  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

  val mobileKey = "example-mobile-key"

  val ldConfig = LDConfig.Builder(AutoEnvAttributes.Enabled)
    .mobileKey(mobileKey)
    .plugins(
      Components.plugins().setPlugins(
        listOf(
          Observability(this@BaseApplication, mobileKey),
          SessionReplay(
            options = ReplayOptions(
              privacyProfile = PrivacyProfile(
                maskTextInputs = true,
                maskText = false
              )
            )
          )
        )
      )
    )
    .build()
  ```
</CodeGroup>

The available privacy options are:

* **maskTextInputs**: When `true`, masks all text input fields including editable text and paste operations. Defaults to `true`.
* **maskText**: When `true`, masks all non-input text elements in the UI. Defaults to `false`.
* **maskBySemanticsKeywords**: When `true`, masks sensitive views that contain password fields or text matching sensitive keywords. Defaults to `false`.
* **maskViews**: A list of Android `View` classes to mask. Because matching uses the exact class names, subclasses do not match. Use the `view()` helper with either a Kotlin class or a fully qualified class name.
* **maskWebViews**: When `true`, masks the contents of web views using a default list of WebView class names. This includes subclasses, and web views hosted inside Jetpack Compose through `AndroidView`. Defaults to `false`.
* **maskXMLViewIds**: A list of resource IDs to mask. Accepts the `@+id/example`, `@id/example`, or `example` format.
* **unmaskXMLViewIds**: A list of resource IDs to unmask. Uses the same format as `maskXMLViewIds`.

#### Sensitive keywords

When `maskBySemanticsKeywords` is enabled, the SDK automatically masks any Compose UI text or content descriptions containing predetermined keywords. Keywords you specify are not case-sensitive. For the current set of keywords, read [`PrivacyProfile`](https://github.com/launchdarkly/observability-sdk/blob/main/sdk/%40launchdarkly/observability-android/lib/src/main/kotlin/com/launchdarkly/observability/replay/PrivacyProfile.kt).

#### Common privacy configurations

For maximum privacy (recommended for production):

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

For debugging or development, you can turn masking off:

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

For selective masking, which masks inputs and sensitive data but shows regular text:

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

### Custom masking with ldMask

In addition to the privacy profile settings, you can explicitly mask individual UI elements by using the `.ldMask()` modifier. This is useful when you need to mask specific sensitive fields while allowing other content to remain visible.

#### Masking XML views

For traditional Android XML-based views, you can mask any `View` by calling the `.ldMask()` extension function:

<CodeGroup>
  ```kotlin title="Masking 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>

#### Masking Jetpack Compose elements

For Jetpack Compose, you can add the `.ldMask()` modifier to any composable to mask it in session replay recordings:

<CodeGroup>
  ```kotlin title="Masking Compose elements" 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
    )
  }
  ```
</CodeGroup>

#### Unmasking elements

You can also explicitly unmask elements that would otherwise be masked by the privacy profile settings using the `.ldUnmask()` modifier:

<CodeGroup>
  ```kotlin title="Unmasking elements" lines wrap theme={null}
  import com.launchdarkly.observability.api.ldUnmask

  @Composable
  fun PublicInfoField() {
    var info by remember { mutableStateOf("") }
    TextField(
        value = info,
        onValueChange = { info = it },
        modifier = Modifier
            .fillMaxWidth()
            .ldUnmask() // explicitly unmask this field
    )
  }
  ```
</CodeGroup>

The `.ldMask()` and `.ldUnmask()` modifiers give you fine-grained control over which UI elements are masked in session replay recordings, allowing you to balance privacy protection with useful debugging information.

#### Mask by resource ID

Instead of calling `.ldMask()` on each element, you can mask or unmask XML views by their resource ID. Use the `maskXMLViewIds` and `unmaskXMLViewIds` options in your `PrivacyProfile`.

Here is an example:

<CodeGroup>
  ```kotlin title="Mask by resource ID" expandable lines wrap theme={null}
  import com.launchdarkly.observability.replay.PrivacyProfile
  import com.launchdarkly.observability.replay.ReplayOptions
  import com.launchdarkly.observability.replay.view
  import com.launchdarkly.observability.replay.plugin.SessionReplay

  val sessionReplay = SessionReplay(
    ReplayOptions(
        privacyProfile = PrivacyProfile(
            maskTextInputs = true,
            maskViews = listOf(
                view(android.widget.ImageView::class),
                view("android.widget.EditText"),
            ),
            maskWebViews = true,
            maskXMLViewIds = listOf(
                "@+id/password",
                "credit_card_number",
            ),
            unmaskXMLViewIds = listOf(
                "@+id/greeting",
            ),
        )
    )
  )
  ```
</CodeGroup>

These options apply to views with an ID that resolves to a resource entry name. `unmaskXMLViewIds` takes precedence over global rules such as `maskText` and `maskTextInputs`, but an explicit mask on the same view or any of its parent views still wins. To learn more, read [Masking precedence](#masking-precedence).

#### Masking precedence

When the SDK decides whether to mask a view, it evaluates the following rules in order and stops at the first rule that applies:

1. **Explicit masking**: If the view or any of its parent views is explicitly masked with `.ldMask()` or a matching `maskXMLViewIds` entry, the SDK masks the view. This overrides all other rules.
2. **Explicit unmasking**: If the view or any of its parent views is explicitly unmasked with `.ldUnmask()` or a matching `unmaskXMLViewIds` entry, the SDK does not mask the view.
3. **Global configuration**: If a global privacy option such as `maskTextInputs` or `maskText` applies to the view, the SDK follows that option.

If rules conflict at the same level, masking wins over unmasking.

For more information on session replay configuration, read [Configuration for session replay](/docs/sdk/features/session-replay-config).

## Manual instrumentation

After initializing the observability plugin, use the `LDObserve` singleton to manually instrument your Android application with custom metrics, logs, errors, traces, and product analytics events.

### Recording custom metrics

Use the metric methods on `LDObserve` to record a `Metric` with a name and a value. Each method maps to a different OpenTelemetry instrument type:

<CodeGroup>
  ```kotlin title="Record metrics (Kotlin)" expandable lines wrap theme={null}
  import com.launchdarkly.observability.sdk.LDObserve
  import com.launchdarkly.observability.interfaces.Metric

  // Record a raw measurement
  LDObserve.recordMetric(Metric("user_actions", 1.0))

  // Record a monotonically increasing counter
  LDObserve.recordCount(Metric("api_calls", 1.0))

  // Increment a counter
  LDObserve.recordIncr(Metric("page_views", 1.0))

  // Record a value distribution
  LDObserve.recordHistogram(Metric("response_time", 150.0))

  // Record a value that can increase or decrease
  LDObserve.recordUpDownCounter(Metric("active_connections", 1.0))
  ```
</CodeGroup>

### Recording custom logs

Use `LDObserve.recordLog` to emit a structured log. Pass attributes as a plain Kotlin map through the `properties` parameter, or use the `Attributes` overload when you need precise OpenTelemetry typing:

<CodeGroup>
  ```kotlin title="Record logs (Kotlin)" lines wrap theme={null}
  // Record a basic log message
  LDObserve.recordLog(
    message = "User login successful",
    severity = Severity.INFO
  )

  // Record logs with custom properties
  LDObserve.recordLog(
    message = "Authentication completed",
    severity = Severity.INFO,
    properties = mapOf(
        "user_id" to "12345",
        "action" to "login"
    )
  )
  ```

  ```kotlin title="Record logs with OTel-typed attributes (advanced)" lines wrap theme={null}
  // Use OTel-typed attributes when exact OpenTelemetry typing is required
  LDObserve.recordLog(
    message = "Authentication completed",
    severity = Severity.INFO,
    attributes = Attributes.of(
        AttributeKey.stringKey("user_id"), "12345",
        AttributeKey.stringKey("action"), "login"
    )
  )
  ```
</CodeGroup>

### Recording custom errors

Use `LDObserve.recordError` to report a caught exception, with optional attributes for context:

<CodeGroup>
  ```kotlin title="Record errors (Kotlin)" lines wrap theme={null}
  import com.launchdarkly.observability.sdk.LDObserve
  import io.opentelemetry.api.common.AttributeKey
  import io.opentelemetry.api.common.Attributes

  try {
    processPayment()
  } catch (e: Exception) {
    LDObserve.recordError(
        e,
        Attributes.of(
            AttributeKey.stringKey("component"), "payment",
            AttributeKey.stringKey("error_code"), "PAYMENT_FAILED"
        )
    )
  }
  ```
</CodeGroup>

The plugin also reports uncaught exceptions automatically while `instrumentations.crashReporting` remains `true`, which is the default.

### Recording custom traces

Use `LDObserve.startSpan` to create a span for tracing an operation. Always end the span when the operation completes:

<CodeGroup>
  ```kotlin title="Record traces (Kotlin)" lines wrap theme={null}
  // Start a span with custom properties
  val span = LDObserve.startSpan(
    name = "database_query",
    properties = mapOf(
        "table" to "users",
        "operation" to "select"
    )
  )

  // Perform your operation
  performDatabaseQuery()

  // Always end the span
  span.end()
  ```

  ```kotlin title="Record traces with OTel-typed attributes (advanced)" lines wrap theme={null}
  // Use OTel-typed attributes when exact OpenTelemetry typing is required
  val span = LDObserve.startSpan(
    name = "database_query",
    attributes = Attributes.of(
        AttributeKey.stringKey("table"), "users",
        AttributeKey.stringKey("operation"), "select"
    )
  )
  performDatabaseQuery()
  span.end()
  ```
</CodeGroup>

### Recording product analytics events

Use `track` to record a custom event as a product analytics span. Use `trackScreenView` to record a screen view:

<CodeGroup>
  ```kotlin title="Track a custom event (Kotlin)" lines wrap theme={null}
  // Track an event with properties and an optional metric value
  LDObserve.track(
    key = "purchase_completed",
    properties = mapOf(
        "product_id" to "SKU-123",
        "price" to 29.99
    ),
    metricValue = 29.99
  )

  // Track an event with no properties
  LDObserve.track(key = "button_tapped")
  ```
</CodeGroup>

<CodeGroup>
  ```kotlin title="Track a screen view (Kotlin)" expandable lines wrap theme={null}
  // Convenience — name only
  LDObserve.trackScreenView(name = "ProductDetail")

  // With category
  LDObserve.trackScreenView(name = "Checkout", category = "purchase")

  // Full details with custom properties
  LDObserve.trackScreenView(
    name = "ProductDetail",
    screenClass = "ProductDetailActivity",
    screenId = "product-123",
    category = "browsing",
    properties = mapOf(
        "product_id" to "SKU-123",
        "source" to "search_results"
    )
  )
  ```
</CodeGroup>

## Distributed tracing

To implement distributed tracing, all spans generated across different function calls and services for the same user request must share the same context.

While nested spans in the same synchronous scope automatically use the context of their parent span, the context is not automatically propagated when launching new coroutines or switching dispatchers. Use the OpenTelemetry `Context` API to capture and restore the parent span context explicitly in asynchronous code:

<CodeGroup>
  ```java title="Capture and restore span context across threads (Java)" expandable lines wrap theme={null}
  import io.opentelemetry.context.Context;
  import io.opentelemetry.context.Scope;

  Span parentSpan = LDObserve.startSpan("parentSpan", new HashMap<>());
  try (Scope parentScope = parentSpan.makeCurrent()) {
    Context context = Context.current();

    new Thread(() -> {
        try (Scope childScope = context.makeCurrent()) {
            Span childSpan = LDObserve.startSpan("childSpan", new HashMap<>());
            // do work
            childSpan.end();
        }
    }).start();
  } finally {
    parentSpan.end();
  }
  ```

  ```kotlin title="Capture and restore span context across coroutines (Kotlin)" expandable lines wrap theme={null}
  import io.opentelemetry.context.Context

  val parentSpan = LDObserve.startSpan("parentSpan")

  // Capture the current context, which includes the active span
  val context = Context.current()

  launch(Dispatchers.IO) {
    val span = LDObserve.startSpan(
        name = "log-context-demo",
        properties = mapOf("demo" to "log-with-context")
    )
    // Capture span context while still on the originating thread.
    val capturedContext = span.makeCurrent().use { span.spanContext }
    span.end()
    // Simulate a detached thread where OTel context is lost automatically.
    // Span.current() here returns INVALID, so we pass the captured context explicitly.
    Thread {
        Span.wrap(capturedContext).makeCurrent().use {
            val childSpan = LDObserve.startSpan("child of log-context-demo")
            childSpan.end()
        }
        LDObserve.recordLog(
            message = text,
            severity = Severity.WARN,
            properties = mapOf("source" to "detached-thread-demo"),
            spanContext = capturedContext
        )
    }.start()
  }

  parentSpan.end()
  ```
</CodeGroup>

Alternatively, you can store the current context and restore it later in a different scope:

<CodeGroup>
  ```java title="Store and restore span context (Java)" expandable lines wrap theme={null}
  import io.opentelemetry.context.Context;
  import io.opentelemetry.context.Scope;

  Span parentSpan = LDObserve.startSpan("parentSpan", new HashMap<>());
  try (Scope parentScope = parentSpan.makeCurrent()) {
    // Now parentSpan is active in Context.current()
    Context ctx = Context.current();

    // Later, in another thread, restore the context
    executor.execute(() -> {
        try (Scope scope = ctx.makeCurrent()) {
            Span nestedSpan = LDObserve.startSpan("nestedSpan", new HashMap<>());
            // do work — nestedSpan is a child of parentSpan
            nestedSpan.end();
        }
    });
  } finally {
    parentSpan.end();
  }
  ```

  ```kotlin title="Store and restore span context (Kotlin)" lines wrap theme={null}
  import io.opentelemetry.context.Context

  // Capture the current context
  val ctx = Context.current()

  // Later, in another scope, restore the context
  ctx.makeCurrent().use {
    val span = LDObserve.startSpan("nestedSpan")
    // do work
    span.end()
  }
  ```
</CodeGroup>

To propagate a span context to an entirely different service, use the OpenTelemetry `W3CTraceContextPropagator` to inject the context into outgoing HTTP headers. Services that receive the headers can then extract and use the context.

<CodeGroup>
  ```java title="Inject context into HTTP headers (Java/OkHttp)" lines wrap theme={null}
  import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
  import io.opentelemetry.context.Context;
  import okhttp3.Request;

  W3CTraceContextPropagator propagator = W3CTraceContextPropagator.getInstance();
  Request.Builder requestBuilder = new Request.Builder().url(url);

  propagator.inject(
    Context.current(),
    requestBuilder,
    (carrier, key, value) -> carrier.addHeader(key, value)
  );

  Request request = requestBuilder.build();
  ```

  ```kotlin title="Inject context into HTTP headers (Kotlin/OkHttp)" lines wrap theme={null}
  import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
  import io.opentelemetry.context.Context
  import okhttp3.Request

  val propagator = W3CTraceContextPropagator.getInstance()
  val requestBuilder = Request.Builder().url(url)

  propagator.inject(
    Context.current(),
    requestBuilder
  ) { carrier, key, value -> carrier?.addHeader(key, value) }

  val request = requestBuilder.build()
  ```
</CodeGroup>

The above example adds header content similar to:

<CodeGroup>
  ```http title="Example header content" lines wrap theme={null}
  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
  tracestate: (optional vendor data)
  ```
</CodeGroup>

Services that receive the headers use the `extract` method from `W3CTraceContextPropagator` to obtain the trace context and use it as the parent context for new spans.

## Explore supported features

The observability plugin supports the following features. After the SDK and plugins are initialized, you can access these from within your application:

* [Configuration for client-side observability](/docs/sdk/features/observability-config-client-side)
* [Configuration for session replay](/docs/sdk/features/session-replay-config)
* [Errors](/docs/sdk/features/observability-errors#android)
* [Logs](/docs/sdk/features/observability-logs#android)
* [Metrics](/docs/sdk/features/observability-metrics#android)
* [Symbolication](/docs/sdk/features/observability-symbolication#android)
* [Tracing](/docs/sdk/features/observability-traces#android)

## Review observability data in LaunchDarkly

After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly. You can review this information in the LaunchDarkly user interface. To learn how, read [Observability](/docs/home/observability).

The observability data collected includes:

* **Error monitoring**: Unhandled exceptions, crashes, and manually recorded errors with stack traces
* **Logs**: Application logs with configurable severity levels and custom attributes
* **Traces**: Distributed tracing data including span timing, nested operations, and custom instrumentation
* **Metrics**: Performance metrics, custom counters, histograms, and gauge measurements
* **Session data**: User session information including lifecycle events and timing

Specifically, the observability data includes events that LaunchDarkly uses to automatically create the following metrics:

* User error rate and crash frequency
* Application performance metrics such as launch time and session duration
* Feature flag evaluation context and timing
* Custom business metrics recorded through the SDK

To learn more about autogenerated metrics, read [Observability autogenerated metrics](/docs/home/metrics/autogen/observability).
