diff --git a/sdk/@launchdarkly/mobile-dotnet/README.md b/sdk/@launchdarkly/mobile-dotnet/README.md index 39997fe6a8..6daf1819c9 100644 --- a/sdk/@launchdarkly/mobile-dotnet/README.md +++ b/sdk/@launchdarkly/mobile-dotnet/README.md @@ -132,6 +132,34 @@ public static class MauiProgram } ``` +### Observability Options + +`ObservabilityOptions` accepts the following settings, all optional: + +| Option | Default | Description | +|---|---|---| +| `IsEnabled` | `true` | Whether observability telemetry is recorded. | +| `ServiceName` | `"observability-maui"` | The `service.name` resource attribute. Set it to identify your app. | +| `ServiceVersion` | Host app version | The `service.version` resource attribute. When not set, the app's version (`AppInfo.Current.VersionString`) is reported. | +| `OtlpEndpoint` | LaunchDarkly collector | OTLP/HTTP endpoint telemetry is exported to. Override it to send data through a proxy. | +| `BackendUrl` | LaunchDarkly backend | Backend used for session and replay metadata. | +| `ContextFriendlyName` | `null` | A human-readable name for the current context, shown instead of its key. | +| `Attributes` | `null` | Extra resource attributes attached to every exported signal, such as `deployment.environment`. | +| `CustomHeaders` | `null` | Extra HTTP headers added to OTLP exports, for example for a proxy or authentication. | +| `LogsApiLevel` | `ObservabilityLogLevel.Info` | Minimum severity of exported logs. `ObservabilityLogLevel.None` disables log export. | +| `Instrumentation` | All enabled | `NetworkRequests` and `LaunchTimes` toggles. | + +```csharp +var observabilityOptions = new ObservabilityOptions( + serviceName: "my-maui-app", + serviceVersion: "2.4.0", + customHeaders: new Dictionary { { "X-Proxy-Token", "..." } }, + logsApiLevel: ObservabilityLogLevel.Warn +); +``` + +After initialization, `LDObserve.NativeVersion` returns the version of the underlying native observability SDK, which is useful in diagnostics and support requests. + ### Recording Observability Data After initialization of the LaunchDarkly client, use `LDObserve` to record metrics, logs, and errors: @@ -398,6 +426,25 @@ public static class MauiProgram The same third argument works with the standalone variant: `LDObserve.Init(mobileKey, observabilityOptions, sessionReplayOptions)`. +### Capture Options + +`SessionReplayOptions` controls how often and at what quality frames are captured: + +* `SampleRate`: (Default: `1.0`) Probability from `0.0` to `1.0` that a session is recorded when replay is enabled. `0.25` records about a quarter of sessions. +* `FrameRate`: (Default: `1.0`) Target capture rate in frames per second. +* `Scale`: (Default: `1.0`) Capture resolution: `1.0` is 1x (160 DPI), `2.0` is 2x, and so on. Higher values capture more detail but produce larger frames. +* `ImageQuality`: (Default: `0.3`) JPEG quality of exported frames, from `0.0` (smallest) to `1.0` (best). + +```csharp +var replayOptions = new SessionReplayOptions( + isEnabled: true, + sampleRate: 0.25, + frameRate: 2.0, + scale: 1.5, + imageQuality: 0.5 +); +``` + ### Privacy Options You can control what information is captured during a session using `PrivacyOptions`: @@ -406,10 +453,11 @@ You can control what information is captured during a session using `PrivacyOpti * `MaskWebViews`: (Default: `false`) Masks all web view content. * `MaskLabels`: (Default: `false`) Masks all text labels. * `MaskImages`: (Default: `false`) Masks all images. +* `MinimumAlpha`: (Default: `0.02`) Opacity below which a view is treated as invisible and not masked. ### Manual Masking -You can manually mask or unmask specific UI components using the provided extension methods on any MAUI `View`. +You can manually mask, unmask, or ignore specific UI components using the provided extension methods on any MAUI `View`. Call them once the view has been attached to the screen (its `Handler` is set), for example from `OnHandlerChanged` or a page's `OnAppearing`; before that they have no effect. ```csharp using LaunchDarkly.SessionReplay; @@ -419,6 +467,9 @@ mySensitiveView.LDMask(); // Unmask a specific view myPublicView.LDUnmask(); + +// Leave a view out of recordings (masked instead on Android) +myVideoView.LDIgnore(); ``` ## Contributing diff --git a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/ObservabilityBridge.kt b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/ObservabilityBridge.kt index 08190aca91..ea37acc3e3 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/ObservabilityBridge.kt +++ b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/ObservabilityBridge.kt @@ -79,6 +79,9 @@ public class ObservabilityBridge( debug = false, otlpEndpoint = observability.otlpEndpoint, backendUrl = observability.backendUrl, + contextFriendlyName = observability.contextFriendlyName, + customHeaders = observability.customHeaders ?: emptyMap(), + logsApiLevel = toLogLevel(observability.logsApiLevel), tracesApi = com.launchdarkly.observability.api.ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = true), metricsApi = com.launchdarkly.observability.api.ObservabilityOptions.MetricsApi.enabled(), // Screen views are reported from the MAUI layer, so the Android-side span is left off @@ -101,8 +104,13 @@ public class ObservabilityBridge( maskTextInputs = privacy.maskTextInputs, maskText = privacy.maskLabels, maskImageViews = privacy.maskImages, - maskWebViews = privacy.maskWebViews - ) + maskWebViews = privacy.maskWebViews, + minimumAlpha = privacy.minimumAlpha.toFloat() + ), + sampleRate = replay.sampleRate, + frameRate = replay.frameRate, + scale = replay.scale, + imageQuality = replay.imageQuality ) } catch (t: Throwable) { printException("LD:ObservabilityBridge failed to build ReplayOptions", t) @@ -141,6 +149,11 @@ public class ObservabilityBridge( private const val MAUI_DISTRO_NAME = "observability-maui-android" } + private fun toLogLevel(severity: Int): com.launchdarkly.observability.api.ObservabilityOptions.LogLevel = + com.launchdarkly.observability.api.ObservabilityOptions.LogLevel.values() + .firstOrNull { it.level == severity } + ?: com.launchdarkly.observability.api.ObservabilityOptions.LogLevel.INFO + private fun printException(prefix: String, t: Throwable) { logger.error("$prefix ${t::class.java.name}: ${t.message}") val writer = java.io.StringWriter() diff --git a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/OptionsBridge.kt b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/OptionsBridge.kt index d8af6ee65d..f2e2c48e1c 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/OptionsBridge.kt +++ b/sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/OptionsBridge.kt @@ -9,6 +9,9 @@ public class LDObservabilityOptions { @JvmField var contextFriendlyName: String? = null @JvmField var attributes: HashMap? = null @JvmField var launchTime: Boolean = true + @JvmField var customHeaders: HashMap? = null + // OpenTelemetry severity number; Int.MAX_VALUE disables log export. + @JvmField var logsApiLevel: Int = 9 constructor() @@ -19,8 +22,10 @@ public class LDObservabilityOptions { otlpEndpoint: String, backendUrl: String, contextFriendlyName: String?, - attributes: HashMap? = null, - launchTime: Boolean = true + attributes: HashMap?, + launchTime: Boolean, + customHeaders: HashMap?, + logsApiLevel: Int ) { this.isEnabled = isEnabled this.serviceName = serviceName @@ -30,6 +35,8 @@ public class LDObservabilityOptions { this.contextFriendlyName = contextFriendlyName this.attributes = attributes this.launchTime = launchTime + this.customHeaders = customHeaders + this.logsApiLevel = logsApiLevel } } @@ -61,16 +68,28 @@ public class LDSessionReplayOptions { @JvmField var isEnabled: Boolean = true @JvmField var serviceName: String = "" @JvmField var privacy: LDPrivacyOptions = LDPrivacyOptions() + @JvmField var sampleRate: Double = 1.0 + @JvmField var frameRate: Double = 1.0 + @JvmField var scale: Double = 1.0 + @JvmField var imageQuality: Double = 0.3 constructor() constructor( isEnabled: Boolean, serviceName: String, - privacy: LDPrivacyOptions + privacy: LDPrivacyOptions, + sampleRate: Double, + frameRate: Double, + scale: Double, + imageQuality: Double ) { this.isEnabled = isEnabled this.serviceName = serviceName this.privacy = privacy + this.sampleRate = sampleRate + this.frameRate = frameRate + this.scale = scale + this.imageQuality = imageQuality } } diff --git a/sdk/@launchdarkly/mobile-dotnet/macios/LDObserve.MaciOS.Binding/ApiDefinition.cs b/sdk/@launchdarkly/mobile-dotnet/macios/LDObserve.MaciOS.Binding/ApiDefinition.cs index af6b5721be..e12dbf8630 100644 --- a/sdk/@launchdarkly/mobile-dotnet/macios/LDObserve.MaciOS.Binding/ApiDefinition.cs +++ b/sdk/@launchdarkly/mobile-dotnet/macios/LDObserve.MaciOS.Binding/ApiDefinition.cs @@ -36,6 +36,12 @@ interface ObjcObservabilityOptions [Export("launchTimes")] bool LaunchTimes { get; set; } + + [NullAllowed, Export("customHeaders")] + NSDictionary CustomHeaders { get; set; } + + [Export("logsApiLevel")] + nint LogsApiLevel { get; set; } } [BaseType(typeof(NSObject))] @@ -55,6 +61,21 @@ interface ObjcSessionReplayOptions [Export("maskImages")] bool MaskImages { get; set; } + + [Export("minimumAlpha")] + double MinimumAlpha { get; set; } + + [Export("sampleRate")] + double SampleRate { get; set; } + + [Export("frameRate")] + double FrameRate { get; set; } + + [Export("scale")] + double Scale { get; set; } + + [Export("imageQuality")] + double ImageQuality { get; set; } } [BaseType(typeof(NSObject))] diff --git a/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/ObservabilityBridge.swift b/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/ObservabilityBridge.swift index 0d1f7b741d..074c30dae8 100644 --- a/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/ObservabilityBridge.swift +++ b/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/ObservabilityBridge.swift @@ -19,6 +19,17 @@ internal func buildResourceAttributes(_ source: NSDictionary?) -> [String: Attri return result } +internal func buildCustomHeaders(_ source: NSDictionary?) -> [String: String] { + guard let source, source.count > 0 else { + return [:] + } + var result = [String: String](minimumCapacity: source.count) + for (key, value) in source { + result[String(describing: key)] = (value as? String) ?? String(describing: value) + } + return result +} + @objc(ObservabilityBridge) public final class ObservabilityBridge: NSObject { @@ -47,6 +58,8 @@ public final class ObservabilityBridge: NSObject { otlpEndpoint: observability.otlpEndpoint, backendUrl: observability.backendUrl, resourceAttributes: buildResourceAttributes(observability.attributes), + customHeaders: buildCustomHeaders(observability.customHeaders), + logsApiLevel: .init(rawValue: observability.logsApiLevel) ?? .none, crashReporting: .init(source: .none), instrumentation: .init( urlSession: .disabled, // Network tracing happens on the .NET side via System.Net.Http activities. @@ -66,12 +79,17 @@ public final class ObservabilityBridge: NSObject { observabilityPlugin, SessionReplay(options: .init( isEnabled: replay.isEnabled, + sampleRate: replay.sampleRate, privacy: .init( maskTextInputs: replay.maskTextInputs, maskWebViews: replay.maskWebViews, maskLabels: replay.maskLabels, - maskImages: replay.maskImages - ) + maskImages: replay.maskImages, + minimumAlpha: CGFloat(replay.minimumAlpha) + ), + frameRate: replay.frameRate, + scale: CGFloat(replay.scale), + imageQuality: CGFloat(replay.imageQuality) )) ] diff --git a/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/OptionsBridge.swift b/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/OptionsBridge.swift index 6001e46403..2533da6e8f 100644 --- a/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/OptionsBridge.swift +++ b/sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/OptionsBridge.swift @@ -10,6 +10,9 @@ public final class ObjcObservabilityOptions: NSObject { // Not consumed yet — network tracing is handled on the .NET side. Kept for future iOS-native URL session instrumentation. @objc public var networkRequests: Bool = true @objc public var launchTimes: Bool = true + @objc public var customHeaders: NSDictionary? + /// OpenTelemetry severity number; values above `fatal4` (24) disable log export. + @objc public var logsApiLevel: Int = 9 @objc public override init() { super.init() @@ -23,6 +26,11 @@ public final class ObjcSessionReplayOptions: NSObject { @objc public var maskWebViews: Bool = false @objc public var maskLabels: Bool = false @objc public var maskImages: Bool = false + @objc public var minimumAlpha: Double = 0.02 + @objc public var sampleRate: Double = 1.0 + @objc public var frameRate: Double = 1.0 + @objc public var scale: Double = 1.0 + @objc public var imageQuality: Double = 0.3 @objc public override init() { super.init() diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props b/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props index ac83e36c3d..03ce88ebde 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props +++ b/sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props @@ -5,7 +5,7 @@ LaunchDarkly.SessionReplay - 0.11.3 + 0.14.1 LaunchDarkly LaunchDarkly LaunchDarkly diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.Android.cs b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.Android.cs index 46f58a99f4..9e47e57fa1 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.Android.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.Android.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using LaunchDarkly.SessionReplay; #if ANDROID @@ -10,12 +11,14 @@ internal static class LDNativeAndroidMapping return new global::LDObserveAndroid.LDObservabilityOptions( options.IsEnabled, options.ServiceName, - options.ServiceVersion, + options.ResolvedServiceVersion, options.OtlpEndpoint, options.BackendUrl, options.ContextFriendlyName, DictionaryTypeConverters.ToJavaDictionary(options.Attributes), - options.Instrumentation.LaunchTimes + options.Instrumentation.LaunchTimes, + options.CustomHeaders is null ? null : new Dictionary(options.CustomHeaders), + (int)options.LogsApiLevel ); } @@ -32,7 +35,11 @@ internal static class LDNativeAndroidMapping return new global::LDObserveAndroid.LDSessionReplayOptions( options.IsEnabled, options.ServiceName, - nativePrivacy + nativePrivacy, + options.SampleRate, + options.FrameRate, + options.Scale, + options.ImageQuality ); } } diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.cs b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.cs index eac87505f9..5f80e62d8b 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.cs @@ -19,6 +19,8 @@ internal class LDNative public SessionReplayOptions Replay { get; set; } public String NativeVersion { get; set; } = string.Empty; + internal static string CurrentNativeVersion { get; private set; } = string.Empty; + private LDNative(ObservabilityOptions observability, SessionReplayOptions replay) { Observability = observability; @@ -57,6 +59,8 @@ internal static LDNative Start(string mobileKey, ObservabilityOptions observabil bridge.Start(mobileKey, observability, replay, observabilityVersion); #endif + CurrentNativeVersion = ldNative.NativeVersion; + return ldNative; } } diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/SRClient.iOS.cs b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/SRClient.iOS.cs index db63581c15..68390e21db 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/bridge/SRClient.iOS.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/bridge/SRClient.iOS.cs @@ -1,4 +1,5 @@ #if IOS +using System.Linq; using Foundation; using LDObserveMaciOS; using LaunchDarkly.SessionReplay; @@ -23,12 +24,18 @@ public void Start(string mobileKey, ObservabilityOptions observability, SessionR var objcObs = new ObjcObservabilityOptions { ServiceName = observability.ServiceName ?? "observability-maui", - ServiceVersion = observability.ServiceVersion ?? "0.1.0", + ServiceVersion = observability.ResolvedServiceVersion, OtlpEndpoint = observability.OtlpEndpoint ?? "https://otel.observability.app.launchdarkly.com:4318", BackendUrl = observability.BackendUrl ?? "https://pub.observability.app.launchdarkly.com", Attributes = DictionaryTypeConverters.ToNSDictionary(observability.Attributes), NetworkRequests = observability.Instrumentation.NetworkRequests, - LaunchTimes = observability.Instrumentation.LaunchTimes + LaunchTimes = observability.Instrumentation.LaunchTimes, + CustomHeaders = observability.CustomHeaders is null + ? null + : NSDictionary.FromObjectsAndKeys( + observability.CustomHeaders.Values.Select(v => (NSObject)new NSString(v)).ToArray(), + observability.CustomHeaders.Keys.Select(k => (NSObject)new NSString(k)).ToArray()), + LogsApiLevel = (nint)(int)observability.LogsApiLevel }; var objcReplay = new ObjcSessionReplayOptions @@ -37,7 +44,12 @@ public void Start(string mobileKey, ObservabilityOptions observability, SessionR MaskTextInputs = replay.Privacy?.MaskTextInputs ?? true, MaskWebViews = replay.Privacy?.MaskWebViews ?? false, MaskLabels = replay.Privacy?.MaskLabels ?? false, - MaskImages = replay.Privacy?.MaskImages ?? false + MaskImages = replay.Privacy?.MaskImages ?? false, + MinimumAlpha = replay.Privacy?.MinimumAlpha ?? 0.02, + SampleRate = replay.SampleRate, + FrameRate = replay.FrameRate, + Scale = replay.Scale, + ImageQuality = replay.ImageQuality }; _native.Start(mobileKey, objcObs, objcReplay, observabilityVersion); diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/observe/api/LDObserve.cs b/sdk/@launchdarkly/mobile-dotnet/observability/observe/api/LDObserve.cs index f5ebb428f2..f6daf97cf2 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/observe/api/LDObserve.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/observe/api/LDObserve.cs @@ -50,6 +50,12 @@ public static void Init(string mobileKey, ObservabilityOptions observability, Se public static void Init(LdClient client, ObservabilityOptions observability, SessionReplayOptions? replay = null) => client.RegisterPlugin(new ObservabilityPlugin(observability, replay)); + /// + /// The native observability SDK version reported during startup, or an empty string + /// before initialization. + /// + public static string NativeVersion => LDNative.CurrentNativeVersion; + // -------- Public API -------- /// diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/observe/plugin/ObservabilityOptions.cs b/sdk/@launchdarkly/mobile-dotnet/observability/observe/plugin/ObservabilityOptions.cs index e2827f09a3..6c14f3784f 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/observe/plugin/ObservabilityOptions.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/observe/plugin/ObservabilityOptions.cs @@ -1,22 +1,43 @@ using System; using System.Collections.Generic; +using Microsoft.Maui.ApplicationModel; namespace LaunchDarkly.Observability; public class ObservabilityOptions { public const string DefaultServiceName = "observability-maui"; + [Obsolete("ServiceVersion now defaults to the host app version (AppInfo.Current.VersionString). This constant is no longer used.")] public const string DefaultServiceVersion = "0.1.0"; public const string DefaultOtlpEndpoint = "https://otel.observability.app.launchdarkly.com:4318"; public const string DefaultBackendUrl = "https://pub.observability.app.launchdarkly.com"; public bool IsEnabled { get; set; } = true; public string ServiceName { get; set; } = DefaultServiceName; - public string ServiceVersion { get; set; } = DefaultServiceVersion; + + /// + /// The service.version resource attribute, such as a release version or Git SHA. + /// When null (the default), the host app's version + /// () is reported. + /// + public string? ServiceVersion { get; set; } + public string OtlpEndpoint { get; set; } = DefaultOtlpEndpoint; public string BackendUrl { get; set; } = DefaultBackendUrl; public string? ContextFriendlyName { get; set; } public IDictionary? Attributes { get; set; } + + /// + /// Extra HTTP headers added to OTLP exports (e.g. for proxies or auth). + /// + public IDictionary? CustomHeaders { get; set; } + + /// + /// Minimum severity of logs exported. Use + /// to disable log export. Defaults to . + /// + public ObservabilityLogLevel LogsApiLevel { get; set; } = ObservabilityLogLevel.Info; + public InstrumentationOptions Instrumentation { get; set; } = new(); public ObservabilityOptions() { } @@ -24,12 +45,14 @@ public ObservabilityOptions() { } public ObservabilityOptions( bool isEnabled = true, string serviceName = DefaultServiceName, - string serviceVersion = DefaultServiceVersion, + string? serviceVersion = null, string? otlpEndpoint = null, string? backendUrl = null, string? contextFriendlyName = null, IDictionary? attributes = null, - InstrumentationOptions? instrumentation = null) + InstrumentationOptions? instrumentation = null, + IDictionary? customHeaders = null, + ObservabilityLogLevel logsApiLevel = ObservabilityLogLevel.Info) { IsEnabled = isEnabled; ServiceName = serviceName; @@ -39,9 +62,62 @@ public ObservabilityOptions( ContextFriendlyName = contextFriendlyName; Attributes = attributes; Instrumentation = instrumentation ?? new InstrumentationOptions(); + CustomHeaders = customHeaders; + LogsApiLevel = logsApiLevel; + } + + internal string ResolvedServiceVersion + { + get + { + if (!string.IsNullOrEmpty(ServiceVersion)) + return ServiceVersion; + try + { + return AppInfo.Current.VersionString ?? string.Empty; + } + catch (Exception) + { + return string.Empty; + } + } } } +/// +/// Severity threshold for exported logs. Values match the OpenTelemetry log severity numbers. +/// +public enum ObservabilityLogLevel +{ + Trace = 1, + Trace2 = 2, + Trace3 = 3, + Trace4 = 4, + Debug = 5, + Debug2 = 6, + Debug3 = 7, + Debug4 = 8, + Info = 9, + Info2 = 10, + Info3 = 11, + Info4 = 12, + Warn = 13, + Warn2 = 14, + Warn3 = 15, + Warn4 = 16, + Error = 17, + Error2 = 18, + Error3 = 19, + Error4 = 20, + Fatal = 21, + Fatal2 = 22, + Fatal3 = 23, + Fatal4 = 24, + + /// Disables log export entirely. + None = int.MaxValue, +} + public class InstrumentationOptions { public bool NetworkRequests { get; set; } = true; diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/replay/api/LDViewExtensions.cs b/sdk/@launchdarkly/mobile-dotnet/observability/replay/api/LDViewExtensions.cs index e75b253cc0..19abdf72fa 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/replay/api/LDViewExtensions.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/replay/api/LDViewExtensions.cs @@ -32,4 +32,19 @@ public static void LDUnmask(this View view) LDMasking.Unmask(nativeView); #endif } + + /// + /// Leaves the view out of recordings. On Android, where the native SDK has no per-view + /// ignore, the view is masked instead. + /// + public static void LDIgnore(this View view) + { + #if IOS + if (view?.Handler?.PlatformView is UIView uiView) + LDMasking.Ignore(uiView); + #elif ANDROID + if (view?.Handler?.PlatformView is Android.Views.View nativeView) + LDMasking.Mask(nativeView); + #endif + } } diff --git a/sdk/@launchdarkly/mobile-dotnet/observability/replay/plugin/SessionReplayOptions.cs b/sdk/@launchdarkly/mobile-dotnet/observability/replay/plugin/SessionReplayOptions.cs index 3862d4a835..6fc67f430e 100644 --- a/sdk/@launchdarkly/mobile-dotnet/observability/replay/plugin/SessionReplayOptions.cs +++ b/sdk/@launchdarkly/mobile-dotnet/observability/replay/plugin/SessionReplayOptions.cs @@ -35,6 +35,27 @@ public PrivacyOptions( public string ServiceName { get; set; } = "sessionreplay-dotnet"; public PrivacyOptions Privacy { get; set; } = new PrivacyOptions(); + /// + /// Probability from 0.0 to 1.0 that session replay starts when enabled. + /// Values at or below zero never record; values at or above one always record. + /// + public double SampleRate { get; set; } = 1.0; + + /// Target capture rate in frames per second. + public double FrameRate { get; set; } = 1.0; + + /// + /// Capture resolution: 1.0 = 1x (160 DPI), 2.0 = 2x, and so on. + /// Higher values capture more detail at the cost of larger frames. + /// + public double Scale { get; set; } = 1.0; + + /// + /// JPEG quality of exported frames, from 0.0 (smallest) to 1.0 (best). + /// Values outside that range are clamped by the native SDK. + /// + public double ImageQuality { get; set; } = 0.3; + public SessionReplayOptions() { } @@ -42,11 +63,19 @@ public SessionReplayOptions() public SessionReplayOptions( bool isEnabled = true, string serviceName = "sessionreplay-dotnet", - PrivacyOptions? privacy = null + PrivacyOptions? privacy = null, + double sampleRate = 1.0, + double frameRate = 1.0, + double scale = 1.0, + double imageQuality = 0.3 ) { IsEnabled = isEnabled; ServiceName = serviceName; Privacy = privacy ?? new PrivacyOptions(); + SampleRate = sampleRate; + FrameRate = frameRate; + Scale = scale; + ImageQuality = imageQuality; } } diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs b/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs index e1f9adc74d..1d2177cdaf 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs @@ -122,6 +122,7 @@ public static MauiApp CreateMauiApp() var client = LdClient.Init(ldConfig, context, TimeSpan.FromSeconds(0)); LDObserve.Init(client, observabilityOptions, replayOptions); + Console.WriteLine($"LD native version: {LDObserve.NativeVersion}"); // standalone variant (no LaunchDarkly client): //LDObserve.Init(mobileKey, observabilityOptions, replayOptions);