Skip to content

Commit 0e2482b

Browse files
feat: MAUI Network tracing 0.8.0 (#463)
## Summary - **Network request tracing for .NET MAUI**: Adds `System.Net.Http` Activity source to `LDTracer` so HTTP requests made from .NET are automatically captured as spans. On iOS, native `urlSession` instrumentation is disabled since tracing now happens on the .NET side. On Android, `UseNativeHttpHandler` is set to `false` in the sample project so `SocketsHttpHandler` emits Activity diagnostics. - **Configurable instrumentation options**: Introduces `InstrumentationOptions` (with `NetworkRequests` and `LaunchTimes` toggles) on `ObservabilityOptions`, plumbed through the iOS (Swift) and Android (Kotlin) native bridges so each instrumentation can be individually enabled/disabled from the .NET layer. - **Span-context-aware logging**: `LDObserve.RecordLog` now accepts an optional `SpanContext?` parameter. When provided, its trace/span IDs are forwarded to the native logger; otherwise the ambient `Activity.Current` is used. The native logger is obtained via new `ObjcLogger` (iOS) and `RealLogger` (Android) bridge classes, replacing the old `recordLog` method on `ObservabilityBridge`. - **New public APIs on `LDObserve`**: `RecordError(Exception)`, `StartRootSpan(string)`, and `GetTracer()` (with no-op fallback before init). - **Android SDK**: `TracesApi.startSpan` now defaults `attributes` to `Attributes.empty()`. - **E2E example**: Android `ViewModel` demonstrates parent/child span creation and cross-thread context propagation. - **Version bump**: `LaunchDarkly.SessionReplay` 0.6.0 → 0.8.0. <img width="1592" height="1740" alt="image" src="https://github.com/user-attachments/assets/7f95d3e1-db90-4607-bf8d-48f0706b5716" /> ## Changes by area ### .NET SDK (`mobile-dotnet/observability`) - New `InstrumentationOptions` class with `NetworkRequests` and `LaunchTimes` booleans (default `true`), added to `ObservabilityOptions`. - `ObservabilityPlugin` enables `System.Net.Http.EnableActivityPropagation` when network request tracing is on. - `LDTracer` is no longer a singleton; accepts `serviceName` and `networkRequests` params; conditionally subscribes to `System.Net.Http` Activity source. - New `ObservabilityService` consolidates all bridge calls, tracer lifecycle, and native logger acquisition. Replaces deleted `NativeObserve`. - `LDObserve` is now a thin static facade over `ObservabilityService`; safe to call before initialization (no-ops). - `PluginOrchestrator` renamed properties to `ObservabilityService` / `SessionReplayService`; calls `LDObserve.Initialize()` after native start. - Native bridge mappings (`LDNative.Android.cs`, `SRClient.iOS.cs`) pass new instrumentation options through. ### Native bridges — iOS (Swift) - `ObjcObservabilityOptions` adds `networkRequests` and `launchTimes` properties. - `ObservabilityBridge` disables `urlSession` instrumentation and makes `launchTimes` configurable. - New `ObjcLogger` class exposed via `LDObserveBridge.getObjcLogger()`. - iOS binding `ApiDefinition.cs` updated with new properties and `ObjcLogger` interface. ### Native bridges — Android (Kotlin) - `LDObservabilityOptions` adds `launchTime` field; options classes moved to `OptionsBridge.kt`. - `ObservabilityBridge` reads `launchTime` from options instead of hardcoding `true`; `recordLog` removed. - New `RealLogger` wrapper around `KotlinLogger` for C# binding. - `LDObserveBridgeAdapter` exposes `getLogger()`. ### Android SDK (`observability-android`) - `TracesApi.startSpan` parameter `attributes` now defaults to `Attributes.empty()`. ### E2E / Sample - Android e2e `ViewModel` demonstrates parent-child span creation and cross-thread context propagation. - MAUI sample `MauiProgram.cs` uses the new `InstrumentationOptions` API. - MAUI sample `.csproj` sets `UseNativeHttpHandler=false` for Android to enable `System.Net.Http` Activity diagnostics. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches cross-platform observability initialization and tracing sources (including automatic `System.Net.Http` span capture), so misconfiguration could change telemetry volume/correlation or miss spans on iOS/Android. > > **Overview** > Adds **automatic .NET MAUI HTTP request tracing** by extending `LDTracer` to optionally subscribe to the `System.Net.Http` Activity source and enabling `System.Net.Http.EnableActivityPropagation` when configured. > > Introduces `InstrumentationOptions` on `ObservabilityOptions` (toggles for `NetworkRequests` and `LaunchTimes`) and plumbs these through the Android (Kotlin) and iOS (Swift + binding) native bridges; iOS disables native `urlSession` tracing in favor of .NET-side tracing, and Android bridge now respects a configurable `launchTime` flag. > > Hardens initialization by making `PluginOrchestrator` initialize only once, updates samples/e2e to demonstrate cross-thread span context propagation and config usage (including Android `UseNativeHttpHandler=false`), and bumps `LaunchDarkly.SessionReplay` package version to `0.8.0`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit abda519. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 467105a commit 0e2482b

18 files changed

Lines changed: 89 additions & 16 deletions

File tree

e2e/android/app/src/main/java/com/example/androidobservability/ViewModel.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import com.launchdarkly.sdk.android.LDClient
1414
import io.opentelemetry.api.common.AttributeKey
1515
import io.opentelemetry.api.common.Attributes
1616
import io.opentelemetry.api.logs.Severity
17+
import io.opentelemetry.api.trace.Span
18+
import io.opentelemetry.context.Context
1719
import kotlinx.coroutines.Dispatchers
1820
import kotlinx.coroutines.launch
1921
import okhttp3.OkHttpClient
@@ -22,6 +24,7 @@ import java.io.BufferedInputStream
2224
import java.net.HttpURLConnection
2325
import java.net.URL
2426

27+
2528
class ViewModel(application: Application) : AndroidViewModel(application) {
2629

2730
fun triggerMetric() {
@@ -83,6 +86,23 @@ class ViewModel(application: Application) : AndroidViewModel(application) {
8386

8487
fun triggerLogWithContext(message: String) {
8588
val text = message.ifEmpty { "Log with span context" }
89+
90+
val parentSpan = LDObserve.startSpan("parentSpan")
91+
parentSpan.makeCurrent().use {
92+
val context = Context.current()
93+
94+
Thread {
95+
context.makeCurrent().use {
96+
val childSpan = LDObserve.startSpan("childSpan")
97+
childSpan.makeCurrent().use {
98+
// do work
99+
childSpan.end()
100+
}
101+
}
102+
}.start()
103+
}
104+
parentSpan.end()
105+
86106
viewModelScope.launch(Dispatchers.IO) {
87107
val span = LDObserve.startSpan(
88108
name = "log-context-demo",
@@ -97,6 +117,10 @@ class ViewModel(application: Application) : AndroidViewModel(application) {
97117
// Simulate a detached thread where OTel context is lost automatically.
98118
// Span.current() here returns INVALID, so we pass the captured context explicitly.
99119
Thread {
120+
Span.wrap(capturedContext).makeCurrent().use {
121+
val childSpan = LDObserve.startSpan("child of log-context-demo", Attributes.empty())
122+
childSpan.end()
123+
}
100124
LDObserve.recordLog(
101125
message = text,
102126
severity = Severity.WARN,

sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/ObservabilityBridge.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public class ObservabilityBridge(
8484
tracesApi = com.launchdarkly.observability.api.ObservabilityOptions.TracesApi(includeErrors = true, includeSpans = true),
8585
metricsApi = com.launchdarkly.observability.api.ObservabilityOptions.MetricsApi.enabled(),
8686
instrumentations = com.launchdarkly.observability.api.ObservabilityOptions.Instrumentations(
87-
crashReporting = false, launchTime = true, activityLifecycle = true
87+
crashReporting = false, launchTime = observability.launchTime, activityLifecycle = true
8888
),
8989
logAdapter = LDAndroidLogging.adapter(),
9090
)

sdk/@launchdarkly/mobile-dotnet/android/native/LDObserve/src/main/java/com/example/LDObserve/OptionsBridge.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class LDObservabilityOptions {
88
@JvmField var backendUrl: String = ""
99
@JvmField var contextFriendlyName: String? = null
1010
@JvmField var attributes: HashMap<String, Any?>? = null
11+
@JvmField var launchTime: Boolean = true
1112

1213
constructor()
1314

@@ -18,7 +19,8 @@ public class LDObservabilityOptions {
1819
otlpEndpoint: String,
1920
backendUrl: String,
2021
contextFriendlyName: String?,
21-
attributes: HashMap<String, Any?>? = null
22+
attributes: HashMap<String, Any?>? = null,
23+
launchTime: Boolean = true
2224
) {
2325
this.isEnabled = isEnabled
2426
this.serviceName = serviceName
@@ -27,6 +29,7 @@ public class LDObservabilityOptions {
2729
this.backendUrl = backendUrl
2830
this.contextFriendlyName = contextFriendlyName
2931
this.attributes = attributes
32+
this.launchTime = launchTime
3033
}
3134
}
3235

sdk/@launchdarkly/mobile-dotnet/macios/LDObserve.MaciOS.Binding/ApiDefinition.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ interface ObjcObservabilityOptions
3030

3131
[NullAllowed, Export("attributes")]
3232
NSDictionary Attributes { get; set; }
33+
34+
[Export("networkRequests")]
35+
bool NetworkRequests { get; set; }
36+
37+
[Export("launchTimes")]
38+
bool LaunchTimes { get; set; }
3339
}
3440

3541
[BaseType(typeof(NSObject))]

sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/ObservabilityBridge.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,12 @@ public final class ObservabilityBridge: NSObject {
4949
resourceAttributes: buildResourceAttributes(observability.attributes),
5050
crashReporting: .init(source: .none),
5151
instrumentation: .init(
52-
urlSession: .enabled,
52+
urlSession: .disabled, // Network tracing happens on the .NET side via System.Net.Http activities.
5353
userTaps: .enabled,
5454
memory: .disabled,
5555
memoryWarnings: .disabled,
5656
cpu: .disabled,
57-
launchTimes: .enabled
57+
launchTimes: observability.launchTimes ? .enabled : .disabled
5858
)
5959
))
6060
observabilityPlugin.distroAttributes = [

sdk/@launchdarkly/mobile-dotnet/macios/native/LDObserve/Sources/OptionsBridge.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public final class ObjcObservabilityOptions: NSObject {
77
@objc public var otlpEndpoint: String = ""
88
@objc public var backendUrl: String = ""
99
@objc public var attributes: NSDictionary?
10+
@objc public var networkRequests: Bool = true
11+
@objc public var launchTimes: Bool = true
1012

1113
@objc public override init() {
1214
super.init()

sdk/@launchdarkly/mobile-dotnet/observability/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<PackageId>LaunchDarkly.SessionReplay</PackageId>
4-
<Version>0.6.0</Version>
4+
<Version>0.8.0</Version>
55
<UseLocalClientSdk>false</UseLocalClientSdk>
66
<Authors>LaunchDarkly</Authors>
77
<Owners>LaunchDarkly</Owners>

sdk/@launchdarkly/mobile-dotnet/observability/bridge/LDNative.Android.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ internal static class LDNativeAndroidMapping
1414
options.OtlpEndpoint,
1515
options.BackendUrl,
1616
options.ContextFriendlyName,
17-
DictionaryTypeConverters.ToJavaDictionary(options.Attributes)
17+
DictionaryTypeConverters.ToJavaDictionary(options.Attributes),
18+
options.Instrumentation.LaunchTimes
1819
);
1920
}
2021

sdk/@launchdarkly/mobile-dotnet/observability/bridge/SRClient.iOS.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public void Start(string mobileKey, ObservabilityOptions observability, SessionR
2626
ServiceVersion = observability.ServiceVersion ?? "0.1.0",
2727
OtlpEndpoint = observability.OtlpEndpoint ?? "https://otel.observability.app.launchdarkly.com:4318",
2828
BackendUrl = observability.BackendUrl ?? "https://pub.observability.app.launchdarkly.com",
29-
Attributes = DictionaryTypeConverters.ToNSDictionary(observability.Attributes)
29+
Attributes = DictionaryTypeConverters.ToNSDictionary(observability.Attributes),
30+
NetworkRequests = observability.Instrumentation.NetworkRequests,
31+
LaunchTimes = observability.Instrumentation.LaunchTimes
3032
};
3133

3234
var objcReplay = new ObjcSessionReplayOptions

sdk/@launchdarkly/mobile-dotnet/observability/infra/PluginOrchestrator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal sealed class PluginOrchestrator
1616

1717
private int _createdCount;
1818
private int _registeredCount;
19+
private bool _initialized;
1920

2021
internal ObservabilityService? ObservabilityService { get; private set; }
2122
internal SessionReplayService? SessionReplayService { get; private set; }
@@ -24,7 +25,8 @@ private PluginOrchestrator() { }
2425

2526
private void InitializeAll()
2627
{
27-
if (_registeredCount < _createdCount) return;
28+
if (_initialized || _registeredCount < _createdCount) return;
29+
_initialized = true;
2830

2931
var metadata = ObservabilityService?.Metadata ?? SessionReplayService?.Metadata;
3032
if (metadata == null) return;

0 commit comments

Comments
 (0)