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

# iOS SDK observability reference

<View title="Developer">
  <Note>
    **This LaunchDarkly observability plugin is available for early access**

    This LaunchDarkly observability plugin is 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 iOS SDK.

  The iOS SDK supports the **observability plugin** for error monitoring, logging, and tracing, and the **session replay plugin** for recording user sessions.

  <Note>
    **SDK quick links**

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

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

      <tbody>
        <tr>
          <td>GitHub repository</td>
          <td>[swift-launchdarkly-observability](https://github.com/launchdarkly/swift-launchdarkly-observability)</td>
        </tr>

        <tr>
          <td>Published module</td>
          <td>[Swift Package Manager](https://swift.org/package-manager/)</td>
        </tr>
      </tbody>
    </table>
  </Note>

  ## Prerequisites and dependencies

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

  The observability plugin is compatible with the [iOS SDK](/docs/sdk/client-side/ios), version 9.14.0 and later, and is only available if you are using Swift.

  ## Get started

  Follow these steps to get started:

  * [Install the plugin](#install-the-plugin)
  * [Initialize the iOS SDK client](#initialize-the-client)
  * [Configure the plugin options](#configure-the-plugin-options)
  * [Configure product analytics event collection](#configure-product-analytics-event-collection)
  * [Track screen views](#track-screen-views)
  * [Configure session replay](#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 iOS SDK to provide observability.

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

  Here's how:

  <CodeGroup>
    ```swift title="Package.swift, using Swift Package Manager" lines wrap theme={null}
    //...
        dependencies: [
            .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor("9.0.0")),
            .package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability.git", .upToNextMinor("1.0.0")),
        ],
        targets: [
            .target(
                name: "YOUR_TARGET",
                dependencies: ["LaunchDarkly"]
            )
        ],
    //...
    ```

    ```ruby title="Podfile, using CocoaPods" lines wrap theme={null}
    use_frameworks!
    target 'YourTargetName' do
      pod 'LaunchDarkly', '~> 9.0'
      pod 'LaunchDarklyObservability', '~> 1.0'
    end
    ```

    ```swift title="Cartfile, using Carthage" lines wrap theme={null}
    github "launchdarkly/ios-client" ~> 9.0
    github "launchdarkly/swift-launchdarkly-observability" ~> 1.0
    ```
  </CodeGroup>

  Then, import the plugin into your code:

  <CodeGroup>
    ```swift title="Swift" lines wrap theme={null}
    import LaunchDarkly
    import LaunchDarklyObservability
    import OpenTelemetryApi
    ```
  </CodeGroup>

  ## Initialize the client

  Next, initialize the SDK and the plugin.

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

  Here's how to initialize the SDK and plugin:

  <CodeGroup>
    ```swift title="iOS SDK v9.14+ (Swift)" lines wrap theme={null}
    let config = LDConfig(mobileKey: "example-mobile-key", autoEnvAttributes: .enabled)
    config.plugins = [Observability()]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(config: config, context: context, startWaitSeconds: 5) { timedOut in
        if timedOut {
            // Client may not have the most recent flags for the configured context
        } else {
            // Client has received flags for the configured context
        }
    }
    ```
  </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>
    ```swift title="Plugin options, iOS SDK v9.14+" expandable lines wrap theme={null}
    // Create the plugin options
    let options = ObservabilityOptions(
        serviceName: "MyApp",
        serviceVersion: "1.2.3",
        otlpEndpoint: "https://otel.observability.app.launchdarkly.com:4318",
        resourceAttributes: [
            "environment": .string("production"),
            "team": .string("mobile-team"),
            "app.version": .string("1.2.3")
        ],
        customHeaders: ["Custom-Header": "header-value"],
        sessionBackgroundTimeout: 30 * 60, // 30 minutes in seconds
        isDebug: false
    )

    // Create the observability plugin with the options
    let observabilityPlugin = Observability(options: options)

    let config = LDConfig(mobileKey: "example-mobile-key", autoEnvAttributes: .enabled)
    config.plugins = [observabilityPlugin]
    ```
  </CodeGroup>

  ### Configuration options

  The `ObservabilityOptions` struct provides the following parameters:

  * **isEnabled**: When `false`, the plugin installs but exports no telemetry. Defaults to `true`.
  * **serviceName**: The service name for the application. Defaults to "observability-swift".
  * **serviceVersion**: The service version for the application. Defaults to "0.1.0".
  * **otlpEndpoint**: The endpoint URL for the OpenTelemetry Protocol (OTLP) exporter. Defaults to the LaunchDarkly endpoint.
  * **resourceAttributes**: Additional OpenTelemetry resource attributes to include in telemetry data.
  * **customHeaders**: Custom headers to include with OTLP exports.
  * **sessionBackgroundTimeout**: How long the app can stay in the background, in seconds, before the current session ends. Defaults to 15 minutes (900 seconds).
  * **isDebug**: Enables additional logging for debugging. Defaults to `false`.
  * **logsApiLevel**: The minimum severity of logs to export. Use `.none` to disable log collection. Defaults to `.info`.
  * **tracesApi**: Controls trace collection. Defaults to `.enabled`.
  * **metricsApi**: Controls metric collection. Defaults to `.enabled`.
  * **crashReporting**: Controls automatic crash reporting. Defaults to `.enabled`.
  * **tracingOrigins**: Which outgoing request origins propagate distributed tracing headers. Defaults to `.disabled`.
  * **urlBlocklist**: URL patterns to exclude from automatic `URLSession` instrumentation. Defaults to an empty array.
  * **contextFriendlyName**: A human-readable name to attach to the LaunchDarkly context for this session.
  * **instrumentation**: Controls automatic instrumentation. To learn more, read [Instrumentation options](#instrumentation-options).
  * **analytics**: Controls which product analytics events the plugin emits as OpenTelemetry spans. To learn more, read [Configure product analytics event collection](#configure-product-analytics-event-collection).

  ### Instrumentation options

  The `instrumentation` option controls which automatic instrumentations the plugin runs. Pass an `Instrumentation` value, or use the `.enabled` and `.disabled` presets to turn all of them on or off at once.

  The `Instrumentation` options are:

  * **urlSession**: Instruments `URLSession` requests. Defaults to `.disabled`.
  * **userTaps**: Runs tap detection. The `analytics.taps` option separately controls whether the plugin publishes detected taps as `click` spans. If `userTaps` is `.disabled`, the plugin publishes no `click` spans regardless of the `analytics.taps` value. Neither option affects session replay capture. Defaults to `.enabled`.
  * **memory**: Records memory usage metrics. Defaults to `.disabled`.
  * **memoryWarnings**: Records memory warning events. Defaults to `.disabled`.
  * **cpu**: Records CPU usage metrics. Defaults to `.disabled`.
  * **launchTimes**: Emits launch-time performance telemetry, including the `app.start` span event that carries the cold or warm startup dimension on the `app_launch` span. Defaults to `.enabled`.
  * **screens**: Automatically detects screen changes by swizzling `UIViewController`. Screen detection drives both the automatic `screen_view` span and the session replay `Navigate` events. Defaults to `.enabled`. The `analytics.screenViews` option gates `screen_view` spans.

  Here is an example that turns off automatic screen detection while leaving other instrumentation at its default:

  <CodeGroup>
    ```swift title="Instrumentation configuration" lines wrap theme={null}
    let options = ObservabilityOptions(
        instrumentation: ObservabilityOptions.Instrumentation(screens: .disabled)
    )
    ```
  </CodeGroup>

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

  ## Configure product analytics event collection

  The iOS 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** (manual): A `track` span when your code calls `track(key:properties:metricValue:)`. To learn more, read [Recording product analytics events](#recording-product-analytics-events).
  * **Screen views** (automatic and manual): A `screen_view` span when a screen appears, 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 disable all product analytics events, set `analytics` to `.disabled` in `ObservabilityOptions`. To control individual event types, pass an `Analytics` value directly:

  <CodeGroup>
    ```swift title="Disable all product analytics events" lines wrap theme={null}
    let options = ObservabilityOptions(
        analytics: .disabled
    )
    ```

    ```swift title="Control individual event types" lines wrap theme={null}
    let options = ObservabilityOptions(
        analytics: ObservabilityOptions.Analytics(
            taps: .enabled,
            trackEvents: .enabled,
            screenViews: .enabled,
            appLifecycle: .enabled,
            appLaunch: .enabled
        )
    )
    ```
  </CodeGroup>

  The `Analytics` options are:

  * **taps**: Emits a `click` span for each detected tap. Defaults to `.enabled`.
  * **trackEvents**: Emits a `track` span when a custom event is tracked with `track(key:properties:metricValue:)`. Defaults to `.enabled`.
  * **screenViews**: Emits a `screen_view` span when a screen appears. Defaults to `.enabled`.
  * **appLifecycle**: Emits `app_foreground` and `app_background` spans as the app moves between states. Defaults to `.enabled`.
  * **appLaunch**: Emits an `app_launch` span once per process launch, including an `app.start` span event with the cold or warm startup dimension. Defaults to `.enabled`.

  ### App launch events

  The iOS observability plugin records a single `app_launch` span once per process launch. The plugin captures process start timing as early as possible during launch using a startup constructor that runs before `main`, then emits the `app_launch` span after observability initializes. This means the recorded startup duration reflects actual launch time rather than SDK initialization time.

  The `app_launch` span includes:

  * `event.launch_type`: Classifies the launch as `install` (first launch after install), `update` (first launch after a version update), or `relaunch` (a subsequent launch of the same version). The plugin determines the launch type by comparing the current app version to the previously stored version.
  * Version attributes, including the current app version and, on an update, the previous version.
  * An `app.start` span event with a `start.type` attribute (`cold` or `warm`) and, when available, a `start.duration_ms` attribute. A warm start is detected when the operating system prewarms the app, using the `ActivePrewarm` environment variable on iOS 15 and later.

  To learn more about app launch attributes across SDKs, read [App launch events](/docs/home/observability/product-analytics#app-launch-events).

  ## Track screen views

  The plugin emits a `screen_view` span when your app shows a screen. Each span uses the `event.*` attribute namespace, including `event.name`, `event.screen_class`, `event.screen_id`, `event.previous_screen`, and `event.category`. The plugin resolves `event.previous_screen` from a shared screen stack to ensure correctness whether the plugin captured the screen or your code recorded it manually.

  Each recorded screen change also produces a session replay `Navigate` event to reflect navigation in the replay timeline.

  ### Capture UIKit view controllers automatically

  When `instrumentation.screens` is `.enabled` (the default), the plugin swizzles `UIViewController` and records a `screen_view` span each time a view controller appears. The plugin skips container and system controllers such as `UINavigationController` and `UITabBarController`.

  To customize how the plugin reports a controller, conform it to `LDScreenNameProviding`:

  <CodeGroup>
    ```swift title="Custom screen name" lines wrap theme={null}
    import LaunchDarklyObservability

    final class CheckoutViewController: UIViewController, LDScreenNameProviding {
        var ldScreenName: String? { "Checkout" }
        var ldScreenCategory: String? { "Commerce" }
    }
    ```
  </CodeGroup>

  ### Capture SwiftUI screens manually

  The `UIViewController` swizzle does not observe pure SwiftUI navigation, such as `NavigationStack` destinations. You must record those screens manually.

  Use the `trackScreen` modifier on the screen's root view. Only one call per screen is required, because the plugin resolves `event.previous_screen` from the shared screen stack:

  <CodeGroup>
    ```swift title="Track a SwiftUI screen" lines wrap theme={null}
    import SwiftUI
    import LaunchDarklyObservability

    struct ProfileView: View {
        var body: some View {
            VStack {
                // ...
            }
            .trackScreen("Profile", category: "Account")
        }
    }
    ```
  </CodeGroup>

  #### Track navigation stacks and modals

  The `trackScreen` modifier records on `.onAppear`. SwiftUI does not re-run `.onAppear` when you pop back to a screen in a `NavigationStack` or when a `sheet` or `fullScreenCover` closes. Use the path-aware and presentation-aware modifiers to ensure that back navigation and modal returns are tracked correctly.

  Apply `trackScreenStack` to the `NavigationStack` itself, passing the same `path` binding. The plugin records the top of the path, or the `root` name when the path is empty, on first appearance and on every push and pop. Return `nil` from `destination` to skip a screen that already records itself:

  <CodeGroup>
    ```swift title="Track a navigation stack" expandable lines wrap theme={null}
    import SwiftUI
    import LaunchDarklyObservability

    struct RootView: View {
        @State private var path: [Route] = []

        var body: some View {
            NavigationStack(path: $path) {
                // ...
            }
            .trackScreenStack(path, root: "Home") { route in
                switch route {
                case .profile: return "Profile"
                case .settings: return "Settings"
                }
            }
        }
    }
    ```
  </CodeGroup>

  Apply `trackScreenReturn` to a presenting screen to record it again after a modal closes. Pass the flag, or combination of flags, that drives the presentation. The plugin records the screen on each transition from `true` to `false`:

  <CodeGroup>
    ```swift title="Track a return from a modal" lines wrap theme={null}
    struct RootView: View {
        @State private var activeSheet: Sheet?

        var body: some View {
            List { /* ... */ }
                .sheet(item: $activeSheet) { sheet in /* ... */ }
                .trackScreenReturn("Home", isPresented: activeSheet != nil)
        }
    }
    ```
  </CodeGroup>

  Driving presentations from a single optional or enum route, such as `sheet(item:)`, keeps `isPresented` to one expression. If you use multiple `sheet(isPresented:)` flags, combine them, as in `isPresented: a || b || c`.

  Both `trackScreenStack` and `trackScreenReturn` require iOS 14, macOS 11, tvOS 14, or watchOS 7 and later.

  ### Record a screen view directly

  You can also call `trackScreenView` from anywhere after the SDK initializes. To learn more, read [Recording product analytics events](#recording-product-analytics-events).

  Manual `trackScreenView` calls work even if you disable automatic detection with `instrumentation.screens`. The `analytics.screenViews` option still gates the emitted `screen_view` span.

  ## Manual instrumentation

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

  ### Recording custom metrics

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

  <CodeGroup>
    ```swift title="Record metrics" expandable lines wrap theme={null}
    // Record a point-in-time metric
    LDObserve.shared.recordMetric(metric: Metric(name: "response_time_ms", value: 250.0))

    // Record metrics with attributes
    let attributes: [String: AttributeValue] = [
        "endpoint": .string("/api/users"),
        "method": .string("GET")
    ]
    LDObserve.shared.recordMetric(metric: Metric(
        name: "api_call_duration", 
        value: 120.5, 
        attributes: attributes
    ))

    // Record different metric types
    LDObserve.shared.recordCount(metric: Metric(name: "button_clicks", value: 1.0))
    LDObserve.shared.recordIncr(metric: Metric(name: "page_views", value: 1.0))
    LDObserve.shared.recordHistogram(metric: Metric(name: "request_size_bytes", value: 1024.0))
    LDObserve.shared.recordUpDownCounter(metric: Metric(name: "active_connections", value: 5.0))
    ```
  </CodeGroup>

  ### Recording custom logs

  Use `LDObserve.shared.recordLog` to emit a structured log. Pass attributes as a plain Swift dictionary through the `properties` parameter:

  <CodeGroup>
    ```swift title="Record logs" expandable lines wrap theme={null}
    // Record a basic log message
    LDObserve.shared.recordLog(
        message: "User login successful",
        severity: .info,
        properties: [:]
    )

    // Record logs with custom properties
    LDObserve.shared.recordLog(
        message: "Authentication completed",
        severity: .info,
        properties: [
            "user_id": "12345",
            "action": "login"
        ]
    )
    ```

    ```swift title="Record logs with OTel-typed attributes (advanced)" lines wrap theme={null}
    // Use OTel-typed attributes when exact OpenTelemetry typing is required
    let logAttributes: [String: AttributeValue] = [
        "user_id": .string("12345"),
        "action": .string("login")
    ]
    LDObserve.shared.recordLog(
        message: "Authentication completed",
        severity: .info,
        attributes: logAttributes
    )
    ```
  </CodeGroup>

  ### Recording custom errors

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

  <CodeGroup>
    ```swift title="Record errors" lines wrap theme={null}
    do {
        // Some operation that might fail
        try performNetworkRequest()
    } catch {
        // Record the error with context
        let errorAttributes: [String: AttributeValue] = [
            "operation": .string("network_request"),
            "endpoint": .string("/api/data")
        ]
        LDObserve.shared.recordError(error: error, attributes: errorAttributes)
    }
    ```
  </CodeGroup>

  ### Recording custom traces

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

  <CodeGroup>
    ```swift title="Record traces" expandable lines wrap theme={null}
    // Start a span with custom properties
    let span = LDObserve.shared.startSpan(
        name: "database_query",
        properties: [
            "table": "users",
            "operation": "select"
        ]
    )

    // Perform your operation
    performDatabaseQuery()

    // Optionally add more attributes during execution
    span.setAttribute(key: "rows_returned", value: .int(42))

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

    ```swift title="Record traces with OTel-typed attributes (advanced)" lines wrap theme={null}
    // Use OTel-typed attributes when exact OpenTelemetry typing is required
    let attributes: [String: AttributeValue] = [
        "table": .string("users"),
        "operation": .string("select")
    ]
    let span = LDObserve.shared.startSpan(name: "database_query", attributes: attributes)
    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>
    ```swift title="Track a custom event" lines wrap theme={null}
    // Track an event with properties and an optional metric value
    LDObserve.shared.track(
        key: "purchase_completed",
        properties: [
            "product_id": "SKU-123",
            "price": 29.99
        ],
        metricValue: 29.99
    )

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

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

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

    // Full details with custom properties
    LDObserve.shared.trackScreenView(
        name: "ProductDetail",
        screenClass: "ProductDetailViewController",
        screenId: "product-123",
        category: "browsing",
        properties: [
            "product_id": "SKU-123",
            "source": "search_results"
        ]
    )
    ```
  </CodeGroup>

  ### Using span builder for advanced tracing

  Use `LDObserve.shared.spanBuilder` when you need more control over a span than `startSpan` provides, such as setting the span kind or adding attributes before the span starts:

  <CodeGroup>
    ```swift title="Advanced span usage" lines wrap theme={null}
    // Get span builder for more control
    let spanBuilder = LDObserve.shared.spanBuilder(spanName: "complex_operation")
        .setSpanKind(spanKind: .client)

    spanBuilder.setAttribute(key: "user.id", value: .string("12345"))
    let span = spanBuilder.startSpan()

    // Make the span current for nested operations
    span.makeCurrent()

    // Perform work that might create child spans
    performComplexWork()

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

  ### Using 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 scope automatically use the context of their parent span, Swift's concurrency model (`async`, `await`, or `Task`) does not propagate the parent span context. Use the OpenTelemetry API to obtain and pass the parent span context explicitly to asynchronous methods:

  <CodeGroup>
    ```swift title="Obtain and propagate span context to asynchronous methods" lines wrap theme={null}
    let span0 = LDObserve.shared.startSpan(name: "parentSpan", properties: [:])

    await OpenTelemetry.instance.contextProvider.withActiveSpan(span0) {
      let span1 = LDObserve.shared.startSpan(name: "NestedSpan1", properties: [:])
      await OpenTelemetry.instance.contextProvider.withActiveSpan(span1) {
        LDObserve.shared.recordLog(message: "NestedLog", severity: .info, properties: [:])
      }
      span1.end()
    }
    span0.end()
    ```
  </CodeGroup>

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

  <CodeGroup>
    ```swift title="Obtain and restore a span context" expandable lines wrap theme={null}
    // Capture the current context
    let span = LDObserve.shared.startSpan(
        name: "log-context-demo",
        properties: ["demo": "log-with-context"]
    )
    let capturedContext = span.context
    span.end()

    // Simulate a detached task where the OpenTelemetry context would be lost automatically.
    DispatchQueue.global(qos: .background).async {
        LDObserve.shared.recordLog(
            message: "Log with span context",
            severity: .warn,
            properties: ["source": "detached-queue-demo"],
            spanContext: capturedContext
        )
    }
    ```
  </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>
    ```swift title="Inject context into HTTP header" lines wrap theme={null}
    import OpenTelemetryApi

    var request = URLRequest(url: url)
    let activeSpan = OpenTelemetry.instance.contextProvider.activeSpan
    let propagator = W3CTraceContextPropagator()

    // Inject current active span context into headers
    var headers: [String: String] = [:]
    propagator.inject(
        spanContext: activeSpan.context,
        carrier: &headers,
        setter: { carrier, key, value in carrier[key] = value }
    )
    headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
    ```
  </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 would use the `extract` method from `W3CTraceContextPropagator` to obtain fields from header and use them as the parent context for new spans.

  ## Session management

  The observability plugin automatically manages sessions and handles application lifecycle events. Sessions are automatically ended when:

  * The application is backgrounded for longer than the configured `sessionBackgroundTimeout`, which defaults to 15 minutes
  * The application is terminated
  * A new session is explicitly started

  ### Session timeout configuration

  You can configure how long the plugin waits before ending a session when the app goes to the background:

  <CodeGroup>
    ```swift title="Configure session timeout" lines wrap theme={null}
    let options = ObservabilityOptions(
        sessionBackgroundTimeout: 45 * 60 // 45 minutes in seconds
    )
    ```
  </CodeGroup>

  ## Automatic instrumentation

  The observability plugin automatically instruments your iOS application to collect:

  * **Application launch and lifecycle events**: App launch, foreground, and background events, recorded as product analytics spans. To learn more, read [Configure product analytics event collection](#configure-product-analytics-event-collection).
  * **Session tracking**: Automatic session start and end events with timing.
  * **Network requests**: HTTP request/response data when enabled.
  * **Taps**: A `click` span for each tap interaction.
  * **Screen views**: A `screen_view` span when a `UIViewController` appears, and a session replay `Navigate` event on each screen change. To learn more, read [Track screen views](#track-screen-views).
  * **LaunchDarkly SDK events**: Feature flag evaluations and SDK operations.

  ## Session Replay

  <Note>
    **Session Replay is in Early Access**

    Session Replay is available in Early Access. APIs are subject to change until a 1.x version is released.
  </Note>

  Session Replay captures user interactions and screen recordings to help you understand how users interact with your application. Session Replay works as an additional plugin that requires the observability plugin to be configured first.

  ### Install the Session Replay plugin

  First, add the Session Replay package as a dependency alongside the observability plugin:

  <CodeGroup>
    ```swift title="Package.swift, using Swift Package Manager" lines wrap theme={null}
    //...
        dependencies: [
            .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor("9.0.0")),
            .package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability.git", .upToNextMinor("1.0.0")),
        ],
        targets: [
            .target(
                name: "YOUR_TARGET",
                dependencies: [
                    "LaunchDarkly",
                    .product(name: "LaunchDarklySessionReplay", package: "swift-launchdarkly-observability")
                ]
            )
        ],
    //...
    ```

    ```ruby title="Podfile, using CocoaPods" lines wrap theme={null}
    use_frameworks!
    target 'YourTargetName' do
      pod 'LaunchDarkly', '~> 9.0'
      pod 'LaunchDarklyObservability', '~> 1.0'
      pod 'LaunchDarklySessionReplay', '~> 1.0'
    end
    ```
  </CodeGroup>

  Then, import the Session Replay plugin into your code:

  <CodeGroup>
    ```swift title="Swift" lines wrap theme={null}
    import LaunchDarkly
    import LaunchDarklyObservability
    import LaunchDarklySessionReplay
    ```
  </CodeGroup>

  ### Initialize Session Replay

  To enable Session Replay, add the `SessionReplay` plugin to your SDK configuration alongside the `Observability` plugin. The `Observability` plugin must be added before the `SessionReplay` plugin:

  <CodeGroup>
    ```swift title="iOS SDK v9.14+ with Session Replay" expandable lines wrap theme={null}
    let mobileKey = "example-mobile-key"
    let config = LDConfig(
        mobileKey: mobileKey,
        autoEnvAttributes: .enabled
    )
    config.plugins = [
        // Observability plugin must be added before SessionReplay
        Observability(options: .init(
            serviceName: "ios-app",
            sessionBackgroundTimeout: 3)),
        SessionReplay(options: .init(
            isEnabled: true,
            privacy: .init(
                maskTextInputs: true,
                maskWebViews: false,
                maskImages: false,
                maskAccessibilityIdentifiers: ["email-field", "password-field"]
            )
        ))
    ]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(
        config: config,
        context: context,
        startWaitSeconds: 5.0,
        completion: { (timedOut: Bool) -> Void in
            if timedOut {
                // Client may not have the most recent flags for the configured context
            } else {
                // Client has received flags for the configured context
            }
        }
    )
    ```
  </CodeGroup>

  ### 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 `isEnabled` to `false` in `SessionReplayOptions`, then call `LDReplay.shared.start()` when you're ready to begin recording.

  First, configure the plugin with `isEnabled: false`:

  <CodeGroup>
    ```swift title="Manual start configuration, iOS SDK v9.14+" expandable lines wrap theme={null}
    let config = LDConfig(
        mobileKey: "example-mobile-key",
        autoEnvAttributes: .enabled
    )
    config.plugins = [
        Observability(),
        SessionReplay(options: .init(
            isEnabled: false, // Don't start recording automatically
            privacy: .init(
                maskTextInputs: true
            )
        ))
    ]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(config: config, context: context, startWaitSeconds: 5)
    ```
  </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>
    ```swift title="Start recording" lines wrap theme={null}
    // Start recording after user consent or feature flag check
    LDReplay.shared.start()
    ```

    ```swift title="Start recording with feature flag" lines wrap theme={null}
    // Start recording based on a feature flag
    let replayEnabled = LDClient.get()?.boolVariation(forKey: "enable-session-replay", defaultValue: false) ?? false
    if replayEnabled {
        LDReplay.shared.start()
    }
    ```

    ```swift title="Stop recording" lines wrap theme={null}
    LDReplay.shared.stop()
    ```
  </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

  ### Configure Session Replay privacy options

  The Session Replay plugin provides privacy options to control what data is captured. Configure these options when initializing the plugin:

  <CodeGroup>
    ```swift title="Session Replay privacy options" lines wrap theme={null}
    SessionReplay(options: .init(
        isEnabled: true,
        serviceName: "my-swift-app",
        privacy: .init(
            maskTextInputs: true,
            maskWebViews: false,
            maskLabels: false,
            maskImages: false,
            maskUIViews: [SensitiveView.self],
            ignoreUIViews: [PublicView.self],
            maskAccessibilityIdentifiers: ["email-field", "password-field"],
            ignoreAccessibilityIdentifiers: ["public-label"],
            minimumAlpha: 0.02
        )
    ))
    ```
  </CodeGroup>

  #### Privacy configuration options

  The `PrivacyOptions` struct provides the following parameters:

  * **maskTextInputs**: Mask all text input fields. Defaults to `true`.
  * **maskWebViews**: Mask the contents of web views (`WKWebView` and `UIWebView`). When this setting is enabled, web views are rendered as blank rectangles in session replays. Defaults to `false`.
  * **maskLabels**: Mask all text labels. Defaults to `false`.
  * **maskImages**: Mask all images. Defaults to `false`.
  * **maskUIViews**: Array of `UIView` classes to automatically mask in recordings.
  * **ignoreUIViews**: Array of `UIView` classes to exclude from masking rules.
  * **maskAccessibilityIdentifiers**: Array of accessibility identifiers to mask. Use this to mask specific UI elements by their accessibility identifier.
  * **ignoreAccessibilityIdentifiers**: Array of accessibility identifiers to exclude from masking rules.
  * **minimumAlpha**: Minimum alpha value for view visibility in recordings. Views with alpha below this threshold are not captured. Defaults to `0.02`.

  ### Fine-grained masking control

  You can override the default privacy settings on individual views. Use `.ldMask()` to mask a view, `.ldUnmask()` to exempt one from masking, and `.ldPrivate(isEnabled:)` to mask a view conditionally. These give you precise control over what session replay captures.

  #### SwiftUI view masking

  Use view modifiers to control masking for SwiftUI views:

  <CodeGroup>
    ```swift title="SwiftUI masking control" expandable lines wrap theme={null}
    import SwiftUI
    import LaunchDarklySessionReplay

    struct ContentView: View {
        @State private var email = ""
        @State private var shouldMaskEmail = true

        var body: some View {
            VStack {
                // Mask this specific view
                Text("Sensitive information")
                    .ldMask()

                // Unmask this view (even if it would be masked by default)
                Image("profile-photo")
                    .ldUnmask()

                // Conditionally mask based on a flag
                TextField("Email", text: $email)
                    .ldPrivate(isEnabled: shouldMaskEmail)
            }
        }
    }
    ```
  </CodeGroup>

  #### UIKit view masking

  Use the `.ldMask()`, `.ldUnmask()`, and `.ldPrivate(isEnabled:)` methods on `UIView` instances:

  <CodeGroup>
    ```swift title="UIKit masking control" expandable lines wrap theme={null}
    import UIKit
    import LaunchDarklySessionReplay

    class CreditCardViewController: UIViewController {
        let cvvField = UITextField()
        let nameField = UITextField()
        let cardNumberField = UITextField()

        override func viewDidLoad() {
            super.viewDidLoad()

            // Mask the CVV field
            cvvField.ldMask()

            // Unmask the name field (even if text inputs are masked by default)
            nameField.ldUnmask()

            // Conditionally mask based on a flag
            cardNumberField.ldPrivate(isEnabled: true)
        }
    }
    ```
  </CodeGroup>

  #### 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 `maskAccessibilityIdentifiers` 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()`, the SDK does not mask the view.
  3. **Global configuration**: If a global privacy option such as `maskTextInputs` or `maskImages` applies to the view, the SDK follows that option.

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

  ## 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#ios)
  * [Logs](/docs/sdk/features/observability-logs#ios)
  * [Metrics](/docs/sdk/features/observability-metrics#ios)
  * [Symbolication](/docs/sdk/features/observability-symbolication#ios)
  * [Tracing](/docs/sdk/features/observability-traces#ios)

  ## Review observability data in LaunchDarkly

  After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly in the form of custom events and OpenTelemetry data. 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).
</View>

<View title="Federal docs">
  <Note>
    **This LaunchDarkly observability plugin is available for early access**

    This LaunchDarkly observability plugin is 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 iOS SDK.

  The iOS SDK supports the **observability plugin** for error monitoring, logging, and tracing, and the **session replay plugin** for recording user sessions.

  <Note>
    **SDK quick links**

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

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

      <tbody>
        <tr>
          <td>GitHub repository</td>
          <td>[swift-launchdarkly-observability](https://github.com/launchdarkly/swift-launchdarkly-observability)</td>
        </tr>

        <tr>
          <td>Published module</td>
          <td>[Swift Package Manager](https://swift.org/package-manager/)</td>
        </tr>
      </tbody>
    </table>
  </Note>

  ## Prerequisites and dependencies

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

  The observability plugin is compatible with the [iOS SDK](/docs/sdk/client-side/ios), version 9.14.0 and later, and is only available if you are using Swift.

  ## Get started

  Follow these steps to get started:

  * [Install the plugin](#install-the-plugin)
  * [Initialize the iOS SDK client](#initialize-the-client)
  * [Configure the plugin options](#configure-the-plugin-options)
  * [Configure product analytics event collection](#configure-product-analytics-event-collection)
  * [Track screen views](#track-screen-views)
  * [Configure session replay](#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 iOS SDK to provide observability.

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

  Here's how:

  <CodeGroup>
    ```swift title="Package.swift, using Swift Package Manager" lines wrap theme={null}
    //...
        dependencies: [
            .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor("9.0.0")),
            .package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability.git", .upToNextMinor("1.0.0")),
        ],
        targets: [
            .target(
                name: "YOUR_TARGET",
                dependencies: ["LaunchDarkly"]
            )
        ],
    //...
    ```

    ```ruby title="Podfile, using CocoaPods" lines wrap theme={null}
    use_frameworks!
    target 'YourTargetName' do
      pod 'LaunchDarkly', '~> 9.0'
      pod 'LaunchDarklyObservability', '~> 1.0'
    end
    ```

    ```swift title="Cartfile, using Carthage" lines wrap theme={null}
    github "launchdarkly/ios-client" ~> 9.0
    github "launchdarkly/swift-launchdarkly-observability" ~> 1.0
    ```
  </CodeGroup>

  Then, import the plugin into your code:

  <CodeGroup>
    ```swift title="Swift" lines wrap theme={null}
    import LaunchDarkly
    import LaunchDarklyObservability
    import OpenTelemetryApi
    ```
  </CodeGroup>

  ## Initialize the client

  Next, initialize the SDK and the plugin.

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

  Here's how to initialize the SDK and plugin:

  <CodeGroup>
    ```swift title="iOS SDK v9.14+ (Swift)" lines wrap theme={null}
    let config = LDConfig(mobileKey: "example-mobile-key", autoEnvAttributes: .enabled)
    config.plugins = [Observability()]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(config: config, context: context, startWaitSeconds: 5) { timedOut in
        if timedOut {
            // Client may not have the most recent flags for the configured context
        } else {
            // Client has received flags for the configured context
        }
    }
    ```
  </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>
    ```swift title="Plugin options, iOS SDK v9.14+" expandable lines wrap theme={null}
    // Create the plugin options
    let options = ObservabilityOptions(
        serviceName: "MyApp",
        serviceVersion: "1.2.3",
        otlpEndpoint: "https://otel.observability.app.launchdarkly.com:4318",
        resourceAttributes: [
            "environment": .string("production"),
            "team": .string("mobile-team"),
            "app.version": .string("1.2.3")
        ],
        customHeaders: ["Custom-Header": "header-value"],
        sessionBackgroundTimeout: 30 * 60, // 30 minutes in seconds
        isDebug: false
    )

    // Create the observability plugin with the options
    let observabilityPlugin = Observability(options: options)

    let config = LDConfig(mobileKey: "example-mobile-key", autoEnvAttributes: .enabled)
    config.plugins = [observabilityPlugin]
    ```
  </CodeGroup>

  ### Configuration options

  The `ObservabilityOptions` struct provides the following parameters:

  * **isEnabled**: When `false`, the plugin installs but exports no telemetry. Defaults to `true`.
  * **serviceName**: The service name for the application. Defaults to "observability-swift".
  * **serviceVersion**: The service version for the application. Defaults to "0.1.0".
  * **otlpEndpoint**: The endpoint URL for the OpenTelemetry Protocol (OTLP) exporter. Defaults to the LaunchDarkly endpoint.
  * **resourceAttributes**: Additional OpenTelemetry resource attributes to include in telemetry data.
  * **customHeaders**: Custom headers to include with OTLP exports.
  * **sessionBackgroundTimeout**: How long the app can stay in the background, in seconds, before the current session ends. Defaults to 15 minutes (900 seconds).
  * **isDebug**: Enables additional logging for debugging. Defaults to `false`.
  * **logsApiLevel**: The minimum severity of logs to export. Use `.none` to disable log collection. Defaults to `.info`.
  * **tracesApi**: Controls trace collection. Defaults to `.enabled`.
  * **metricsApi**: Controls metric collection. Defaults to `.enabled`.
  * **crashReporting**: Controls automatic crash reporting. Defaults to `.enabled`.
  * **tracingOrigins**: Which outgoing request origins propagate distributed tracing headers. Defaults to `.disabled`.
  * **urlBlocklist**: URL patterns to exclude from automatic `URLSession` instrumentation. Defaults to an empty array.
  * **contextFriendlyName**: A human-readable name to attach to the LaunchDarkly context for this session.
  * **instrumentation**: Controls automatic instrumentation. To learn more, read [Instrumentation options](#instrumentation-options).
  * **analytics**: Controls which product analytics events the plugin emits as OpenTelemetry spans. To learn more, read [Configure product analytics event collection](#configure-product-analytics-event-collection).

  ### Instrumentation options

  The `instrumentation` option controls which automatic instrumentations the plugin runs. Pass an `Instrumentation` value, or use the `.enabled` and `.disabled` presets to turn all of them on or off at once.

  The `Instrumentation` options are:

  * **urlSession**: Instruments `URLSession` requests. Defaults to `.disabled`.
  * **userTaps**: Runs tap detection. The `analytics.taps` option separately controls whether the plugin publishes detected taps as `click` spans. If `userTaps` is `.disabled`, the plugin publishes no `click` spans regardless of the `analytics.taps` value. Neither option affects session replay capture. Defaults to `.enabled`.
  * **memory**: Records memory usage metrics. Defaults to `.disabled`.
  * **memoryWarnings**: Records memory warning events. Defaults to `.disabled`.
  * **cpu**: Records CPU usage metrics. Defaults to `.disabled`.
  * **launchTimes**: Emits launch-time performance telemetry, including the `app.start` span event that carries the cold or warm startup dimension on the `app_launch` span. Defaults to `.enabled`.
  * **screens**: Automatically detects screen changes by swizzling `UIViewController`. Screen detection drives both the automatic `screen_view` span and the session replay `Navigate` events. Defaults to `.enabled`. The `analytics.screenViews` option gates `screen_view` spans.

  Here is an example that turns off automatic screen detection while leaving other instrumentation at its default:

  <CodeGroup>
    ```swift title="Instrumentation configuration" lines wrap theme={null}
    let options = ObservabilityOptions(
        instrumentation: ObservabilityOptions.Instrumentation(screens: .disabled)
    )
    ```
  </CodeGroup>

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

  ## Configure product analytics event collection

  The iOS 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** (manual): A `track` span when your code calls `track(key:properties:metricValue:)`. To learn more, read [Recording product analytics events](#recording-product-analytics-events).
  * **Screen views** (automatic and manual): A `screen_view` span when a screen appears, 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 disable all product analytics events, set `analytics` to `.disabled` in `ObservabilityOptions`. To control individual event types, pass an `Analytics` value directly:

  <CodeGroup>
    ```swift title="Disable all product analytics events" lines wrap theme={null}
    let options = ObservabilityOptions(
        analytics: .disabled
    )
    ```

    ```swift title="Control individual event types" lines wrap theme={null}
    let options = ObservabilityOptions(
        analytics: ObservabilityOptions.Analytics(
            taps: .enabled,
            trackEvents: .enabled,
            screenViews: .enabled,
            appLifecycle: .enabled,
            appLaunch: .enabled
        )
    )
    ```
  </CodeGroup>

  The `Analytics` options are:

  * **taps**: Emits a `click` span for each detected tap. Defaults to `.enabled`.
  * **trackEvents**: Emits a `track` span when a custom event is tracked with `track(key:properties:metricValue:)`. Defaults to `.enabled`.
  * **screenViews**: Emits a `screen_view` span when a screen appears. Defaults to `.enabled`.
  * **appLifecycle**: Emits `app_foreground` and `app_background` spans as the app moves between states. Defaults to `.enabled`.
  * **appLaunch**: Emits an `app_launch` span once per process launch, including an `app.start` span event with the cold or warm startup dimension. Defaults to `.enabled`.

  ### App launch events

  The iOS observability plugin records a single `app_launch` span once per process launch. The plugin captures process start timing as early as possible during launch using a startup constructor that runs before `main`, then emits the `app_launch` span after observability initializes. This means the recorded startup duration reflects actual launch time rather than SDK initialization time.

  The `app_launch` span includes:

  * `event.launch_type`: Classifies the launch as `install` (first launch after install), `update` (first launch after a version update), or `relaunch` (a subsequent launch of the same version). The plugin determines the launch type by comparing the current app version to the previously stored version.
  * Version attributes, including the current app version and, on an update, the previous version.
  * An `app.start` span event with a `start.type` attribute (`cold` or `warm`) and, when available, a `start.duration_ms` attribute. A warm start is detected when the operating system prewarms the app, using the `ActivePrewarm` environment variable on iOS 15 and later.

  To learn more about app launch attributes across SDKs, read [App launch events](/docs/home/observability/product-analytics#app-launch-events).

  ## Track screen views

  The plugin emits a `screen_view` span when your app shows a screen. Each span uses the `event.*` attribute namespace, including `event.name`, `event.screen_class`, `event.screen_id`, `event.previous_screen`, and `event.category`. The plugin resolves `event.previous_screen` from a shared screen stack to ensure correctness whether the plugin captured the screen or your code recorded it manually.

  Each recorded screen change also produces a session replay `Navigate` event to reflect navigation in the replay timeline.

  ### Capture UIKit view controllers automatically

  When `instrumentation.screens` is `.enabled` (the default), the plugin swizzles `UIViewController` and records a `screen_view` span each time a view controller appears. The plugin skips container and system controllers such as `UINavigationController` and `UITabBarController`.

  To customize how the plugin reports a controller, conform it to `LDScreenNameProviding`:

  <CodeGroup>
    ```swift title="Custom screen name" lines wrap theme={null}
    import LaunchDarklyObservability

    final class CheckoutViewController: UIViewController, LDScreenNameProviding {
        var ldScreenName: String? { "Checkout" }
        var ldScreenCategory: String? { "Commerce" }
    }
    ```
  </CodeGroup>

  ### Capture SwiftUI screens manually

  The `UIViewController` swizzle does not observe pure SwiftUI navigation, such as `NavigationStack` destinations. You must record those screens manually.

  Use the `trackScreen` modifier on the screen's root view. Only one call per screen is required, because the plugin resolves `event.previous_screen` from the shared screen stack:

  <CodeGroup>
    ```swift title="Track a SwiftUI screen" lines wrap theme={null}
    import SwiftUI
    import LaunchDarklyObservability

    struct ProfileView: View {
        var body: some View {
            VStack {
                // ...
            }
            .trackScreen("Profile", category: "Account")
        }
    }
    ```
  </CodeGroup>

  #### Track navigation stacks and modals

  The `trackScreen` modifier records on `.onAppear`. SwiftUI does not re-run `.onAppear` when you pop back to a screen in a `NavigationStack` or when a `sheet` or `fullScreenCover` closes. Use the path-aware and presentation-aware modifiers to ensure that back navigation and modal returns are tracked correctly.

  Apply `trackScreenStack` to the `NavigationStack` itself, passing the same `path` binding. The plugin records the top of the path, or the `root` name when the path is empty, on first appearance and on every push and pop. Return `nil` from `destination` to skip a screen that already records itself:

  <CodeGroup>
    ```swift title="Track a navigation stack" expandable lines wrap theme={null}
    import SwiftUI
    import LaunchDarklyObservability

    struct RootView: View {
        @State private var path: [Route] = []

        var body: some View {
            NavigationStack(path: $path) {
                // ...
            }
            .trackScreenStack(path, root: "Home") { route in
                switch route {
                case .profile: return "Profile"
                case .settings: return "Settings"
                }
            }
        }
    }
    ```
  </CodeGroup>

  Apply `trackScreenReturn` to a presenting screen to record it again after a modal closes. Pass the flag, or combination of flags, that drives the presentation. The plugin records the screen on each transition from `true` to `false`:

  <CodeGroup>
    ```swift title="Track a return from a modal" lines wrap theme={null}
    struct RootView: View {
        @State private var activeSheet: Sheet?

        var body: some View {
            List { /* ... */ }
                .sheet(item: $activeSheet) { sheet in /* ... */ }
                .trackScreenReturn("Home", isPresented: activeSheet != nil)
        }
    }
    ```
  </CodeGroup>

  Driving presentations from a single optional or enum route, such as `sheet(item:)`, keeps `isPresented` to one expression. If you use multiple `sheet(isPresented:)` flags, combine them, as in `isPresented: a || b || c`.

  Both `trackScreenStack` and `trackScreenReturn` require iOS 14, macOS 11, tvOS 14, or watchOS 7 and later.

  ### Record a screen view directly

  You can also call `trackScreenView` from anywhere after the SDK initializes. To learn more, read [Recording product analytics events](#recording-product-analytics-events).

  Manual `trackScreenView` calls work even if you disable automatic detection with `instrumentation.screens`. The `analytics.screenViews` option still gates the emitted `screen_view` span.

  ## Manual instrumentation

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

  ### Recording custom metrics

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

  <CodeGroup>
    ```swift title="Record metrics" expandable lines wrap theme={null}
    // Record a point-in-time metric
    LDObserve.shared.recordMetric(metric: Metric(name: "response_time_ms", value: 250.0))

    // Record metrics with attributes
    let attributes: [String: AttributeValue] = [
        "endpoint": .string("/api/users"),
        "method": .string("GET")
    ]
    LDObserve.shared.recordMetric(metric: Metric(
        name: "api_call_duration", 
        value: 120.5, 
        attributes: attributes
    ))

    // Record different metric types
    LDObserve.shared.recordCount(metric: Metric(name: "button_clicks", value: 1.0))
    LDObserve.shared.recordIncr(metric: Metric(name: "page_views", value: 1.0))
    LDObserve.shared.recordHistogram(metric: Metric(name: "request_size_bytes", value: 1024.0))
    LDObserve.shared.recordUpDownCounter(metric: Metric(name: "active_connections", value: 5.0))
    ```
  </CodeGroup>

  ### Recording custom logs

  Use `LDObserve.shared.recordLog` to emit a structured log. Pass attributes as a plain Swift dictionary through the `properties` parameter:

  <CodeGroup>
    ```swift title="Record logs" expandable lines wrap theme={null}
    // Record a basic log message
    LDObserve.shared.recordLog(
        message: "User login successful",
        severity: .info,
        properties: [:]
    )

    // Record logs with custom properties
    LDObserve.shared.recordLog(
        message: "Authentication completed",
        severity: .info,
        properties: [
            "user_id": "12345",
            "action": "login"
        ]
    )
    ```

    ```swift title="Record logs with OTel-typed attributes (advanced)" lines wrap theme={null}
    // Use OTel-typed attributes when exact OpenTelemetry typing is required
    let logAttributes: [String: AttributeValue] = [
        "user_id": .string("12345"),
        "action": .string("login")
    ]
    LDObserve.shared.recordLog(
        message: "Authentication completed",
        severity: .info,
        attributes: logAttributes
    )
    ```
  </CodeGroup>

  ### Recording custom errors

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

  <CodeGroup>
    ```swift title="Record errors" lines wrap theme={null}
    do {
        // Some operation that might fail
        try performNetworkRequest()
    } catch {
        // Record the error with context
        let errorAttributes: [String: AttributeValue] = [
            "operation": .string("network_request"),
            "endpoint": .string("/api/data")
        ]
        LDObserve.shared.recordError(error: error, attributes: errorAttributes)
    }
    ```
  </CodeGroup>

  ### Recording custom traces

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

  <CodeGroup>
    ```swift title="Record traces" expandable lines wrap theme={null}
    // Start a span with custom properties
    let span = LDObserve.shared.startSpan(
        name: "database_query",
        properties: [
            "table": "users",
            "operation": "select"
        ]
    )

    // Perform your operation
    performDatabaseQuery()

    // Optionally add more attributes during execution
    span.setAttribute(key: "rows_returned", value: .int(42))

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

    ```swift title="Record traces with OTel-typed attributes (advanced)" lines wrap theme={null}
    // Use OTel-typed attributes when exact OpenTelemetry typing is required
    let attributes: [String: AttributeValue] = [
        "table": .string("users"),
        "operation": .string("select")
    ]
    let span = LDObserve.shared.startSpan(name: "database_query", attributes: attributes)
    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>
    ```swift title="Track a custom event" lines wrap theme={null}
    // Track an event with properties and an optional metric value
    LDObserve.shared.track(
        key: "purchase_completed",
        properties: [
            "product_id": "SKU-123",
            "price": 29.99
        ],
        metricValue: 29.99
    )

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

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

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

    // Full details with custom properties
    LDObserve.shared.trackScreenView(
        name: "ProductDetail",
        screenClass: "ProductDetailViewController",
        screenId: "product-123",
        category: "browsing",
        properties: [
            "product_id": "SKU-123",
            "source": "search_results"
        ]
    )
    ```
  </CodeGroup>

  ### Using span builder for advanced tracing

  Use `LDObserve.shared.spanBuilder` when you need more control over a span than `startSpan` provides, such as setting the span kind or adding attributes before the span starts:

  <CodeGroup>
    ```swift title="Advanced span usage" lines wrap theme={null}
    // Get span builder for more control
    let spanBuilder = LDObserve.shared.spanBuilder(spanName: "complex_operation")
        .setSpanKind(spanKind: .client)

    spanBuilder.setAttribute(key: "user.id", value: .string("12345"))
    let span = spanBuilder.startSpan()

    // Make the span current for nested operations
    span.makeCurrent()

    // Perform work that might create child spans
    performComplexWork()

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

  ### Using 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 scope automatically use the context of their parent span, Swift's concurrency model (`async`, `await`, or `Task`) does not propagate the parent span context. Use the OpenTelemetry API to obtain and pass the parent span context explicitly to asynchronous methods:

  <CodeGroup>
    ```swift title="Obtain and propagate span context to asynchronous methods" lines wrap theme={null}
    let span0 = LDObserve.shared.startSpan(name: "parentSpan", properties: [:])

    await OpenTelemetry.instance.contextProvider.withActiveSpan(span0) {
      let span1 = LDObserve.shared.startSpan(name: "NestedSpan1", properties: [:])
      await OpenTelemetry.instance.contextProvider.withActiveSpan(span1) {
        LDObserve.shared.recordLog(message: "NestedLog", severity: .info, properties: [:])
      }
      span1.end()
    }
    span0.end()
    ```
  </CodeGroup>

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

  <CodeGroup>
    ```swift title="Obtain and restore a span context" expandable lines wrap theme={null}
    // Capture the current context
    let span = LDObserve.shared.startSpan(
        name: "log-context-demo",
        properties: ["demo": "log-with-context"]
    )
    let capturedContext = span.context
    span.end()

    // Simulate a detached task where the OpenTelemetry context would be lost automatically.
    DispatchQueue.global(qos: .background).async {
        LDObserve.shared.recordLog(
            message: "Log with span context",
            severity: .warn,
            properties: ["source": "detached-queue-demo"],
            spanContext: capturedContext
        )
    }
    ```
  </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>
    ```swift title="Inject context into HTTP header" lines wrap theme={null}
    import OpenTelemetryApi

    var request = URLRequest(url: url)
    let activeSpan = OpenTelemetry.instance.contextProvider.activeSpan
    let propagator = W3CTraceContextPropagator()

    // Inject current active span context into headers
    var headers: [String: String] = [:]
    propagator.inject(
        spanContext: activeSpan.context,
        carrier: &headers,
        setter: { carrier, key, value in carrier[key] = value }
    )
    headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
    ```
  </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 would use the `extract` method from `W3CTraceContextPropagator` to obtain fields from header and use them as the parent context for new spans.

  ## Session management

  The observability plugin automatically manages sessions and handles application lifecycle events. Sessions are automatically ended when:

  * The application is backgrounded for longer than the configured `sessionBackgroundTimeout`, which defaults to 15 minutes
  * The application is terminated
  * A new session is explicitly started

  ### Session timeout configuration

  You can configure how long the plugin waits before ending a session when the app goes to the background:

  <CodeGroup>
    ```swift title="Configure session timeout" lines wrap theme={null}
    let options = ObservabilityOptions(
        sessionBackgroundTimeout: 45 * 60 // 45 minutes in seconds
    )
    ```
  </CodeGroup>

  ## Automatic instrumentation

  The observability plugin automatically instruments your iOS application to collect:

  * **Application launch and lifecycle events**: App launch, foreground, and background events, recorded as product analytics spans. To learn more, read [Configure product analytics event collection](#configure-product-analytics-event-collection).
  * **Session tracking**: Automatic session start and end events with timing.
  * **Network requests**: HTTP request/response data when enabled.
  * **Taps**: A `click` span for each tap interaction.
  * **Screen views**: A `screen_view` span when a `UIViewController` appears, and a session replay `Navigate` event on each screen change. To learn more, read [Track screen views](#track-screen-views).
  * **LaunchDarkly SDK events**: Feature flag evaluations and SDK operations.

  ## Session Replay

  <Note>
    **Session Replay is in Early Access**

    Session Replay is available in Early Access. APIs are subject to change until a 1.x version is released.
  </Note>

  Session Replay captures user interactions and screen recordings to help you understand how users interact with your application. Session Replay works as an additional plugin that requires the observability plugin to be configured first.

  ### Install the Session Replay plugin

  First, add the Session Replay package as a dependency alongside the observability plugin:

  <CodeGroup>
    ```swift title="Package.swift, using Swift Package Manager" lines wrap theme={null}
    //...
        dependencies: [
            .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor("9.0.0")),
            .package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability.git", .upToNextMinor("1.0.0")),
        ],
        targets: [
            .target(
                name: "YOUR_TARGET",
                dependencies: [
                    "LaunchDarkly",
                    .product(name: "LaunchDarklySessionReplay", package: "swift-launchdarkly-observability")
                ]
            )
        ],
    //...
    ```

    ```ruby title="Podfile, using CocoaPods" lines wrap theme={null}
    use_frameworks!
    target 'YourTargetName' do
      pod 'LaunchDarkly', '~> 9.0'
      pod 'LaunchDarklyObservability', '~> 1.0'
      pod 'LaunchDarklySessionReplay', '~> 1.0'
    end
    ```
  </CodeGroup>

  Then, import the Session Replay plugin into your code:

  <CodeGroup>
    ```swift title="Swift" lines wrap theme={null}
    import LaunchDarkly
    import LaunchDarklyObservability
    import LaunchDarklySessionReplay
    ```
  </CodeGroup>

  ### Initialize Session Replay

  To enable Session Replay, add the `SessionReplay` plugin to your SDK configuration alongside the `Observability` plugin. The `Observability` plugin must be added before the `SessionReplay` plugin:

  <CodeGroup>
    ```swift title="iOS SDK v9.14+ with Session Replay" expandable lines wrap theme={null}
    let mobileKey = "example-mobile-key"
    let config = LDConfig(
        mobileKey: mobileKey,
        autoEnvAttributes: .enabled
    )
    config.plugins = [
        // Observability plugin must be added before SessionReplay
        Observability(options: .init(
            serviceName: "ios-app",
            sessionBackgroundTimeout: 3)),
        SessionReplay(options: .init(
            isEnabled: true,
            privacy: .init(
                maskTextInputs: true,
                maskWebViews: false,
                maskImages: false,
                maskAccessibilityIdentifiers: ["email-field", "password-field"]
            )
        ))
    ]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(
        config: config,
        context: context,
        startWaitSeconds: 5.0,
        completion: { (timedOut: Bool) -> Void in
            if timedOut {
                // Client may not have the most recent flags for the configured context
            } else {
                // Client has received flags for the configured context
            }
        }
    )
    ```
  </CodeGroup>

  ### 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 `isEnabled` to `false` in `SessionReplayOptions`, then call `LDReplay.shared.start()` when you're ready to begin recording.

  First, configure the plugin with `isEnabled: false`:

  <CodeGroup>
    ```swift title="Manual start configuration, iOS SDK v9.14+" expandable lines wrap theme={null}
    let config = LDConfig(
        mobileKey: "example-mobile-key",
        autoEnvAttributes: .enabled
    )
    config.plugins = [
        Observability(),
        SessionReplay(options: .init(
            isEnabled: false, // Don't start recording automatically
            privacy: .init(
                maskTextInputs: true
            )
        ))
    ]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(config: config, context: context, startWaitSeconds: 5)
    ```
  </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>
    ```swift title="Start recording" lines wrap theme={null}
    // Start recording after user consent or feature flag check
    LDReplay.shared.start()
    ```

    ```swift title="Start recording with feature flag" lines wrap theme={null}
    // Start recording based on a feature flag
    let replayEnabled = LDClient.get()?.boolVariation(forKey: "enable-session-replay", defaultValue: false) ?? false
    if replayEnabled {
        LDReplay.shared.start()
    }
    ```

    ```swift title="Stop recording" lines wrap theme={null}
    LDReplay.shared.stop()
    ```
  </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

  ### Configure Session Replay privacy options

  The Session Replay plugin provides privacy options to control what data is captured. Configure these options when initializing the plugin:

  <CodeGroup>
    ```swift title="Session Replay privacy options" lines wrap theme={null}
    SessionReplay(options: .init(
        isEnabled: true,
        serviceName: "my-swift-app",
        privacy: .init(
            maskTextInputs: true,
            maskWebViews: false,
            maskLabels: false,
            maskImages: false,
            maskUIViews: [SensitiveView.self],
            ignoreUIViews: [PublicView.self],
            maskAccessibilityIdentifiers: ["email-field", "password-field"],
            ignoreAccessibilityIdentifiers: ["public-label"],
            minimumAlpha: 0.02
        )
    ))
    ```
  </CodeGroup>

  #### Privacy configuration options

  The `PrivacyOptions` struct provides the following parameters:

  * **maskTextInputs**: Mask all text input fields. Defaults to `true`.
  * **maskWebViews**: Mask the contents of web views (`WKWebView` and `UIWebView`). When this setting is enabled, web views are rendered as blank rectangles in session replays. Defaults to `false`.
  * **maskLabels**: Mask all text labels. Defaults to `false`.
  * **maskImages**: Mask all images. Defaults to `false`.
  * **maskUIViews**: Array of `UIView` classes to automatically mask in recordings.
  * **ignoreUIViews**: Array of `UIView` classes to exclude from masking rules.
  * **maskAccessibilityIdentifiers**: Array of accessibility identifiers to mask. Use this to mask specific UI elements by their accessibility identifier.
  * **ignoreAccessibilityIdentifiers**: Array of accessibility identifiers to exclude from masking rules.
  * **minimumAlpha**: Minimum alpha value for view visibility in recordings. Views with alpha below this threshold are not captured. Defaults to `0.02`.

  ### Fine-grained masking control

  You can override the default privacy settings on individual views. Use `.ldMask()` to mask a view, `.ldUnmask()` to exempt one from masking, and `.ldPrivate(isEnabled:)` to mask a view conditionally. These give you precise control over what session replay captures.

  #### SwiftUI view masking

  Use view modifiers to control masking for SwiftUI views:

  <CodeGroup>
    ```swift title="SwiftUI masking control" expandable lines wrap theme={null}
    import SwiftUI
    import LaunchDarklySessionReplay

    struct ContentView: View {
        @State private var email = ""
        @State private var shouldMaskEmail = true

        var body: some View {
            VStack {
                // Mask this specific view
                Text("Sensitive information")
                    .ldMask()

                // Unmask this view (even if it would be masked by default)
                Image("profile-photo")
                    .ldUnmask()

                // Conditionally mask based on a flag
                TextField("Email", text: $email)
                    .ldPrivate(isEnabled: shouldMaskEmail)
            }
        }
    }
    ```
  </CodeGroup>

  #### UIKit view masking

  Use the `.ldMask()`, `.ldUnmask()`, and `.ldPrivate(isEnabled:)` methods on `UIView` instances:

  <CodeGroup>
    ```swift title="UIKit masking control" expandable lines wrap theme={null}
    import UIKit
    import LaunchDarklySessionReplay

    class CreditCardViewController: UIViewController {
        let cvvField = UITextField()
        let nameField = UITextField()
        let cardNumberField = UITextField()

        override func viewDidLoad() {
            super.viewDidLoad()

            // Mask the CVV field
            cvvField.ldMask()

            // Unmask the name field (even if text inputs are masked by default)
            nameField.ldUnmask()

            // Conditionally mask based on a flag
            cardNumberField.ldPrivate(isEnabled: true)
        }
    }
    ```
  </CodeGroup>

  #### 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 `maskAccessibilityIdentifiers` 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()`, the SDK does not mask the view.
  3. **Global configuration**: If a global privacy option such as `maskTextInputs` or `maskImages` applies to the view, the SDK follows that option.

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

  ## 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#ios)
  * [Logs](/docs/sdk/features/observability-logs#ios)
  * [Metrics](/docs/sdk/features/observability-metrics#ios)
  * [Symbolication](/docs/sdk/features/observability-symbolication#ios)
  * [Tracing](/docs/sdk/features/observability-traces#ios)

  ## Review observability data in LaunchDarkly

  After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly in the form of custom events and OpenTelemetry data. 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).
</View>

<View title="EU docs">
  <Note>
    **This LaunchDarkly observability plugin is available for early access**

    This LaunchDarkly observability plugin is 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 iOS SDK.

  The iOS SDK supports the **observability plugin** for error monitoring, logging, and tracing, and the **session replay plugin** for recording user sessions.

  <Note>
    **SDK quick links**

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

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

      <tbody>
        <tr>
          <td>GitHub repository</td>
          <td>[swift-launchdarkly-observability](https://github.com/launchdarkly/swift-launchdarkly-observability)</td>
        </tr>

        <tr>
          <td>Published module</td>
          <td>[Swift Package Manager](https://swift.org/package-manager/)</td>
        </tr>
      </tbody>
    </table>
  </Note>

  ## Prerequisites and dependencies

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

  The observability plugin is compatible with the [iOS SDK](/docs/sdk/client-side/ios), version 9.14.0 and later, and is only available if you are using Swift.

  ## Get started

  Follow these steps to get started:

  * [Install the plugin](#install-the-plugin)
  * [Initialize the iOS SDK client](#initialize-the-client)
  * [Configure the plugin options](#configure-the-plugin-options)
  * [Configure product analytics event collection](#configure-product-analytics-event-collection)
  * [Track screen views](#track-screen-views)
  * [Configure session replay](#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 iOS SDK to provide observability.

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

  Here's how:

  <CodeGroup>
    ```swift title="Package.swift, using Swift Package Manager" lines wrap theme={null}
    //...
        dependencies: [
            .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor("9.0.0")),
            .package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability.git", .upToNextMinor("1.0.0")),
        ],
        targets: [
            .target(
                name: "YOUR_TARGET",
                dependencies: ["LaunchDarkly"]
            )
        ],
    //...
    ```

    ```ruby title="Podfile, using CocoaPods" lines wrap theme={null}
    use_frameworks!
    target 'YourTargetName' do
      pod 'LaunchDarkly', '~> 9.0'
      pod 'LaunchDarklyObservability', '~> 1.0'
    end
    ```

    ```swift title="Cartfile, using Carthage" lines wrap theme={null}
    github "launchdarkly/ios-client" ~> 9.0
    github "launchdarkly/swift-launchdarkly-observability" ~> 1.0
    ```
  </CodeGroup>

  Then, import the plugin into your code:

  <CodeGroup>
    ```swift title="Swift" lines wrap theme={null}
    import LaunchDarkly
    import LaunchDarklyObservability
    import OpenTelemetryApi
    ```
  </CodeGroup>

  ## Initialize the client

  Next, initialize the SDK and the plugin.

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

  Here's how to initialize the SDK and plugin:

  <CodeGroup>
    ```swift title="iOS SDK v9.14+ (Swift)" lines wrap theme={null}
    let config = LDConfig(mobileKey: "example-mobile-key", autoEnvAttributes: .enabled)
    config.plugins = [Observability()]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(config: config, context: context, startWaitSeconds: 5) { timedOut in
        if timedOut {
            // Client may not have the most recent flags for the configured context
        } else {
            // Client has received flags for the configured context
        }
    }
    ```
  </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>
    ```swift title="Plugin options, iOS SDK v9.14+" expandable lines wrap theme={null}
    // Create the plugin options
    let options = ObservabilityOptions(
        serviceName: "MyApp",
        serviceVersion: "1.2.3",
        otlpEndpoint: "https://otel.observability.app.eu.launchdarkly.com:4318",
        resourceAttributes: [
            "environment": .string("production"),
            "team": .string("mobile-team"),
            "app.version": .string("1.2.3")
        ],
        customHeaders: ["Custom-Header": "header-value"],
        sessionBackgroundTimeout: 30 * 60, // 30 minutes in seconds
        isDebug: false
    )

    // Create the observability plugin with the options
    let observabilityPlugin = Observability(options: options)

    let config = LDConfig(mobileKey: "example-mobile-key", autoEnvAttributes: .enabled)
    config.plugins = [observabilityPlugin]
    ```
  </CodeGroup>

  ### Configuration options

  The `ObservabilityOptions` struct provides the following parameters:

  * **isEnabled**: When `false`, the plugin installs but exports no telemetry. Defaults to `true`.
  * **serviceName**: The service name for the application. Defaults to "observability-swift".
  * **serviceVersion**: The service version for the application. Defaults to "0.1.0".
  * **otlpEndpoint**: The endpoint URL for the OpenTelemetry Protocol (OTLP) exporter. Defaults to the LaunchDarkly endpoint.
  * **resourceAttributes**: Additional OpenTelemetry resource attributes to include in telemetry data.
  * **customHeaders**: Custom headers to include with OTLP exports.
  * **sessionBackgroundTimeout**: How long the app can stay in the background, in seconds, before the current session ends. Defaults to 15 minutes (900 seconds).
  * **isDebug**: Enables additional logging for debugging. Defaults to `false`.
  * **logsApiLevel**: The minimum severity of logs to export. Use `.none` to disable log collection. Defaults to `.info`.
  * **tracesApi**: Controls trace collection. Defaults to `.enabled`.
  * **metricsApi**: Controls metric collection. Defaults to `.enabled`.
  * **crashReporting**: Controls automatic crash reporting. Defaults to `.enabled`.
  * **tracingOrigins**: Which outgoing request origins propagate distributed tracing headers. Defaults to `.disabled`.
  * **urlBlocklist**: URL patterns to exclude from automatic `URLSession` instrumentation. Defaults to an empty array.
  * **contextFriendlyName**: A human-readable name to attach to the LaunchDarkly context for this session.
  * **instrumentation**: Controls automatic instrumentation. To learn more, read [Instrumentation options](#instrumentation-options).
  * **analytics**: Controls which product analytics events the plugin emits as OpenTelemetry spans. To learn more, read [Configure product analytics event collection](#configure-product-analytics-event-collection).

  ### Instrumentation options

  The `instrumentation` option controls which automatic instrumentations the plugin runs. Pass an `Instrumentation` value, or use the `.enabled` and `.disabled` presets to turn all of them on or off at once.

  The `Instrumentation` options are:

  * **urlSession**: Instruments `URLSession` requests. Defaults to `.disabled`.
  * **userTaps**: Runs tap detection. The `analytics.taps` option separately controls whether the plugin publishes detected taps as `click` spans. If `userTaps` is `.disabled`, the plugin publishes no `click` spans regardless of the `analytics.taps` value. Neither option affects session replay capture. Defaults to `.enabled`.
  * **memory**: Records memory usage metrics. Defaults to `.disabled`.
  * **memoryWarnings**: Records memory warning events. Defaults to `.disabled`.
  * **cpu**: Records CPU usage metrics. Defaults to `.disabled`.
  * **launchTimes**: Emits launch-time performance telemetry, including the `app.start` span event that carries the cold or warm startup dimension on the `app_launch` span. Defaults to `.enabled`.
  * **screens**: Automatically detects screen changes by swizzling `UIViewController`. Screen detection drives both the automatic `screen_view` span and the session replay `Navigate` events. Defaults to `.enabled`. The `analytics.screenViews` option gates `screen_view` spans.

  Here is an example that turns off automatic screen detection while leaving other instrumentation at its default:

  <CodeGroup>
    ```swift title="Instrumentation configuration" lines wrap theme={null}
    let options = ObservabilityOptions(
        instrumentation: ObservabilityOptions.Instrumentation(screens: .disabled)
    )
    ```
  </CodeGroup>

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

  ## Configure product analytics event collection

  The iOS 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** (manual): A `track` span when your code calls `track(key:properties:metricValue:)`. To learn more, read [Recording product analytics events](#recording-product-analytics-events).
  * **Screen views** (automatic and manual): A `screen_view` span when a screen appears, 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 disable all product analytics events, set `analytics` to `.disabled` in `ObservabilityOptions`. To control individual event types, pass an `Analytics` value directly:

  <CodeGroup>
    ```swift title="Disable all product analytics events" lines wrap theme={null}
    let options = ObservabilityOptions(
        analytics: .disabled
    )
    ```

    ```swift title="Control individual event types" lines wrap theme={null}
    let options = ObservabilityOptions(
        analytics: ObservabilityOptions.Analytics(
            taps: .enabled,
            trackEvents: .enabled,
            screenViews: .enabled,
            appLifecycle: .enabled,
            appLaunch: .enabled
        )
    )
    ```
  </CodeGroup>

  The `Analytics` options are:

  * **taps**: Emits a `click` span for each detected tap. Defaults to `.enabled`.
  * **trackEvents**: Emits a `track` span when a custom event is tracked with `track(key:properties:metricValue:)`. Defaults to `.enabled`.
  * **screenViews**: Emits a `screen_view` span when a screen appears. Defaults to `.enabled`.
  * **appLifecycle**: Emits `app_foreground` and `app_background` spans as the app moves between states. Defaults to `.enabled`.
  * **appLaunch**: Emits an `app_launch` span once per process launch, including an `app.start` span event with the cold or warm startup dimension. Defaults to `.enabled`.

  ### App launch events

  The iOS observability plugin records a single `app_launch` span once per process launch. The plugin captures process start timing as early as possible during launch using a startup constructor that runs before `main`, then emits the `app_launch` span after observability initializes. This means the recorded startup duration reflects actual launch time rather than SDK initialization time.

  The `app_launch` span includes:

  * `event.launch_type`: Classifies the launch as `install` (first launch after install), `update` (first launch after a version update), or `relaunch` (a subsequent launch of the same version). The plugin determines the launch type by comparing the current app version to the previously stored version.
  * Version attributes, including the current app version and, on an update, the previous version.
  * An `app.start` span event with a `start.type` attribute (`cold` or `warm`) and, when available, a `start.duration_ms` attribute. A warm start is detected when the operating system prewarms the app, using the `ActivePrewarm` environment variable on iOS 15 and later.

  To learn more about app launch attributes across SDKs, read [App launch events](/docs/home/observability/product-analytics#app-launch-events).

  ## Track screen views

  The plugin emits a `screen_view` span when your app shows a screen. Each span uses the `event.*` attribute namespace, including `event.name`, `event.screen_class`, `event.screen_id`, `event.previous_screen`, and `event.category`. The plugin resolves `event.previous_screen` from a shared screen stack to ensure correctness whether the plugin captured the screen or your code recorded it manually.

  Each recorded screen change also produces a session replay `Navigate` event to reflect navigation in the replay timeline.

  ### Capture UIKit view controllers automatically

  When `instrumentation.screens` is `.enabled` (the default), the plugin swizzles `UIViewController` and records a `screen_view` span each time a view controller appears. The plugin skips container and system controllers such as `UINavigationController` and `UITabBarController`.

  To customize how the plugin reports a controller, conform it to `LDScreenNameProviding`:

  <CodeGroup>
    ```swift title="Custom screen name" lines wrap theme={null}
    import LaunchDarklyObservability

    final class CheckoutViewController: UIViewController, LDScreenNameProviding {
        var ldScreenName: String? { "Checkout" }
        var ldScreenCategory: String? { "Commerce" }
    }
    ```
  </CodeGroup>

  ### Capture SwiftUI screens manually

  The `UIViewController` swizzle does not observe pure SwiftUI navigation, such as `NavigationStack` destinations. You must record those screens manually.

  Use the `trackScreen` modifier on the screen's root view. Only one call per screen is required, because the plugin resolves `event.previous_screen` from the shared screen stack:

  <CodeGroup>
    ```swift title="Track a SwiftUI screen" lines wrap theme={null}
    import SwiftUI
    import LaunchDarklyObservability

    struct ProfileView: View {
        var body: some View {
            VStack {
                // ...
            }
            .trackScreen("Profile", category: "Account")
        }
    }
    ```
  </CodeGroup>

  #### Track navigation stacks and modals

  The `trackScreen` modifier records on `.onAppear`. SwiftUI does not re-run `.onAppear` when you pop back to a screen in a `NavigationStack` or when a `sheet` or `fullScreenCover` closes. Use the path-aware and presentation-aware modifiers to ensure that back navigation and modal returns are tracked correctly.

  Apply `trackScreenStack` to the `NavigationStack` itself, passing the same `path` binding. The plugin records the top of the path, or the `root` name when the path is empty, on first appearance and on every push and pop. Return `nil` from `destination` to skip a screen that already records itself:

  <CodeGroup>
    ```swift title="Track a navigation stack" expandable lines wrap theme={null}
    import SwiftUI
    import LaunchDarklyObservability

    struct RootView: View {
        @State private var path: [Route] = []

        var body: some View {
            NavigationStack(path: $path) {
                // ...
            }
            .trackScreenStack(path, root: "Home") { route in
                switch route {
                case .profile: return "Profile"
                case .settings: return "Settings"
                }
            }
        }
    }
    ```
  </CodeGroup>

  Apply `trackScreenReturn` to a presenting screen to record it again after a modal closes. Pass the flag, or combination of flags, that drives the presentation. The plugin records the screen on each transition from `true` to `false`:

  <CodeGroup>
    ```swift title="Track a return from a modal" lines wrap theme={null}
    struct RootView: View {
        @State private var activeSheet: Sheet?

        var body: some View {
            List { /* ... */ }
                .sheet(item: $activeSheet) { sheet in /* ... */ }
                .trackScreenReturn("Home", isPresented: activeSheet != nil)
        }
    }
    ```
  </CodeGroup>

  Driving presentations from a single optional or enum route, such as `sheet(item:)`, keeps `isPresented` to one expression. If you use multiple `sheet(isPresented:)` flags, combine them, as in `isPresented: a || b || c`.

  Both `trackScreenStack` and `trackScreenReturn` require iOS 14, macOS 11, tvOS 14, or watchOS 7 and later.

  ### Record a screen view directly

  You can also call `trackScreenView` from anywhere after the SDK initializes. To learn more, read [Recording product analytics events](#recording-product-analytics-events).

  Manual `trackScreenView` calls work even if you disable automatic detection with `instrumentation.screens`. The `analytics.screenViews` option still gates the emitted `screen_view` span.

  ## Manual instrumentation

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

  ### Recording custom metrics

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

  <CodeGroup>
    ```swift title="Record metrics" expandable lines wrap theme={null}
    // Record a point-in-time metric
    LDObserve.shared.recordMetric(metric: Metric(name: "response_time_ms", value: 250.0))

    // Record metrics with attributes
    let attributes: [String: AttributeValue] = [
        "endpoint": .string("/api/users"),
        "method": .string("GET")
    ]
    LDObserve.shared.recordMetric(metric: Metric(
        name: "api_call_duration", 
        value: 120.5, 
        attributes: attributes
    ))

    // Record different metric types
    LDObserve.shared.recordCount(metric: Metric(name: "button_clicks", value: 1.0))
    LDObserve.shared.recordIncr(metric: Metric(name: "page_views", value: 1.0))
    LDObserve.shared.recordHistogram(metric: Metric(name: "request_size_bytes", value: 1024.0))
    LDObserve.shared.recordUpDownCounter(metric: Metric(name: "active_connections", value: 5.0))
    ```
  </CodeGroup>

  ### Recording custom logs

  Use `LDObserve.shared.recordLog` to emit a structured log. Pass attributes as a plain Swift dictionary through the `properties` parameter:

  <CodeGroup>
    ```swift title="Record logs" expandable lines wrap theme={null}
    // Record a basic log message
    LDObserve.shared.recordLog(
        message: "User login successful",
        severity: .info,
        properties: [:]
    )

    // Record logs with custom properties
    LDObserve.shared.recordLog(
        message: "Authentication completed",
        severity: .info,
        properties: [
            "user_id": "12345",
            "action": "login"
        ]
    )
    ```

    ```swift title="Record logs with OTel-typed attributes (advanced)" lines wrap theme={null}
    // Use OTel-typed attributes when exact OpenTelemetry typing is required
    let logAttributes: [String: AttributeValue] = [
        "user_id": .string("12345"),
        "action": .string("login")
    ]
    LDObserve.shared.recordLog(
        message: "Authentication completed",
        severity: .info,
        attributes: logAttributes
    )
    ```
  </CodeGroup>

  ### Recording custom errors

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

  <CodeGroup>
    ```swift title="Record errors" lines wrap theme={null}
    do {
        // Some operation that might fail
        try performNetworkRequest()
    } catch {
        // Record the error with context
        let errorAttributes: [String: AttributeValue] = [
            "operation": .string("network_request"),
            "endpoint": .string("/api/data")
        ]
        LDObserve.shared.recordError(error: error, attributes: errorAttributes)
    }
    ```
  </CodeGroup>

  ### Recording custom traces

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

  <CodeGroup>
    ```swift title="Record traces" expandable lines wrap theme={null}
    // Start a span with custom properties
    let span = LDObserve.shared.startSpan(
        name: "database_query",
        properties: [
            "table": "users",
            "operation": "select"
        ]
    )

    // Perform your operation
    performDatabaseQuery()

    // Optionally add more attributes during execution
    span.setAttribute(key: "rows_returned", value: .int(42))

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

    ```swift title="Record traces with OTel-typed attributes (advanced)" lines wrap theme={null}
    // Use OTel-typed attributes when exact OpenTelemetry typing is required
    let attributes: [String: AttributeValue] = [
        "table": .string("users"),
        "operation": .string("select")
    ]
    let span = LDObserve.shared.startSpan(name: "database_query", attributes: attributes)
    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>
    ```swift title="Track a custom event" lines wrap theme={null}
    // Track an event with properties and an optional metric value
    LDObserve.shared.track(
        key: "purchase_completed",
        properties: [
            "product_id": "SKU-123",
            "price": 29.99
        ],
        metricValue: 29.99
    )

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

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

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

    // Full details with custom properties
    LDObserve.shared.trackScreenView(
        name: "ProductDetail",
        screenClass: "ProductDetailViewController",
        screenId: "product-123",
        category: "browsing",
        properties: [
            "product_id": "SKU-123",
            "source": "search_results"
        ]
    )
    ```
  </CodeGroup>

  ### Using span builder for advanced tracing

  Use `LDObserve.shared.spanBuilder` when you need more control over a span than `startSpan` provides, such as setting the span kind or adding attributes before the span starts:

  <CodeGroup>
    ```swift title="Advanced span usage" lines wrap theme={null}
    // Get span builder for more control
    let spanBuilder = LDObserve.shared.spanBuilder(spanName: "complex_operation")
        .setSpanKind(spanKind: .client)

    spanBuilder.setAttribute(key: "user.id", value: .string("12345"))
    let span = spanBuilder.startSpan()

    // Make the span current for nested operations
    span.makeCurrent()

    // Perform work that might create child spans
    performComplexWork()

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

  ### Using 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 scope automatically use the context of their parent span, Swift's concurrency model (`async`, `await`, or `Task`) does not propagate the parent span context. Use the OpenTelemetry API to obtain and pass the parent span context explicitly to asynchronous methods:

  <CodeGroup>
    ```swift title="Obtain and propagate span context to asynchronous methods" lines wrap theme={null}
    let span0 = LDObserve.shared.startSpan(name: "parentSpan", properties: [:])

    await OpenTelemetry.instance.contextProvider.withActiveSpan(span0) {
      let span1 = LDObserve.shared.startSpan(name: "NestedSpan1", properties: [:])
      await OpenTelemetry.instance.contextProvider.withActiveSpan(span1) {
        LDObserve.shared.recordLog(message: "NestedLog", severity: .info, properties: [:])
      }
      span1.end()
    }
    span0.end()
    ```
  </CodeGroup>

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

  <CodeGroup>
    ```swift title="Obtain and restore a span context" expandable lines wrap theme={null}
    // Capture the current context
    let span = LDObserve.shared.startSpan(
        name: "log-context-demo",
        properties: ["demo": "log-with-context"]
    )
    let capturedContext = span.context
    span.end()

    // Simulate a detached task where the OpenTelemetry context would be lost automatically.
    DispatchQueue.global(qos: .background).async {
        LDObserve.shared.recordLog(
            message: "Log with span context",
            severity: .warn,
            properties: ["source": "detached-queue-demo"],
            spanContext: capturedContext
        )
    }
    ```
  </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>
    ```swift title="Inject context into HTTP header" lines wrap theme={null}
    import OpenTelemetryApi

    var request = URLRequest(url: url)
    let activeSpan = OpenTelemetry.instance.contextProvider.activeSpan
    let propagator = W3CTraceContextPropagator()

    // Inject current active span context into headers
    var headers: [String: String] = [:]
    propagator.inject(
        spanContext: activeSpan.context,
        carrier: &headers,
        setter: { carrier, key, value in carrier[key] = value }
    )
    headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
    ```
  </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 would use the `extract` method from `W3CTraceContextPropagator` to obtain fields from header and use them as the parent context for new spans.

  ## Session management

  The observability plugin automatically manages sessions and handles application lifecycle events. Sessions are automatically ended when:

  * The application is backgrounded for longer than the configured `sessionBackgroundTimeout`, which defaults to 15 minutes
  * The application is terminated
  * A new session is explicitly started

  ### Session timeout configuration

  You can configure how long the plugin waits before ending a session when the app goes to the background:

  <CodeGroup>
    ```swift title="Configure session timeout" lines wrap theme={null}
    let options = ObservabilityOptions(
        sessionBackgroundTimeout: 45 * 60 // 45 minutes in seconds
    )
    ```
  </CodeGroup>

  ## Automatic instrumentation

  The observability plugin automatically instruments your iOS application to collect:

  * **Application launch and lifecycle events**: App launch, foreground, and background events, recorded as product analytics spans. To learn more, read [Configure product analytics event collection](#configure-product-analytics-event-collection).
  * **Session tracking**: Automatic session start and end events with timing.
  * **Network requests**: HTTP request/response data when enabled.
  * **Taps**: A `click` span for each tap interaction.
  * **Screen views**: A `screen_view` span when a `UIViewController` appears, and a session replay `Navigate` event on each screen change. To learn more, read [Track screen views](#track-screen-views).
  * **LaunchDarkly SDK events**: Feature flag evaluations and SDK operations.

  ## Session Replay

  <Note>
    **Session Replay is in Early Access**

    Session Replay is available in Early Access. APIs are subject to change until a 1.x version is released.
  </Note>

  Session Replay captures user interactions and screen recordings to help you understand how users interact with your application. Session Replay works as an additional plugin that requires the observability plugin to be configured first.

  ### Install the Session Replay plugin

  First, add the Session Replay package as a dependency alongside the observability plugin:

  <CodeGroup>
    ```swift title="Package.swift, using Swift Package Manager" lines wrap theme={null}
    //...
        dependencies: [
            .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor("9.0.0")),
            .package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability.git", .upToNextMinor("1.0.0")),
        ],
        targets: [
            .target(
                name: "YOUR_TARGET",
                dependencies: [
                    "LaunchDarkly",
                    .product(name: "LaunchDarklySessionReplay", package: "swift-launchdarkly-observability")
                ]
            )
        ],
    //...
    ```

    ```ruby title="Podfile, using CocoaPods" lines wrap theme={null}
    use_frameworks!
    target 'YourTargetName' do
      pod 'LaunchDarkly', '~> 9.0'
      pod 'LaunchDarklyObservability', '~> 1.0'
      pod 'LaunchDarklySessionReplay', '~> 1.0'
    end
    ```
  </CodeGroup>

  Then, import the Session Replay plugin into your code:

  <CodeGroup>
    ```swift title="Swift" lines wrap theme={null}
    import LaunchDarkly
    import LaunchDarklyObservability
    import LaunchDarklySessionReplay
    ```
  </CodeGroup>

  ### Initialize Session Replay

  To enable Session Replay, add the `SessionReplay` plugin to your SDK configuration alongside the `Observability` plugin. The `Observability` plugin must be added before the `SessionReplay` plugin:

  <CodeGroup>
    ```swift title="iOS SDK v9.14+ with Session Replay" expandable lines wrap theme={null}
    let mobileKey = "example-mobile-key"
    let config = LDConfig(
        mobileKey: mobileKey,
        autoEnvAttributes: .enabled
    )
    config.plugins = [
        // Observability plugin must be added before SessionReplay
        Observability(options: .init(
            serviceName: "ios-app",
            sessionBackgroundTimeout: 3)),
        SessionReplay(options: .init(
            isEnabled: true,
            privacy: .init(
                maskTextInputs: true,
                maskWebViews: false,
                maskImages: false,
                maskAccessibilityIdentifiers: ["email-field", "password-field"]
            )
        ))
    ]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(
        config: config,
        context: context,
        startWaitSeconds: 5.0,
        completion: { (timedOut: Bool) -> Void in
            if timedOut {
                // Client may not have the most recent flags for the configured context
            } else {
                // Client has received flags for the configured context
            }
        }
    )
    ```
  </CodeGroup>

  ### 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 `isEnabled` to `false` in `SessionReplayOptions`, then call `LDReplay.shared.start()` when you're ready to begin recording.

  First, configure the plugin with `isEnabled: false`:

  <CodeGroup>
    ```swift title="Manual start configuration, iOS SDK v9.14+" expandable lines wrap theme={null}
    let config = LDConfig(
        mobileKey: "example-mobile-key",
        autoEnvAttributes: .enabled
    )
    config.plugins = [
        Observability(),
        SessionReplay(options: .init(
            isEnabled: false, // Don't start recording automatically
            privacy: .init(
                maskTextInputs: true
            )
        ))
    ]

    let contextBuilder = LDContextBuilder(key: "example-context-key")
    guard case .success(let context) = contextBuilder.build()
    else { return }

    LDClient.start(config: config, context: context, startWaitSeconds: 5)
    ```
  </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>
    ```swift title="Start recording" lines wrap theme={null}
    // Start recording after user consent or feature flag check
    LDReplay.shared.start()
    ```

    ```swift title="Start recording with feature flag" lines wrap theme={null}
    // Start recording based on a feature flag
    let replayEnabled = LDClient.get()?.boolVariation(forKey: "enable-session-replay", defaultValue: false) ?? false
    if replayEnabled {
        LDReplay.shared.start()
    }
    ```

    ```swift title="Stop recording" lines wrap theme={null}
    LDReplay.shared.stop()
    ```
  </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

  ### Configure Session Replay privacy options

  The Session Replay plugin provides privacy options to control what data is captured. Configure these options when initializing the plugin:

  <CodeGroup>
    ```swift title="Session Replay privacy options" lines wrap theme={null}
    SessionReplay(options: .init(
        isEnabled: true,
        serviceName: "my-swift-app",
        privacy: .init(
            maskTextInputs: true,
            maskWebViews: false,
            maskLabels: false,
            maskImages: false,
            maskUIViews: [SensitiveView.self],
            ignoreUIViews: [PublicView.self],
            maskAccessibilityIdentifiers: ["email-field", "password-field"],
            ignoreAccessibilityIdentifiers: ["public-label"],
            minimumAlpha: 0.02
        )
    ))
    ```
  </CodeGroup>

  #### Privacy configuration options

  The `PrivacyOptions` struct provides the following parameters:

  * **maskTextInputs**: Mask all text input fields. Defaults to `true`.
  * **maskWebViews**: Mask the contents of web views (`WKWebView` and `UIWebView`). When this setting is enabled, web views are rendered as blank rectangles in session replays. Defaults to `false`.
  * **maskLabels**: Mask all text labels. Defaults to `false`.
  * **maskImages**: Mask all images. Defaults to `false`.
  * **maskUIViews**: Array of `UIView` classes to automatically mask in recordings.
  * **ignoreUIViews**: Array of `UIView` classes to exclude from masking rules.
  * **maskAccessibilityIdentifiers**: Array of accessibility identifiers to mask. Use this to mask specific UI elements by their accessibility identifier.
  * **ignoreAccessibilityIdentifiers**: Array of accessibility identifiers to exclude from masking rules.
  * **minimumAlpha**: Minimum alpha value for view visibility in recordings. Views with alpha below this threshold are not captured. Defaults to `0.02`.

  ### Fine-grained masking control

  You can override the default privacy settings on individual views. Use `.ldMask()` to mask a view, `.ldUnmask()` to exempt one from masking, and `.ldPrivate(isEnabled:)` to mask a view conditionally. These give you precise control over what session replay captures.

  #### SwiftUI view masking

  Use view modifiers to control masking for SwiftUI views:

  <CodeGroup>
    ```swift title="SwiftUI masking control" expandable lines wrap theme={null}
    import SwiftUI
    import LaunchDarklySessionReplay

    struct ContentView: View {
        @State private var email = ""
        @State private var shouldMaskEmail = true

        var body: some View {
            VStack {
                // Mask this specific view
                Text("Sensitive information")
                    .ldMask()

                // Unmask this view (even if it would be masked by default)
                Image("profile-photo")
                    .ldUnmask()

                // Conditionally mask based on a flag
                TextField("Email", text: $email)
                    .ldPrivate(isEnabled: shouldMaskEmail)
            }
        }
    }
    ```
  </CodeGroup>

  #### UIKit view masking

  Use the `.ldMask()`, `.ldUnmask()`, and `.ldPrivate(isEnabled:)` methods on `UIView` instances:

  <CodeGroup>
    ```swift title="UIKit masking control" expandable lines wrap theme={null}
    import UIKit
    import LaunchDarklySessionReplay

    class CreditCardViewController: UIViewController {
        let cvvField = UITextField()
        let nameField = UITextField()
        let cardNumberField = UITextField()

        override func viewDidLoad() {
            super.viewDidLoad()

            // Mask the CVV field
            cvvField.ldMask()

            // Unmask the name field (even if text inputs are masked by default)
            nameField.ldUnmask()

            // Conditionally mask based on a flag
            cardNumberField.ldPrivate(isEnabled: true)
        }
    }
    ```
  </CodeGroup>

  #### 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 `maskAccessibilityIdentifiers` 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()`, the SDK does not mask the view.
  3. **Global configuration**: If a global privacy option such as `maskTextInputs` or `maskImages` applies to the view, the SDK follows that option.

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

  ## 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#ios)
  * [Logs](/docs/sdk/features/observability-logs#ios)
  * [Metrics](/docs/sdk/features/observability-metrics#ios)
  * [Symbolication](/docs/sdk/features/observability-symbolication#ios)
  * [Tracing](/docs/sdk/features/observability-traces#ios)

  ## Review observability data in LaunchDarkly

  After you initialize the SDK and observability plugin, your application automatically starts sending observability data back to LaunchDarkly in the form of custom events and OpenTelemetry data. 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).
</View>
