LaunchDarkly Observability SDK for Swift
NB: APIs are subject to change until a 1.x version is released.
The iOS observability plugin automatically instruments:
- Activity Lifecycle: App lifecycle events and transitions
- HTTP Requests: URLSession requests
- Crash Reporting: Automatic crash reporting
- Feature Flag Evaluations: Evaluation events added to your spans.
- Session Management: User session tracking and background timeout handling
A complete example application is available in the swift-launchdarkly-observability/ExampleApp directory.
NOTE: since APIs are subject to change until a 1.x version is released, pointing to main branch is a temporal workaround to test/use the package
Add the Swift Package dependency in Xcode or add it to your Package.swift:
.package(url: "https://github.com/launchdarkly/swift-launchdarkly-observability", branch: "main"),Add the pods to your Podfile:
pod 'LaunchDarklyObservability'
pod 'LaunchDarklySessionReplay' # optional, only if using Session ReplaySome transitive dependencies (e.g. LDSwiftEventSource) still declare an iOS 11.0 deployment target, which is below the minimum required by recent Xcode SDKs. Add the following post_install hook to your Podfile to raise their deployment target automatically:
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 13.0
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
end
endAdd the observability plugin to your LaunchDarkly iOS Client SDK configuration:
import UIKit
import LaunchDarkly
import LaunchDarklyObservability
let mobileKey = "your-mobile-key"
let config = { () -> LDConfig in
var config = LDConfig(
mobileKey: mobileKey,
autoEnvAttributes: .enabled
)
config.plugins = [
Observability(options: .init(sessionBackgroundTimeout: 3))
]
return config
}()
let context = { () -> LDContext in
var contextBuilder = LDContextBuilder(
key: "12345"
)
contextBuilder.kind("user")
do {
return try contextBuilder.build().get()
} catch {
abort()
}
}()
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
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
}
}
)
return true
}
}Session Replay captures user interactions and screen recordings to help you understand how users interact with your application. To enable Session Replay, add the SessionReplay plugin alongside the Observability plugin:
import UIKit
import LaunchDarkly
import LaunchDarklyObservability
import LaunchDarklySessionReplay
let mobileKey = "your-mobile-key"
let config = { () -> LDConfig in
var 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,
sampleRate: 1.0,
privacy: .init(
maskTextInputs: true,
maskWebViews: false,
maskImages: false,
maskAccessibilityIdentifiers: ["email-field", "password-field"]
)
))
]
return config
}()
let context = { () -> LDContext in
var contextBuilder = LDContextBuilder(key: "12345")
contextBuilder.kind("user")
do {
return try contextBuilder.build().get()
} catch {
abort()
}
}()
final class AppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
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
}
}
)
return true
}
}By default, Session Replay attempts to start recording as soon as the SDK is initialized if isEnabled is set to true. The sampleRate option controls whether that attempt actually starts recording. Use a value from 0.0 to 1.0, where 0.0 never records and 1.0 always records.
SessionReplay(options: .init(
isEnabled: true,
sampleRate: 0.25,
// ... other options
))If you want to initialize Session Replay without activating recording immediately (e.g., to wait for user consent or a specific event), set isEnabled to false in the options:
SessionReplay(options: .init(
isEnabled: false,
// ... other options
))You can then attempt to activate recording later by setting LDReplay.shared.isEnabled to true. This still applies sampling.
// From a SwiftUI View or @MainActor isolated class
LDReplay.shared.isEnabled = trueTo inspect the outcome, use start() and check the returned SessionReplayStartResult, or read LDReplay.shared.isRunning to see whether Session Replay is actually recording:
let result = LDReplay.shared.start()
switch result {
case .started, .alreadyStarted:
// Session Replay is running.
case .sampledOut:
// Session Replay is enabled, but this session was not selected by sampleRate.
case .unavailable:
// Session Replay has not been registered.
}
let isRecording = LDReplay.shared.isRunningFor debugging, you can bypass sampling for a manual start:
LDReplay.shared.start(ignoreSampling: true)Configure privacy settings to control what data is captured:
- maskTextInputs: Mask all text input fields (default:
true) - maskWebViews: Mask contents of Web Views (default:
false) - maskLabels: Mask all text labels (default:
false) - maskImages: Mask all images (default:
false) - maskAccessibilityIdentifiers: Array of accessibility identifiers to mask
- maskUIViews: Array of UIView classes to mask
- minimumAlpha: Minimum alpha value for view visibility (default:
0.02)
You can override the default privacy settings on individual views using modifiers:
SwiftUI Views:
import SwiftUI
import LaunchDarklySessionReplay
struct ContentView: View {
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)
}
}
}UIKit Views:
import UIKit
import LaunchDarklySessionReplay
class CreditCardViewController: UIViewController {
let cvvField = UITextField()
let nameField = UITextField()
override func viewDidLoad() {
super.viewDidLoad()
// Mask the CVV container
cvvField.ldMask()
// Unmask the name field (even if text inputs are masked by default)
nameField.ldUnmask()
// Conditionally mask based on a flag
cvvField.ldPrivate(isEnabled: true)
}
}When deciding whether a specific view should be masked in a Session Replay, the SDK evaluates rules in a strict order of precedence. It checks these conditions from top to bottom and stops at the first one that applies:
- Explicit Masking (Highest Priority): Is the view, or any of its parent views, explicitly masked (e.g., using
.ldMask()or matchingmaskAccessibilityIdentifiers)?- Yes: The view is masked. This overrides all other rules.
- Explicit Unmasking: Is the view, or any of its parent views, explicitly unmasked (e.g., using
.ldUnmask())?- Yes: The view is unmasked.
- Global Configuration: Does your global privacy configuration (like
maskTextInputs,maskImages, etc.) apply to this view?- Yes: The view follows the global configuration.
Note: If multiple rules conflict at the same level, masking wins over unmasking.
You can customize the observability plugin with various options:
import UIKit
import LaunchDarkly
import LaunchDarklyObservability
let config = { () -> LDConfig in
var config = LDConfig(
mobileKey: mobileKey,
autoEnvAttributes: .enabled
)
config.plugins = [
Observability(
options: .init(
serviceName: "ios-app",
serviceVersion: "0.1.0",
resourceAttributes: [
"environment": .string("production"),
"team": .string("mobile")
],
customHeaders: [
("X-Custom-Header", "custom-value")
],
sessionBackgroundTimeout: 60,
isDebug: true
)
)
]
return config
}()After initialization of the LaunchDarkly iOS Client SDK, use LDObserve to record metrics, logs, errors, and traces:
import LaunchDarklyObservability
import OpenTelemetryApi
// Record metrics
LDObserve.shared.recordMetric(metric: .init(name: "user_actions", value: 1.0))
LDObserve.shared.recordCount(metric: .init(name: "api_calls", value: 1.0))
LDObserve.shared.recordIncr(metric: .init(name: "page_views", value: 1.0))
LDObserve.shared.recordHistogram(metric: .init(name: "response_time", value: 150.0))
LDObserve.shared.recordUpDownCounter(metric: .init(name: "active_connections", value: 1.0))
// Record logs
LDObserve.shared.recordLog(
message: "User performed action",
severity: .info,
attributes: [
"user_id": .string("12345"),
"action": .string("button_click")
]
)
// Record errors
LDObserve.shared.recordError(
error: paymentError,
attributes: [
"component": .string("payment"),
"error_code": .string("PAYMENT_FAILED")
]
)
// Create spans for tracing
let span = LDObserve.shared.startSpan(
name: "api_request",
attributes: [
"endpoint": .string("/api/users"),
"method": .string("GET")
]
)
span.end()We encourage pull requests and other contributions from the community. Check out our contributing guidelines for instructions on how to contribute to this SDK.
- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
- Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
- Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
- Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
- Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read our documentation for a complete list.
- Explore LaunchDarkly
- launchdarkly.com for more information
- docs.launchdarkly.com for our documentation and SDK reference guides
- apidocs.launchdarkly.com for our API documentation
- launchdarkly.com/blog for the latest product updates