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

# .NET MAUI 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 and session replay SDK for .NET MAUI.

The .NET MAUI SDK supports **observability** for error monitoring and logging, and **session replay** for capturing UI snapshots. It provides cross-platform support for both Android and iOS through .NET MAUI's native platform bindings.

<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>GitHub repository</td>

        <td>
          [LaunchDarkly.SessionReplay](https://github.com/launchdarkly/observability-sdk/tree/main/sdk/%40launchdarkly/mobile-dotnet)
        </td>
      </tr>

      <tr>
        <td>Published package</td>
        <td>[NuGet](https://www.nuget.org/packages/LaunchDarkly.SessionReplay)</td>
      </tr>
    </tbody>
  </table>
</Note>

## Prerequisites and dependencies

This SDK requires:

* .NET 9 or .NET 10 with the MAUI workload installed
* Android API level 23 or higher (Android 6.0, Marshmallow)
* iOS 15 or higher

## Get started

Follow these steps to get started:

* [Install the SDK](#install-the-sdk)
* [Initialize the SDK](#initialize-the-sdk)
* [Enable HTTP request tracing](#enable-http-request-tracing)
* [Configure observability options](#configure-observability-options)
* [Configure session replay](#configure-session-replay)
* [Configure session replay privacy options](#configure-session-replay-privacy-options)
* [Use custom masking](#use-custom-masking)
* [Explore supported features](#explore-supported-features)
* [Review observability data in LaunchDarkly](#review-observability-data-in-launchdarkly)

## Install the SDK

Add the `LaunchDarkly.SessionReplay` NuGet package to your MAUI project:

<CodeGroup>
  ```bash title=".NET CLI" lines wrap theme={null}
  dotnet add package LaunchDarkly.SessionReplay
  ```

  ```xml title="PackageReference" lines wrap theme={null}
  <PackageReference Include="LaunchDarkly.SessionReplay" Version="0.*" />
  ```
</CodeGroup>

After installing, run `dotnet clean` before building. This is especially important for iOS targets, as stale build artifacts can cause issues. If you encounter dependency resolution problems, run `dotnet restore` first:

<CodeGroup>
  ```bash title=".NET CLI" lines wrap theme={null}
  dotnet restore
  dotnet clean
  dotnet build
  ```
</CodeGroup>

<Warning>
  **Visual Studio with Android SDK on Windows**

  Visual Studio on Windows can have issues installing the SDK due to the Android SDK being in a system-protected directory. If you encounter build errors related to Android SDK access, you may need to move the Android SDK to a user-writable location.

  For detailed instructions, read the [Android SDK relocation guide for Windows](https://github.com/launchdarkly/observability-sdk/blob/main/sdk/%40launchdarkly/mobile-dotnet/docs/move-android-sdk-windows.md).
</Warning>

Then, import the SDK namespaces in your code:

<CodeGroup>
  ```csharp title="C#" lines wrap theme={null}
  using LaunchDarkly.SessionReplay;
  using LaunchDarkly.Sdk.Client;
  using LaunchDarkly.Sdk.Client.Integrations;
  using LaunchDarkly.Observability;
  ```
</CodeGroup>

## Initialize the SDK

Initialize the SDK by registering the `ObservabilityPlugin` on the LaunchDarkly client configuration. Call this during your app's startup, typically in `MauiProgram.cs`.

The `ObservabilityPlugin` handles both observability and session replay. Register it using the `PluginConfigurationBuilder` to integrate it directly with the LaunchDarkly client lifecycle. To turn on session replay, pass a `SessionReplayOptions` object as the plugin's second argument. To learn more, read [Configure session replay](#configure-session-replay).

<Warning>
  **.NET MAUI observability SDK credentials**

  The .NET MAUI 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:

<CodeGroup>
  ```csharp title="MauiProgram.cs" expandable lines wrap theme={null}
  using LaunchDarkly.Observability;
  using LaunchDarkly.Sdk;
  using LaunchDarkly.Sdk.Client;
  using LaunchDarkly.Sdk.Client.Integrations;

  public static class MauiProgram
  {
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        // ... other configuration ...

        var mobileKey = "example-mobile-key";

        var ldConfig = Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled)
            .Plugins(new PluginConfigurationBuilder()
                .Add(new ObservabilityPlugin(new ObservabilityOptions(
                    isEnabled: true,
                    serviceName: "my-maui-app"
                )))
            ).Build();

        var context = Context.New("example-user-key");
        var client = LdClient.Init(ldConfig, context, TimeSpan.FromSeconds(10));

        return builder.Build();
    }
  }
  ```
</CodeGroup>

## Enable HTTP request tracing

The SDK automatically creates spans for outgoing HTTP requests made through `HttpClient`. For these spans to be recorded, set `HttpActivityPropagationSupport` to `true` in your app's project file.

Here is an example:

<CodeGroup>
  ```xml title="Project file" lines wrap theme={null}
  <PropertyGroup>
  <HttpActivityPropagationSupport>true</HttpActivityPropagationSupport>
  </PropertyGroup>
  ```
</CodeGroup>

The .NET Android SDK sets `HttpActivityPropagationSupport` to `false` by default in every build configuration, and the .NET iOS SDK sets it to `false` in Release builds. While it is `false`, the trimmer removes the `System.Net.Http` diagnostics that the observability SDK listens to. As a result, the SDK records no HTTP spans, even if you enable `NetworkRequests` instrumentation. The SDK cannot turn this setting on at runtime.

## Configure observability options

You can customize the observability behavior by passing an `ObservabilityOptions` object to the `ObservabilityPlugin` constructor.

Here is an example:

<CodeGroup>
  ```csharp title="Observability options" lines wrap theme={null}
  var ldConfig = Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled)
    .Plugins(new PluginConfigurationBuilder()
        .Add(new ObservabilityPlugin(new ObservabilityOptions(
            isEnabled: true,
            serviceName: "my-maui-app",
            serviceVersion: "2.4.0",
            customHeaders: new Dictionary<string, string> { { "X-Proxy-Token", "example-token" } },
            logsApiLevel: ObservabilityLogLevel.Warn
        )))
    ).Build();
  ```
</CodeGroup>

The available `ObservabilityOptions` configuration options are:

* **IsEnabled**: Whether the SDK records observability telemetry. Defaults to `true`.
* **ServiceName**: The service name for the application. Defaults to `"observability-maui"`.
* **ServiceVersion**: The version of the service. If you do not set it, the SDK reports your app's version, from `AppInfo.Current.VersionString`.
* **OtlpEndpoint**: The endpoint URL for the OpenTelemetry Protocol (OTLP) exporter. Defaults to the LaunchDarkly endpoint. Override it to send data through a proxy.
* **BackendUrl**: The backend URL for the observability service. Defaults to the LaunchDarkly backend.
* **ContextFriendlyName**: An optional friendly name for the user context, displayed in the LaunchDarkly UI.
* **Attributes**: Additional resource attributes to attach to every exported signal, such as `deployment.environment`.
* **CustomHeaders**: Additional HTTP headers to add to OTLP exports, for example, for a proxy or for authentication.
* **LogsApiLevel**: The minimum severity of logs to export. Uses the `ObservabilityLogLevel` enum, whose values match the OpenTelemetry severity numbers, from `Trace` to `Fatal4`. Set it to `ObservabilityLogLevel.None` to turn off log export. Defaults to `ObservabilityLogLevel.Info`.
* **Instrumentation**: An `InstrumentationOptions` object with two settings, `NetworkRequests` and `LaunchTimes`, that turn automatic instrumentation of `HttpClient` requests and app launch times on or off. Both default to `true`.

<Info>
  **Default service version changed in version 0.13.0**

  In SDK versions earlier than 0.13.0, `ServiceVersion` defaults to `"0.1.0"`. Starting in version 0.13.0, apps that do not set `ServiceVersion` report their own app version instead. The `ObservabilityOptions.DefaultServiceVersion` constant is obsolete.
</Info>

After the SDK initializes, `LDObserve.NativeVersion` returns the version of the underlying native observability SDK. Include it when you contact LaunchDarkly Support.

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

## Configure session replay

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

To turn on session replay, pass a `SessionReplayOptions` object as the second argument to the `ObservabilityPlugin` constructor.

Here's how to configure session replay:

<CodeGroup>
  ```csharp title="Session replay options" lines wrap theme={null}
  var ldConfig = Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled)
    .Plugins(new PluginConfigurationBuilder()
        .Add(new ObservabilityPlugin(
            new ObservabilityOptions(
                isEnabled: true,
                serviceName: "my-maui-app"
            ),
            new SessionReplayOptions(
                isEnabled: true,
                sampleRate: 0.25,
                privacy: new SessionReplayOptions.PrivacyOptions(
                    maskTextInputs: true,
                    maskWebViews: false,
                    maskLabels: false
                )
            )
        ))
    ).Build();
  ```
</CodeGroup>

The available `SessionReplayOptions` configuration options are:

* **IsEnabled**: Enables or disables session replay. Defaults to `true`.
* **ServiceName**: The service name for session replay telemetry. Defaults to `"sessionreplay-dotnet"`.
* **Privacy**: Controls how UI elements are masked in the replay. To learn more, read [Configure session replay privacy options](#configure-session-replay-privacy-options).
* **SampleRate**: The probability, from `0.0` to `1.0`, that the SDK records a session. For example, `0.25` records about a quarter of sessions. Defaults to `1.0`.
* **FrameRate**: The target capture rate, in frames per second. Defaults to `1.0`.
* **Scale**: The capture resolution. `1.0` is 1x (160 dots per inch), `2.0` is 2x, and so on. Higher values capture more detail but produce larger frames. Defaults to `1.0`.
* **ImageQuality**: The JPEG quality of exported frames, from `0.0` (smallest) to `1.0` (best). Defaults to `0.3`.

## Configure session replay privacy options

The `PrivacyOptions` class controls how UI elements are masked during session replay. By default, text inputs are masked to protect user privacy.

Here's how to configure privacy settings:

<CodeGroup>
  ```csharp title="Privacy options" lines wrap theme={null}
  var replay = new SessionReplayOptions(
    isEnabled: true,
    privacy: new SessionReplayOptions.PrivacyOptions(
        maskTextInputs: true,
        maskWebViews: false,
        maskLabels: false,
        maskImages: false,
        minimumAlpha: 0.02
    )
  );
  ```
</CodeGroup>

The available privacy options are:

* **MaskTextInputs**: When `true`, masks all text input fields. Defaults to `true`.
* **MaskWebViews**: When `true`, masks the contents of web views. Web views are rendered as blank rectangles in session replays. Defaults to `false`.
* **MaskLabels**: When `true`, masks all text labels. Defaults to `false`.
* **MaskImages**: When `true`, masks all images. Defaults to `false`.
* **MinimumAlpha**: Minimum alpha value for view visibility in recordings. Views with alpha below this threshold are not captured. Defaults to `0.02`.

### Common privacy configurations

For maximum privacy (recommended for production):

<CodeGroup>
  ```csharp title="Maximum privacy" lines wrap theme={null}
  var privacy = new SessionReplayOptions.PrivacyOptions(
    maskTextInputs: true,
    maskWebViews: true,
    maskLabels: true,
    maskImages: true
  );
  ```
</CodeGroup>

For debugging or development, you can turn masking off:

<CodeGroup>
  ```csharp title="No masking" lines wrap theme={null}
  var privacy = new SessionReplayOptions.PrivacyOptions(
    maskTextInputs: false,
    maskWebViews: false,
    maskLabels: false,
    maskImages: false
  );
  ```
</CodeGroup>

For selective masking, you can mask inputs but show regular text and images:

<CodeGroup>
  ```csharp title="Selective masking" lines wrap theme={null}
  var privacy = new SessionReplayOptions.PrivacyOptions(
    maskTextInputs: true,
    maskWebViews: true,
    maskLabels: false,
    maskImages: false
  );
  ```
</CodeGroup>

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

## Use custom masking

In addition to the privacy profile settings, you can explicitly mask or unmask individual MAUI views using the `LDMask()` and `LDUnmask()` extension methods. This gives you fine-grained control over which UI elements are captured in session replay recordings.

Here is an example:

<CodeGroup>
  ```csharp title="Masking individual views" lines wrap theme={null}
  using LaunchDarkly.SessionReplay;

  // Mask a sensitive field in session replay
  var passwordEntry = new Entry { Placeholder = "Password", IsPassword = true };
  passwordEntry.LDMask();

  // Unmask a field that would otherwise be masked by privacy settings
  var publicInfoLabel = new Label { Text = "Public information" };
  publicInfoLabel.LDUnmask();
  ```
</CodeGroup>

<Info>
  **Handler must be attached**

  The `LDMask()` and `LDUnmask()` methods work by accessing the underlying native platform view. The MAUI view's handler must be attached before calling these methods. Call them after the view has been added to the visual tree, such as in a page's `Appearing` event or in `OnHandlerChanged`. Before the handler is attached, these methods have no effect.
</Info>

The `.LDMask()` and `.LDUnmask()` extension methods work on any MAUI `View`, and they apply to the underlying native view on both Android and iOS.

## Explore supported features

The .NET MAUI observability SDK supports the following features. After the SDK is 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)
* [Logs](/docs/sdk/features/observability-logs)

## Using .NET System.Diagnostics

As with other LaunchDarkly observability SDK plugins, the .NET MAUI observability plugin supports OpenTelemetry operations using the following span methods.

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Description</th>
      <th>Return value</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>`LDObserve.StartActiveSpan(name)`</td>
      <td>Uses the current active span as its parent.</td>
      <td>`TelemetrySpan`</td>
    </tr>

    <tr>
      <td>`LDObserve.StartActiveSpan(name, parentContext)`</td>
      <td>Uses the provided `SpanContext` as its parent.</td>
      <td>`TelemetrySpan`</td>
    </tr>

    <tr>
      <td>`LDObserve.StartActiveSpan(name, kind, parentContext)`</td>
      <td>Uses the provided `SpanContext` with a custom kind as its parent.</td>
      <td>`TelemetrySpan`</td>
    </tr>

    <tr>
      <td>`LDObserve.StartRootSpan(name)`</td>
      <td>Creates a new trace.</td>
      <td>`TelemetrySpan`</td>
    </tr>

    <tr>
      <td>`LDObserve.GetTracer()`</td>
      <td>Returns an OpenTelemetry tracer.</td>
      <td>`Tracer`</td>
    </tr>
  </tbody>
</table>

If your .NET application already uses Microsoft's `System.Diagnostics` API, LaunchDarkly's observability plugin provides the following methods help you continue using your existing trace code.

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Description</th>
      <th>Return value</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>`LDObserve.StartActivity(name)`</td>
      <td>Uses the current activity as its parent.</td>
      <td>`Activity?`</td>
    </tr>

    <tr>
      <td>`LDObserve.StartRootActivity(name)`</td>
      <td>Creates a new trace.</td>
      <td>`Activity?`</td>
    </tr>

    <tr>
      <td>`LDObserve.GetActivitySource()`</td>
      <td>Returns an `ActivitySource` to create and start `Activity` objects.</td>
      <td>`ActivitySource`</td>
    </tr>
  </tbody>
</table>

Here's an example of using the LaunchDarkly observability plugin with the `System.Diagnostics` API:

<CodeGroup>
  ```csharp title="Using observability with the .NET System.Diagnostics API" lines wrap theme={null}
  using System.Diagnostics;

  using var activity = LDObserve.StartActivity("api_request");
  activity?.SetTag("endpoint", "/api/users");
  activity?.SetTag("method", "GET");
  activity?.AddEvent(new ActivityEvent("cache.miss"));

  // Create a root activity (no parent), equivalent to StartRootSpan in OpenTelemetry
  using var root = LDObserve.StartRootActivity("independent-operation");
  root?.SetTag("sequence", "1");

  // Or, get the ActivitySource directly for full control
  var source = LDObserve.GetActivitySource();
  using var custom = source.StartActivity("custom", ActivityKind.Client);
  ```
</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. This section shows techniques for nesting spans and propagating span contexts across services when using the .NET MAUI observability SDK.

### Creating a root span

Creating a root span creates a new trace regardless of any current span context that might exist. Use `StartRootSpan` to create your initial trace and span context.

<CodeGroup>
  ```csharp title="Creating a root span" lines wrap theme={null}
  using var span = LDObserve.StartRootSpan("app-cold-start");
  span.SetAttribute("launch_type", "cold");
  span.SetAttribute("device_model", DeviceInfo.Model);
  span.AddEvent("splash_rendered");

  // ... initialization work ...

  span.AddEvent("home_screen_ready");
  ```
</CodeGroup>

### Nesting spans

Use `StartActiveSpan` to create new spans using the parent span context. This pattern enables you to trace multi-step operations in the same application.

The following example nests all newly-created spans under `LoadProducts`. The `DeserializeJson` span is further nested under `FetchFromApi`.

<CodeGroup>
  ```csharp title="Creating nested spans" expandable lines wrap theme={null}
  private async Task LoadProductsAsync()
  {
    using var loadSpan = LDObserve.StartActiveSpan("LoadProducts");

    List<Product> products;
    using (var fetchSpan = LDObserve.StartActiveSpan("FetchFromApi"))
    {
        var response = await _httpClient.GetAsync("https://api.example.com/products");
        fetchSpan.SetAttribute("http.status_code", (int)response.StatusCode);
        var json = await response.Content.ReadAsStringAsync();

        using var parseSpan = LDObserve.StartActiveSpan("DeserializeJson");
        products = JsonSerializer.Deserialize<List<Product>>(json)!;
        parseSpan.SetAttribute("product_count", products.Count);
    }

    using (var renderSpan = LDObserve.StartActiveSpan("RenderUI"))
    {
        Products.Clear();
        foreach (var p in products)
            Products.Add(p);
    }
  }
  ```
</CodeGroup>

### Propagating span context across threads

When work runs on a detached thread, for example, using `Task.Run`, the span context is lost. Use `span.Context` to capture the context and pass it to other threads.

<CodeGroup>
  ```csharp title="Creating nested spans" expandable lines wrap theme={null}
  private async void OnUploadClicked(object? sender, EventArgs e)
  {
    var span = LDObserve.StartActiveSpan("UploadReport");
    span.SetAttribute("report.type", "daily");
    var capturedContext = span.Context;
    span.End();

    await Task.Run(() =>
    {
        // Activity.Current is null here -- pass capturedContext explicitly
        LDObserve.RecordLog(
            "Upload processing on background thread",
            Severity.Info,
            new Dictionary<string, object?> { { "thread", Environment.CurrentManagedThreadId } },
            spanContext: capturedContext);

        // ... heavy processing ...

        LDObserve.RecordLog(
            "Upload complete",
            Severity.Info,
            spanContext: capturedContext);
    });
  }
  ```
</CodeGroup>

### Using spans with HttpClient

When calling a remote service you must preserve the current span context when creating new spans, or capture details of the service call in the current span.

When `InstrumentationOptions.NetworkRequests` is enabled, every `HttpClient` request generates a `System.Net.Http` span automatically. This is the default behavior, but it requires the `HttpActivityPropagationSupport` project setting. To learn more, read [Enable HTTP request tracing](#enable-http-request-tracing). If a custom span is active at call time, the HTTP span becomes its child. The following example automatically generates a new span for the HTTP GET call and nests it under the active `SyncOrders` span. You do not need to create a span for the HTTP call itself:

<CodeGroup>
  ```csharp title="Creating nested spans" lines wrap theme={null}
  private async Task SyncOrdersAsync()
  {
    using var span = LDObserve.StartActiveSpan("SyncOrders");
    span.SetAttribute("sync.direction", "pull");

    // The HTTP span for this GET is auto-created as a child of "SyncOrders"
    var response = await _httpClient.GetAsync("https://api.example.com/orders?since=yesterday");
    span.SetAttribute("http.status_code", (int)response.StatusCode);

    var orders = await response.Content.ReadFromJsonAsync<List<Order>>();
    span.SetAttribute("order_count", orders?.Count ?? 0);
  }
  ```
</CodeGroup>

As an alternative, you can wrap an HTTP call in a span to capture timing, status codes, and errors in a single trace unit.

<CodeGroup>
  ```csharp title="Creating nested spans" expandable lines wrap theme={null}
  private async Task<UserProfile?> FetchUserProfileAsync(string userId)
  {
    using var span = LDObserve.StartActiveSpan("FetchUserProfile");
    span.SetAttribute("user.id", userId);
    span.SetAttribute("http.method", "GET");

    try
    {
        var url = $"https://api.example.com/users/{userId}";
        span.SetAttribute("http.url", url);

        var response = await _httpClient.GetAsync(url);
        span.SetAttribute("http.status_code", (int)response.StatusCode);

        if (!response.IsSuccessStatusCode)
        {
            span.SetStatus(Status.Error);
            span.SetAttribute("error.type", $"HTTP {response.StatusCode}");
            return null;
        }

        var profile = await response.Content.ReadFromJsonAsync<UserProfile>();
        span.SetStatus(Status.Ok);
        return profile;
    }
    catch (Exception ex)
    {
        span.RecordException(ex);
        span.SetStatus(Status.Error);
        throw;
    }
  }
  ```
</CodeGroup>

### Additional distributed tracing techniques

Your application may also need to perform other, more advanced distributed tracing activities such as:

* Correlating log entries within a span
* Correlating log entries or across different threads
* Recording span exceptions
* Using the Activity API in place of OpenTelemetry methods

To learn more about these techniques, read the .NET MAUI [Distributed Tracing Guide](https://github.com/launchdarkly/observability-sdk/blob/main/sdk/%40launchdarkly/mobile-dotnet/docs/distributed-tracing.md#5-record-exception-and-mark-span-as-failed).

## Review observability data in LaunchDarkly

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