diff --git a/e2e/android/app/build.gradle.kts b/e2e/android/app/build.gradle.kts index a60539c958..65b96bdb1c 100644 --- a/e2e/android/app/build.gradle.kts +++ b/e2e/android/app/build.gradle.kts @@ -104,22 +104,22 @@ dependencies { implementation("com.launchdarkly:launchdarkly-android-client-sdk:5.11.0") - implementation("io.opentelemetry:opentelemetry-api:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk:1.51.0") - implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.51.0") - implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.51.0") + implementation("io.opentelemetry:opentelemetry-api:1.52.0") + implementation("io.opentelemetry:opentelemetry-sdk:1.52.0") + implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.52.0") + implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.52.0") // Android HTTP Url instrumentation - implementation("io.opentelemetry.android.instrumentation:httpurlconnection-library:0.11.0-alpha") - byteBuddy("io.opentelemetry.android.instrumentation:httpurlconnection-agent:0.11.0-alpha") + implementation("io.opentelemetry.android.instrumentation:httpurlconnection-library:0.13.0-alpha") + byteBuddy("io.opentelemetry.android.instrumentation:httpurlconnection-agent:0.13.0-alpha") // Used for accessing the SignalFromDiskExporter class in TestApplication - implementation("io.opentelemetry.android:core:0.11.0-alpha") + implementation("io.opentelemetry.android:core:0.13.0-alpha") // OkHTTP instrumentation - implementation("io.opentelemetry.android.instrumentation:okhttp3-library:0.11.0-alpha") - byteBuddy("io.opentelemetry.android.instrumentation:okhttp3-agent:0.11.0-alpha") - implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("io.opentelemetry.android.instrumentation:okhttp3-library:0.13.0-alpha") + byteBuddy("io.opentelemetry.android.instrumentation:okhttp3-agent:0.13.0-alpha") + implementation("com.squareup.okhttp3:okhttp:5.1.0") implementation("com.google.android.material:material:1.12.0") implementation("androidx.recyclerview:recyclerview:1.3.2") @@ -147,8 +147,8 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.core.ktx) testImplementation(libs.robolectric) - testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0") - testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.51.0") + testImplementation("com.squareup.okhttp3:mockwebserver:5.1.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.52.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") testImplementation(testFixtures(project(":observability-android"))) diff --git a/e2e/android/app/src/testCompose/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt b/e2e/android/app/src/testCompose/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt index 9efaa1251c..0de39e4c4c 100644 --- a/e2e/android/app/src/testCompose/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt +++ b/e2e/android/app/src/testCompose/java/com/example/androidobservability/DisablingConfigOptionsE2ETest.kt @@ -14,6 +14,7 @@ import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue +import okhttp3.mockwebserver.RecordedRequest import org.junit.After import org.junit.Ignore import org.junit.Test @@ -264,17 +265,21 @@ class DisablingConfigOptionsE2ETest { private fun requestsContainsUrl(url: String): Boolean { while (true) { val request = application.mockWebServer?.takeRequest(100, TimeUnit.MILLISECONDS) ?: return false - if (request.requestUrl.toString() == url) return true + if (recordedUrl(request) == url) return true } } private fun requestsStartWith(prefix: String): Boolean { while (true) { val request = application.mockWebServer?.takeRequest(100, TimeUnit.MILLISECONDS) ?: return false - if (request.requestUrl.toString().startsWith(prefix)) return true + if (recordedUrl(request).startsWith(prefix)) return true } } + // MockWebServer 5 records the host as 127.0.0.1, while the expected URLs use `localhost`. + private fun recordedUrl(request: RecordedRequest): String = + request.requestUrl?.newBuilder()?.host("localhost")?.build().toString() + private fun triggerTestLog(severity: Severity = Severity.INFO) { LDObserve.recordLog( message = TEST_LOG_MESSAGE, diff --git a/sdk/@launchdarkly/mobile-dotnet/.vscode/settings.json b/sdk/@launchdarkly/mobile-dotnet/.vscode/settings.json new file mode 100644 index 0000000000..a70fbfc50c --- /dev/null +++ b/sdk/@launchdarkly/mobile-dotnet/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "mobile-dotnet.sln" +} \ No newline at end of file diff --git a/sdk/@launchdarkly/mobile-dotnet/README.md b/sdk/@launchdarkly/mobile-dotnet/README.md index d24be4e430..39997fe6a8 100644 --- a/sdk/@launchdarkly/mobile-dotnet/README.md +++ b/sdk/@launchdarkly/mobile-dotnet/README.md @@ -11,19 +11,33 @@ The LaunchDarkly Observability SDK for .NET MAUI provides automatic and manual i ### Automatic Instrumentation The .NET MAUI observability plugin automatically instruments: -- **HTTP Requests**: Outgoing HTTP requests -- **Crash Reporting**: Automatic crash reporting and stack traces +- **HTTP Requests**: Outgoing HTTP requests made through `HttpClient` (`instrumentation.networkRequests`; requires `HttpActivityPropagationSupport`, see [Enabling HTTP request tracing](#enabling-http-request-tracing)) +- **Launch Times**: App launch performance (`instrumentation.launchTimes`) - **Feature Flag Evaluations**: Evaluation events added to your spans - **Session Management**: User session tracking and background timeout handling +Automatic crash reporting is not enabled yet; use `LDObserve.RecordError` to report exceptions. + ## Prerequisites -* **.NET 9.0** or higher is required. -* MAUI support for **iOS** and **Android**. +* **.NET 9** or **.NET 10** with the MAUI workload. +* **Android** API 23 or later and **iOS** 15 or later. + +### Enabling HTTP request tracing + +HTTP tracing needs one setting in your app's project file: + +```xml + + true + +``` + +The Android SDK sets this to `false` by default in every configuration, and the iOS SDK sets it to `false` in Release builds. While it is `false`, the trimmer removes the `System.Net.Http` diagnostics that the SDK listens to, so no HTTP spans are recorded even with `networkRequests: true`. The SDK cannot turn it back on at runtime. ## Example Application -A complete example application is available in the [sample](./sample) directory. +A complete example application is available in the [sample](./sample) directory. `MauiSample9.csproj` builds it for .NET 9 and `MauiSample10.csproj` for .NET 10; both share the same sources. ## Usage @@ -64,6 +78,21 @@ public static class MauiProgram } ``` +Alternatively, register the plugin yourself when building the client configuration. `ObservabilityPlugin` takes the same options, including optional `SessionReplayOptions` as its second argument: + +```csharp +using LaunchDarkly.Sdk.Client.Integrations; +using LaunchDarkly.SessionReplay; + +var ldConfig = Configuration.Builder(mobileKey, ConfigurationBuilder.AutoEnvAttributes.Enabled) + .Plugins(new PluginConfigurationBuilder() + .Add(new ObservabilityPlugin( + new ObservabilityOptions(isEnabled: true, serviceName: "maui-sample-app"), + new SessionReplayOptions(isEnabled: true)))) + .Build(); +var client = LdClient.Init(ldConfig, context, TimeSpan.FromSeconds(10)); +``` + #### Standalone (without a LaunchDarkly client) If you are not using the LaunchDarkly client, pass your mobile key directly. This boots the native observability stack without registering a plugin: @@ -162,7 +191,7 @@ LDObserve.RecordLog( ); ``` -Supported severity levels: `Trace`, `Debug`, `Info`, `Warn`, `Error`, `Fatal`. +Supported severity levels: `Trace`, `Debug`, `Info`, `Warn`, `Error`, `Fatal`, each with numbered variants (for example `Info2` to `Info4`) matching the OpenTelemetry severity numbers. ##### Logs with Span Context diff --git a/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj b/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj index 6c90d0f0ed..c786e9ac33 100644 --- a/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/android/LDObserve.Android.Binding/LDObserve.Android.Binding.csproj @@ -33,11 +33,6 @@ - - - - - - + diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml b/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml index 0ad042f527..c4914eb73f 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml +++ b/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml @@ -10,7 +10,7 @@ Shell.BackgroundColor="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}"> diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml.cs b/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml.cs index 04b167415b..c4897d5ce9 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml.cs +++ b/sdk/@launchdarkly/mobile-dotnet/sample/AppShell.xaml.cs @@ -6,8 +6,23 @@ public AppShell() { InitializeComponent(); + MainContent.Title = BuildTitle(); + Routing.RegisterRoute(nameof(CreditCardPage), typeof(CreditCardPage)); Routing.RegisterRoute(nameof(NumberPadPage), typeof(NumberPadPage)); Routing.RegisterRoute(nameof(DialogsPage), typeof(DialogsPage)); } + + // MauiSample9 and MauiSample10 share these sources, so the title is what tells the two + // builds apart on a device. + private static string BuildTitle() + { +#if DEBUG + const string configuration = "Debug"; +#else + const string configuration = "Release"; +#endif + var platform = DeviceInfo.Platform == DevicePlatform.WinUI ? "Windows" : DeviceInfo.Platform.ToString(); + return $"{platform} · .NET {Environment.Version.Major} · {configuration}"; + } } diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/DialogsPage.xaml.cs b/sdk/@launchdarkly/mobile-dotnet/sample/DialogsPage.xaml.cs index 00d9c2abf8..35d6b63e0b 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/DialogsPage.xaml.cs +++ b/sdk/@launchdarkly/mobile-dotnet/sample/DialogsPage.xaml.cs @@ -1,4 +1,8 @@ using CommunityToolkit.Maui.Views; +#if NET10_0_OR_GREATER +// Toolkit v12 moved ShowPopupAsync out of CommunityToolkit.Maui.Views. +using CommunityToolkit.Maui.Extensions; +#endif using System.Linq; using System.Threading; #if IOS @@ -236,7 +240,13 @@ private async void OnToolkitPopupSheetClicked(object? sender, EventArgs e) BackgroundColor = Color.FromArgb("#F2B8B5"), TextColor = Colors.White }; + // CommunityToolkit.Maui replaced Popup.Close() with CloseAsync() in v12. The .NET 9 build + // stays on toolkit v11, which caps Microsoft.Maui.Essentials below 10.0.0. +#if NET10_0_OR_GREATER + closeBtn.Clicked += async (s, args) => await popup.CloseAsync(); +#else closeBtn.Clicked += (s, _) => popup.Close(); +#endif popup.Content = new VerticalStackLayout { @@ -268,8 +278,12 @@ private async void OnToolkitPopupSheetClicked(object? sender, EventArgs e) _ = this.ShowPopupAsync(popup); await AutoClose(async () => { +#if NET10_0_OR_GREATER + await popup.CloseAsync(); +#else popup.Close(); await Task.CompletedTask; +#endif }); } diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj new file mode 100644 index 0000000000..0f51bc7794 --- /dev/null +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample10.csproj @@ -0,0 +1,155 @@ + + + + + obj\MauiSample10\ + bin\MauiSample10\ + + + + + + net10.0-android;net10.0-ios + + Exe + MauiSample9 + true + true + enable + enable + + true + + + $(DefaultItemExcludes);bin\**;obj\** + + + MauiSample10 + + + com.companyname.mauisample10 + + + 1.0 + 1 + + 15.0 + 23.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + true + + + + true + + false + + + + + diff --git a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample9.csproj b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample9.csproj index 26158f44ff..7625969d6d 100644 --- a/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample9.csproj +++ b/sdk/@launchdarkly/mobile-dotnet/sample/MauiSample9.csproj @@ -84,6 +84,13 @@ true + + + true + + true